From a0409eef8c3c2f63b149fe871f82eaefb6383898 Mon Sep 17 00:00:00 2001 From: Xurkon Date: Fri, 5 Jun 2026 20:15:26 -0500 Subject: [PATCH] feat: add learner/static data source mode --- Database/QuestieDB.lua | 10 +- .../DatabaseTab/QuestieOptionsDatabase.lua | 54 +++++ Modules/Options/QuestieOptionsDefaults.lua | 3 + Modules/Quest/QuestieQuest.lua | 9 +- Modules/Quest/QuestieQuestPrivates.lua | 38 +++- Modules/QuestieLearner.lua | 203 +++++++++++++++--- Modules/Tooltips/Tooltip.lua | 3 +- Tests/QuestieLearnerDataSourceMode_spec.lua | 39 ++++ 8 files changed, 328 insertions(+), 31 deletions(-) create mode 100644 Tests/QuestieLearnerDataSourceMode_spec.lua diff --git a/Database/QuestieDB.lua b/Database/QuestieDB.lua index 36d6a0e..f121194 100644 --- a/Database/QuestieDB.lua +++ b/Database/QuestieDB.lua @@ -819,7 +819,9 @@ end function QuestieDB.GetSuppressedNPCs(zoneId) local suppressed = {} local ld = Questie.dbLearner.global - if ld and ld.settings and ld.settings.enabled and ld.settings.prioritizeMyData and ld.npcs then + local mode = ld and ld.settings and ld.settings.dataSourceMode or "auto" + if mode == "auto" or mode == "learner" then + if ld and ld.settings and ld.settings.enabled and ld.npcs then local threshold = ld.settings.minConfidencePins or 2 local npcId, entry = next(ld.npcs) while npcId do @@ -829,6 +831,7 @@ function QuestieDB.GetSuppressedNPCs(zoneId) end npcId, entry = next(ld.npcs, npcId) end + end end return suppressed end @@ -840,7 +843,9 @@ end function QuestieDB.GetSuppressedObjects(zoneId) local suppressed = {} local ld = Questie.dbLearner.global - if ld and ld.settings and ld.settings.enabled and ld.settings.prioritizeMyData and ld.objects then + local mode = ld and ld.settings and ld.settings.dataSourceMode or "auto" + if mode == "auto" or mode == "learner" then + if ld and ld.settings and ld.settings.enabled and ld.objects then local threshold = ld.settings.minConfidencePins or 2 local objId, entry = next(ld.objects) while objId do @@ -850,6 +855,7 @@ function QuestieDB.GetSuppressedObjects(zoneId) end objId, entry = next(ld.objects, objId) end + end end return suppressed end diff --git a/Modules/Options/DatabaseTab/QuestieOptionsDatabase.lua b/Modules/Options/DatabaseTab/QuestieOptionsDatabase.lua index a27132f..ceb6d3a 100644 --- a/Modules/Options/DatabaseTab/QuestieOptionsDatabase.lua +++ b/Modules/Options/DatabaseTab/QuestieOptionsDatabase.lua @@ -46,6 +46,23 @@ local function GetLearnedCounts() return s end +local function ApplyLearnerMode() + local QuestieLearner = QuestieLoader:ImportModule("QuestieLearner") + if QuestieLearner and QuestieLearner.ApplyDataSourceMode then + QuestieLearner:ApplyDataSourceMode() + end + + local QuestieEventHandler = QuestieLoader:ImportModule("QuestieEventHandler") + if QuestieEventHandler and QuestieEventHandler.UpdateAllQuests then + QuestieEventHandler:UpdateAllQuests() + end + + local QuestieTracker = QuestieLoader:ImportModule("QuestieTracker") + if QuestieTracker and QuestieTracker.Update then + QuestieTracker:Update() + end +end + ----------------------------------------------------------------------- -- Export Dialog ----------------------------------------------------------------------- @@ -204,6 +221,43 @@ function QuestieOptions.tabs.database:Initialize() name = function() return l10n("What To Learn") end, }, + learner_enabled = { + type = "toggle", + order = 2.05, + name = function() return l10n("Enable Learner Recording") end, + desc = function() return l10n("Record live learner data. Disable this to stop recording and live learner injection.") end, + get = function() return Questie.dbLearner.global and Questie.dbLearner.global.settings and Questie.dbLearner.global.settings.enabled end, + set = function(_, v) + if Questie.dbLearner.global and Questie.dbLearner.global.settings then + Questie.dbLearner.global.settings.enabled = v + ApplyLearnerMode() + end + end, + }, + + data_source_mode = { + type = "select", + order = 2.06, + name = function() return l10n("Data Source Mode") end, + desc = function() return l10n("Choose whether Questie should display learner data, static database data, both, or neither. Learner recording can stay enabled independently.") end, + values = { + auto = l10n("Auto (current behavior)"), + learner = l10n("Learner Only"), + static = l10n("Static Only"), + none = l10n("Neither (base DB only)"), + }, + get = function() + return (Questie.dbLearner.global and Questie.dbLearner.global.settings and Questie.dbLearner.global.settings.dataSourceMode) or "auto" + end, + set = function(_, v) + if Questie.dbLearner.global and Questie.dbLearner.global.settings then + Questie.dbLearner.global.settings.dataSourceMode = v + Questie.dbLearner.global.settings.prioritizeMyData = (v ~= "static" and v ~= "none") + ApplyLearnerMode() + end + end, + }, + learn_npcs = { type = "toggle", order = 2.1, diff --git a/Modules/Options/QuestieOptionsDefaults.lua b/Modules/Options/QuestieOptionsDefaults.lua index 6ec3204..618aa6a 100644 --- a/Modules/Options/QuestieOptionsDefaults.lua +++ b/Modules/Options/QuestieOptionsDefaults.lua @@ -87,6 +87,9 @@ function QuestieOptionsDefaults:Load() learnerBroadcast = true, enableMapIcons = true, enableMiniMapIcons = true, + -- Learner source preference (Database tab) + -- auto = current behavior, learner = learner only, static = static only, none = base DB only + dataSourceMode = "auto", questieShutUp = false, bugWorkarounds = true, hideIconsOnContinents = false, diff --git a/Modules/Quest/QuestieQuest.lua b/Modules/Quest/QuestieQuest.lua index 6272781..9030ea6 100644 --- a/Modules/Quest/QuestieQuest.lua +++ b/Modules/Quest/QuestieQuest.lua @@ -1581,8 +1581,13 @@ function QuestieQuest:PopulateObjective(quest, objectiveIndex, objective, blockI objectiveCenter = { x = x, y = y } end - -- Filter static spawns if prioritizeMyData is enabled and we have high-confidence learned data - if Questie.dbLearner and Questie.dbLearner.global and Questie.dbLearner.global.settings and Questie.dbLearner.global.settings.enabled and Questie.dbLearner.global.settings.prioritizeMyData then + -- Filter static spawns only when the learner is allowed to influence display. + local dataSourceMode = Questie.dbLearner + and Questie.dbLearner.global + and Questie.dbLearner.global.settings + and Questie.dbLearner.global.settings.dataSourceMode or "auto" + if Questie.dbLearner and Questie.dbLearner.global and Questie.dbLearner.global.settings and Questie.dbLearner.global.settings.enabled + and (dataSourceMode == "auto" or dataSourceMode == "learner") then local zone, _ = next(zones) while zone do local suppressed = (objectiveData.Type == "monster" and QuestieDB.GetSuppressedNPCs(zone)) or (objectiveData.Type == "object" and QuestieDB.GetSuppressedObjects(zone)) diff --git a/Modules/Quest/QuestieQuestPrivates.lua b/Modules/Quest/QuestieQuestPrivates.lua index 209e4b5..3253c30 100644 --- a/Modules/Quest/QuestieQuestPrivates.lua +++ b/Modules/Quest/QuestieQuestPrivates.lua @@ -172,14 +172,23 @@ monster = function(npcId, objective) end local isLearned = false + local dataSourceMode = Questie.dbLearner + and Questie.dbLearner.global + and Questie.dbLearner.global.settings + and Questie.dbLearner.global.settings.dataSourceMode or "auto" + + if dataSourceMode == "none" or dataSourceMode == "learner" then + spawns = {} + end -- Learner safety net: when prioritizeMyData is enabled and the Learner has -- verified spawn data for this NPC, prefer it over compiled DB spawns. -- This catches edge cases where the npcDataOverrides chain doesn't fully -- replace retail positions (e.g. format migration gaps, timing issues). - if Questie.IsAscension and Questie.dbLearner and Questie.dbLearner.global then + if Questie.IsAscension and Questie.dbLearner and Questie.dbLearner.global + and (dataSourceMode == "auto" or dataSourceMode == "learner") then local ld = Questie.dbLearner.global - if ld.settings and ld.settings.enabled and ld.settings.prioritizeMyData then + if ld.settings and ld.settings.enabled then local learnedNpc = ld.npcs and ld.npcs[npcId] if learnedNpc then local learnedSpawns = learnedNpc[7] @@ -253,6 +262,30 @@ object = function(objectId, objective) spawns = {} end + local dataSourceMode = Questie.dbLearner + and Questie.dbLearner.global + and Questie.dbLearner.global.settings + and Questie.dbLearner.global.settings.dataSourceMode or "auto" + local isLearned = false + if dataSourceMode == "none" or dataSourceMode == "learner" then + spawns = {} + end + + if Questie.IsAscension and Questie.dbLearner and Questie.dbLearner.global + and (dataSourceMode == "auto" or dataSourceMode == "learner") then + local ld = Questie.dbLearner.global + if ld.settings and ld.settings.enabled then + local learnedObj = ld.objects and ld.objects[objectId] + if learnedObj and learnedObj[4] and next(learnedObj[4]) then + local threshold = ld.settings.minConfidencePins or 1 + if learnedObj.mc and learnedObj.mc >= threshold then + spawns = learnedObj[4] + isLearned = true + end + end + end + end + ---@type SpawnListObject local retObject = { @@ -263,6 +296,7 @@ object = function(objectId, objective) GetIconScale = _GetIconScaleForObject, IconScale = _GetIconScaleForObject(), TooltipKey = "o_" .. objectId, + isLearned = isLearned, } return { diff --git a/Modules/QuestieLearner.lua b/Modules/QuestieLearner.lua index a5a1ea0..2e855f5 100644 --- a/Modules/QuestieLearner.lua +++ b/Modules/QuestieLearner.lua @@ -16,6 +16,9 @@ local ZoneDB = QuestieLoader:ImportModule("ZoneDB") local _Learner = QuestieLearner.private or {} +local GetDataSourceMode +local DeepCopy + local floor = math.floor local abs = math.abs local time = time @@ -32,6 +35,10 @@ local string_len = string.len local string_upper = string.upper local function IsAscensionProtected(dbType, id, key) + local mode = GetDataSourceMode and GetDataSourceMode() or "auto" + if mode == "learner" or mode == "none" then + return false + end local protected = QuestieDB and QuestieDB.ascensionOverrideKeys and QuestieDB.ascensionOverrideKeys[dbType] @@ -44,6 +51,42 @@ local function HasAscensionQuestObjectiveData(questId) return IsAscensionProtected("QUEST", questId, 10) end +local function GetDataSourceMode() + local settings = Questie + and Questie.dbLearner + and Questie.dbLearner.global + and Questie.dbLearner.global.settings + if not settings then + return "auto" + end + + local mode = settings.dataSourceMode + if mode == "auto" or mode == "learner" or mode == "static" or mode == "none" then + return mode + end + + if settings.prioritizeMyData == false then + return "static" + end + + return "auto" +end + +local function WipeTable(tbl) + if type(tbl) ~= "table" then return end + for key in pairs(tbl) do + tbl[key] = nil + end +end + +local function CopyTable(dst, src) + WipeTable(dst) + if type(src) ~= "table" then return end + for key, value in pairs(src) do + dst[key] = DeepCopy(value) + end +end + local function NormalizeSpawnZoneKey(zoneKey) -- Convert raw area IDs (e.g. 3431 from GetAreaID()) to the canonical map IDs -- used by AscensionDB and the rendering system (e.g. 1241 for Sunstrider Isle). @@ -226,6 +269,39 @@ local function DeepCopy(value) return copy end +local function CaptureStaticOverrideSnapshot() + if _Learner.staticOverrideSnapshot then + return + end + + _Learner.staticOverrideSnapshot = { + npcs = DeepCopy(QuestieDB.npcDataOverrides or {}), + quests = DeepCopy(QuestieDB.questDataOverrides or {}), + items = DeepCopy(QuestieDB.itemDataOverrides or {}), + objects = DeepCopy(QuestieDB.objectDataOverrides or {}), + } +end + +local function RestoreStaticOverridesForMode() + local snapshot = _Learner.staticOverrideSnapshot + local mode = GetDataSourceMode() + + if mode == "learner" or mode == "none" then + WipeTable(QuestieDB.npcDataOverrides) + WipeTable(QuestieDB.questDataOverrides) + WipeTable(QuestieDB.itemDataOverrides) + WipeTable(QuestieDB.objectDataOverrides) + return + end + + if not snapshot then return end + + CopyTable(QuestieDB.npcDataOverrides, snapshot.npcs) + CopyTable(QuestieDB.questDataOverrides, snapshot.quests) + CopyTable(QuestieDB.itemDataOverrides, snapshot.items) + CopyTable(QuestieDB.objectDataOverrides, snapshot.objects) +end + -- Returns the grid-bucket key for a coordinate so nearby points share the same slot local function CoordBucket(x, y) return floor(x / COORD_GRID) * COORD_GRID, floor(y / COORD_GRID) * COORD_GRID @@ -323,6 +399,7 @@ local function EnsureLearnedData() learnObjects = true, minConfidencePins = 1, prioritizeMyData = true, + dataSourceMode = "auto", staleThreshold = 90, -- days pruneVerified = false, -- protect verified data by default performanceMode = "balanced", @@ -347,6 +424,18 @@ local function EnsureLearnedData() if s.learnObjects == nil then s.learnObjects = true end if s.minConfidencePins == nil then s.minConfidencePins = 1 end if s.prioritizeMyData == nil then s.prioritizeMyData = true end + if s.dataSourceMode == nil then + if s.prioritizeMyData == false then + s.dataSourceMode = "static" + else + s.dataSourceMode = "auto" + end + end + if s.dataSourceMode == "static" or s.dataSourceMode == "none" then + s.prioritizeMyData = false + else + s.prioritizeMyData = true + end if s.staleThreshold == nil then s.staleThreshold = 90 end @@ -389,6 +478,30 @@ function QuestieLearner:GetSettings() return Questie.dbLearner.global.settings end +function QuestieLearner:GetDataSourceMode() + if not EnsureLearnedData() then return "auto" end + return GetDataSourceMode() +end + +function QuestieLearner:IsLearnerLiveEnabled() + local mode = self:GetDataSourceMode() + return mode == "auto" or mode == "learner" +end + +function QuestieLearner:ApplyDataSourceMode() + if not EnsureLearnedData() then return end + CaptureStaticOverrideSnapshot() + RestoreStaticOverridesForMode() + self:InjectLearnedData() + if QuestieDB and QuestieDB.private then + QuestieDB.private.questCache = {} + QuestieDB.private.itemCache = {} + QuestieDB.private.npcCache = {} + QuestieDB.private.objectCache = {} + end + QuestieLearner.data = Questie.dbLearner.global +end + local function GetLearnerSetting(key, defaultValue) if Questie and Questie.dbLearner and Questie.dbLearner.global and Questie.dbLearner.global.settings then local value = Questie.dbLearner.global.settings[key] @@ -787,6 +900,9 @@ local function _FlushNpcLiveUpdates() end local function _QueueNpcLiveUpdate(npcId) + if not (QuestieLearner and QuestieLearner.IsLearnerLiveEnabled and QuestieLearner:IsLearnerLiveEnabled()) then + return + end _Learner.pendingNpcLiveUpdates = _Learner.pendingNpcLiveUpdates or {} _Learner.pendingNpcLiveUpdates[npcId] = true @@ -861,10 +977,11 @@ local function CrossLinkAfterNPC(npcId) local learned = _GetDB() local npcData = learned.npcs[npcId] if not npcData then return end - local npcOvr = QuestieDB and QuestieDB.npcDataOverrides + local liveEnabled = QuestieLearner and QuestieLearner.IsLearnerLiveEnabled and QuestieLearner:IsLearnerLiveEnabled() + local npcOvr = liveEnabled and QuestieDB and QuestieDB.npcDataOverrides or nil for questId, qData in pairs(learned.quests) do - local qOvr = QuestieDB and QuestieDB.questDataOverrides + local qOvr = liveEnabled and QuestieDB and QuestieDB.questDataOverrides or nil -- Quest starters: quest[2][1] lists NPCs that start this quest if qData[2] and qData[2][1] then @@ -931,9 +1048,10 @@ local function CrossLinkAfterQuest(questId) local learned = _GetDB() local qData = learned.quests[questId] if not qData then return end - local qOvr = QuestieDB and QuestieDB.questDataOverrides - local npcOvr = QuestieDB and QuestieDB.npcDataOverrides - local objOvr = QuestieDB and QuestieDB.objectDataOverrides + local liveEnabled = QuestieLearner and QuestieLearner.IsLearnerLiveEnabled and QuestieLearner:IsLearnerLiveEnabled() + local qOvr = liveEnabled and QuestieDB and QuestieDB.questDataOverrides or nil + local npcOvr = liveEnabled and QuestieDB and QuestieDB.npcDataOverrides or nil + local objOvr = liveEnabled and QuestieDB and QuestieDB.objectDataOverrides or nil -- Starter NPCs: quest[2][1] → npc[10] if qData[2] and qData[2][1] then @@ -973,7 +1091,7 @@ local function CrossLinkAfterQuest(questId) if iData then if not iData[5] then iData[5] = questId - if QuestieDB and QuestieDB.itemDataOverrides then + if liveEnabled and QuestieDB and QuestieDB.itemDataOverrides then local ovr = QuestieDB.itemDataOverrides[qData[11]] or {} QuestieDB.itemDataOverrides[qData[11]] = ovr if not ovr[5] then ovr[5] = questId end @@ -1003,8 +1121,9 @@ local function CrossLinkAfterObject(objectId) local learned = _GetDB() local objData = learned.objects[objectId] if not objData then return end - local objOvr = QuestieDB and QuestieDB.objectDataOverrides - local qOvr = QuestieDB and QuestieDB.questDataOverrides + local liveEnabled = QuestieLearner and QuestieLearner.IsLearnerLiveEnabled and QuestieLearner:IsLearnerLiveEnabled() + local objOvr = liveEnabled and QuestieDB and QuestieDB.objectDataOverrides or nil + local qOvr = liveEnabled and QuestieDB and QuestieDB.questDataOverrides or nil for questId, qData in pairs(learned.quests) do -- Object starters: quest[2][2] @@ -1054,7 +1173,8 @@ local function CrossLinkAfterItem(itemId) local learned = _GetDB() local iData = learned.items[itemId] if not iData then return end - local qOvr = QuestieDB and QuestieDB.questDataOverrides + local liveEnabled = QuestieLearner and QuestieLearner.IsLearnerLiveEnabled and QuestieLearner:IsLearnerLiveEnabled() + local qOvr = liveEnabled and QuestieDB and QuestieDB.questDataOverrides or nil -- If this item starts a quest (item[5]=startQuest), ensure that quest knows -- about it via quest[2][3] (starter items slot) @@ -1062,7 +1182,7 @@ local function CrossLinkAfterItem(itemId) if qData[11] == itemId then if not iData[5] then iData[5] = questId - if QuestieDB and QuestieDB.itemDataOverrides then + if liveEnabled and QuestieDB and QuestieDB.itemDataOverrides then local ovr = QuestieDB.itemDataOverrides[itemId] or {} QuestieDB.itemDataOverrides[itemId] = ovr if not ovr[5] then ovr[5] = questId end @@ -1090,9 +1210,10 @@ end local function CrossLinkAfterQuestGiver(questId, entityId, typeSlot, isStart) local learned = _GetDB() local qData = learned.quests[questId] - local npcOvr = QuestieDB and QuestieDB.npcDataOverrides - local objOvr = QuestieDB and QuestieDB.objectDataOverrides - local qOvr = QuestieDB and QuestieDB.questDataOverrides + local liveEnabled = QuestieLearner and QuestieLearner.IsLearnerLiveEnabled and QuestieLearner:IsLearnerLiveEnabled() + local npcOvr = liveEnabled and QuestieDB and QuestieDB.npcDataOverrides or nil + local objOvr = liveEnabled and QuestieDB and QuestieDB.objectDataOverrides or nil + local qOvr = liveEnabled and QuestieDB and QuestieDB.questDataOverrides or nil if typeSlot == 1 then -- NPC ↔ quest @@ -1180,7 +1301,9 @@ function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionS -- Live injection is intentionally batched: repeated kill/log/loot events for -- the same NPC update saved evidence immediately, then flush QuestieDB once. - _QueueNpcLiveUpdate(npcId) + if self:IsLearnerLiveEnabled() then + _QueueNpcLiveUpdate(npcId) + end if isNew then Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] New NPC learned:", npcId, name or "?") @@ -1589,7 +1712,7 @@ function QuestieLearner:LearnQuest(questId, data) existing.mc = (existing.mc or 0) + 1 -- Live injection into questDataOverrides so GetQuest works without reload - if QuestieDB and QuestieDB.questDataOverrides then + if self:IsLearnerLiveEnabled() and QuestieDB and QuestieDB.questDataOverrides then local ovr = QuestieDB.questDataOverrides[questId] if not ovr then QuestieDB.questDataOverrides[questId] = existing @@ -1633,7 +1756,7 @@ function QuestieLearner:LearnQuestGiver(questId, entityId, entityType, isStart) table.insert(list, entityId) -- Live injection into questDataOverrides so starters/finishers take effect without reload - if QuestieDB and QuestieDB.questDataOverrides and not IsAscensionProtected("QUEST", questId, field) then + if self:IsLearnerLiveEnabled() and QuestieDB and QuestieDB.questDataOverrides and not IsAscensionProtected("QUEST", questId, field) then local ovr = QuestieDB.questDataOverrides[questId] or {} QuestieDB.questDataOverrides[questId] = ovr ovr[field] = ovr[field] or {} @@ -1699,7 +1822,7 @@ function QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText, objectiv end -- 2. Apply to live questDataOverrides immediately (no reload needed) - if QuestieDB and QuestieDB.questDataOverrides and not IsAscensionProtected("QUEST", questId, 10) then + if self:IsLearnerLiveEnabled() and QuestieDB and QuestieDB.questDataOverrides and not IsAscensionProtected("QUEST", questId, 10) then local ovr = QuestieDB.questDataOverrides[questId] or {} QuestieDB.questDataOverrides[questId] = ovr ovr[10] = ovr[10] or {} @@ -1720,12 +1843,14 @@ function QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText, objectiv end -- 3. Re-process the quest so PopulateObjective registers tooltips & map pins - _RefreshActiveQuestPins({ [questId] = true }) + if self:IsLearnerLiveEnabled() then + _RefreshActiveQuestPins({ [questId] = true }) + end -- 3. Register with tooltip system immediately. Preserve the objective icon so -- nameplates can render the correct learned slay/loot/talk marker. local QuestieTooltips = QuestieLoader:ImportModule("QuestieTooltips") - if QuestieTooltips and QuestieTooltips.RegisterObjectiveTooltip and not HasAscensionQuestObjectiveData(questId) then + if self:IsLearnerLiveEnabled() and QuestieTooltips and QuestieTooltips.RegisterObjectiveTooltip and not HasAscensionQuestObjectiveData(questId) then local objectiveIcon local QuestLogCache = QuestieLoader:ImportModule("QuestLogCache") local objectives = QuestLogCache and QuestLogCache.GetQuestObjectives and QuestLogCache.GetQuestObjectives(questId) @@ -1776,7 +1901,7 @@ function QuestieLearner:LearnItem(itemId, name, itemLevel, requiredLevel, itemCl existing.mc = (existing.mc or 0) + 1 -- Live injection into itemDataOverrides so QueryItemSingle works without reload - if QuestieDB and QuestieDB.itemDataOverrides then + if self:IsLearnerLiveEnabled() and QuestieDB and QuestieDB.itemDataOverrides then local ovr = QuestieDB.itemDataOverrides[itemId] if not ovr then QuestieDB.itemDataOverrides[itemId] = existing @@ -1815,7 +1940,7 @@ function QuestieLearner:LearnItemDrop(itemId, npcId) table.insert(existing[2], npcId) -- Live injection: sync drop list to itemDataOverrides - if QuestieDB and QuestieDB.itemDataOverrides and not IsAscensionProtected("ITEM", itemId, 2) then + if self:IsLearnerLiveEnabled() and QuestieDB and QuestieDB.itemDataOverrides and not IsAscensionProtected("ITEM", itemId, 2) then local ovr = QuestieDB.itemDataOverrides[itemId] or {} QuestieDB.itemDataOverrides[itemId] = ovr ovr[2] = ovr[2] or {} @@ -1863,7 +1988,7 @@ function QuestieLearner:LearnObject(objectId, name) existing.mc = (existing.mc or 0) + 1 -- Live injection into objectDataOverrides so QueryObjectSingle works without reload - if QuestieDB and QuestieDB.objectDataOverrides then + if self:IsLearnerLiveEnabled() and QuestieDB and QuestieDB.objectDataOverrides then local ovr = QuestieDB.objectDataOverrides[objectId] if not ovr then QuestieDB.objectDataOverrides[objectId] = existing @@ -1938,6 +2063,13 @@ function QuestieLearner:InjectLearnedData() return end + local mode = self:GetDataSourceMode() + if mode == "static" or mode == "none" then + QuestieLearner.data = Questie.dbLearner.global + Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieLearner] InjectLearnedData skipped because data source mode is", mode) + return + end + local learned = Questie.dbLearner.global -- Normalize malformed saved variables before any migration or injection. -- This prevents old learner rows from carrying numeric spawn fields @@ -2444,6 +2576,7 @@ function QuestieLearner:ClearAllData() Questie.dbLearner.global.quests = {} Questie.dbLearner.global.items = {} Questie.dbLearner.global.objects = {} + self:ApplyDataSourceMode() Questie:Print("Cleared all learned data.") end @@ -3000,6 +3133,20 @@ function QuestieLearner:OnLootOpened() for j = 1, sourceCount, 2 do local sourceGuid = sources[j] local sourceQty = sources[j + 1] + Questie:Debug( + Questie.DEBUG_DEVELOP, + "[QuestieLearner:LootSourceTrace]", + "slot=", + i, + "sourceIndex=", + j, + "guid=", + tostring(sourceGuid), + "qty=", + tostring(sourceQty), + "lootName=", + tostring(lootName) + ) TraceLearnerEntity("loot_source", sourceGuid, nil, sourceQty, lootName) if type(sourceGuid) == "string" then local sourceId, sourceType = GetIdAndTypeFromGUID(sourceGuid) @@ -3037,6 +3184,14 @@ function QuestieLearner:OnGameObjectUsed(objectId) if not objectId or objectId <= 0 then return end local objectName = ResolveObjectName(objectId) + Questie:Debug( + Questie.DEBUG_DEVELOP, + "[QuestieLearner:GameObjectUsedTrace]", + "id=", + tostring(objectId), + "name=", + tostring(objectName) + ) TraceLearnerEntity("gameobject_used", nil, "GameObject", objectId, objectName) self:LearnObject(objectId, objectName) end @@ -3345,7 +3500,7 @@ function QuestieLearner:OnCombatLogEvent(timestamp, eventType, srcGUID, srcName, if guidSpawns then local count = 0 for _ in pairs(guidSpawns) do count = count + 1 end - if count >= 1 then + if self:IsLearnerLiveEnabled() and count >= 1 then _MergeSpawnEvidence(npcId) end end @@ -3824,7 +3979,7 @@ function QuestieLearner:Initialize() EnsureLearnedData() QuestieLearner.data = Questie.dbLearner.global self:RegisterEvents() - self:InjectLearnedData() + self:ApplyDataSourceMode() _RegisterLearnedSpawnTooltipHook() local QuestieLearnerComms = QuestieLoader:ImportModule("QuestieLearnerComms") diff --git a/Modules/Tooltips/Tooltip.lua b/Modules/Tooltips/Tooltip.lua index eca076e..f7bd5aa 100644 --- a/Modules/Tooltips/Tooltip.lua +++ b/Modules/Tooltips/Tooltip.lua @@ -253,7 +253,8 @@ function QuestieTooltips:GetTooltip(key) if (not QuestieTooltips.lookupByKey[key]) then local QuestieLearner = QuestieLoader:ImportModule("QuestieLearner") local QuestLogCache = QuestieLoader:ImportModule("QuestLogCache") - if QuestieLearner and QuestieLearner.data then + local mode = QuestieLearner and QuestieLearner.GetDataSourceMode and QuestieLearner:GetDataSourceMode() or "auto" + if QuestieLearner and QuestieLearner.data and mode ~= "static" and mode ~= "none" then -- Try to find in learned NPCs or objects local id = tonumber(key:sub(3)) if id then diff --git a/Tests/QuestieLearnerDataSourceMode_spec.lua b/Tests/QuestieLearnerDataSourceMode_spec.lua new file mode 100644 index 0000000..ff7b804 --- /dev/null +++ b/Tests/QuestieLearnerDataSourceMode_spec.lua @@ -0,0 +1,39 @@ +local function read(path) + local f = assert(io.open(path, "r"), "cannot open " .. path) + local c = f:read("*a") + f:close() + return c +end + +local function has(content, needle) + return string.find(content, needle, 1, true) ~= nil +end + +describe("QuestieLearner data source mode", function() + it("adds a mode selector and explicit fallback options in the database tab", function() + local dbOptions = read("Modules/Options/DatabaseTab/QuestieOptionsDatabase.lua") + assert.is_true(has(dbOptions, "Data Source Mode")) + assert.is_true(has(dbOptions, "auto = l10n(\"Auto (current behavior)\")")) + assert.is_true(has(dbOptions, "learner = l10n(\"Learner Only\")")) + assert.is_true(has(dbOptions, "static = l10n(\"Static Only\")")) + assert.is_true(has(dbOptions, "none = l10n(\"Neither (base DB only)\")")) + end) + + it("defaults the learner mode to auto and exposes the live refresh hook", function() + local learner = read("Modules/QuestieLearner.lua") + local defaults = read("Modules/Options/QuestieOptionsDefaults.lua") + assert.is_true(has(defaults, "dataSourceMode = \"auto\"")) + assert.is_true(has(learner, "function QuestieLearner:GetDataSourceMode()")) + assert.is_true(has(learner, "function QuestieLearner:IsLearnerLiveEnabled()")) + assert.is_true(has(learner, "function QuestieLearner:ApplyDataSourceMode()")) + end) + + it("gates static suppression and tooltip fallback on the selected mode", function() + local quest = read("Modules/Quest/QuestieQuest.lua") + local priv = read("Modules/Quest/QuestieQuestPrivates.lua") + local tip = read("Modules/Tooltips/Tooltip.lua") + assert.is_true(has(quest, "dataSourceMode == \"auto\" or dataSourceMode == \"learner\"")) + assert.is_true(has(priv, "dataSourceMode == \"none\" or dataSourceMode == \"learner\"")) + assert.is_true(has(tip, "mode ~= \"static\" and mode ~= \"none\"")) + end) +end)