feat: add learner/static data source mode

This commit is contained in:
Xurkon
2026-06-05 20:15:26 -05:00
parent 3f87e04c75
commit a0409eef8c
8 changed files with 328 additions and 31 deletions
+8 -2
View File
@@ -819,7 +819,9 @@ end
function QuestieDB.GetSuppressedNPCs(zoneId) function QuestieDB.GetSuppressedNPCs(zoneId)
local suppressed = {} local suppressed = {}
local ld = Questie.dbLearner.global 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 threshold = ld.settings.minConfidencePins or 2
local npcId, entry = next(ld.npcs) local npcId, entry = next(ld.npcs)
while npcId do while npcId do
@@ -830,6 +832,7 @@ function QuestieDB.GetSuppressedNPCs(zoneId)
npcId, entry = next(ld.npcs, npcId) npcId, entry = next(ld.npcs, npcId)
end end
end end
end
return suppressed return suppressed
end end
@@ -840,7 +843,9 @@ end
function QuestieDB.GetSuppressedObjects(zoneId) function QuestieDB.GetSuppressedObjects(zoneId)
local suppressed = {} local suppressed = {}
local ld = Questie.dbLearner.global 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 threshold = ld.settings.minConfidencePins or 2
local objId, entry = next(ld.objects) local objId, entry = next(ld.objects)
while objId do while objId do
@@ -851,6 +856,7 @@ function QuestieDB.GetSuppressedObjects(zoneId)
objId, entry = next(ld.objects, objId) objId, entry = next(ld.objects, objId)
end end
end end
end
return suppressed return suppressed
end end
@@ -46,6 +46,23 @@ local function GetLearnedCounts()
return s return s
end 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 -- Export Dialog
----------------------------------------------------------------------- -----------------------------------------------------------------------
@@ -204,6 +221,43 @@ function QuestieOptions.tabs.database:Initialize()
name = function() return l10n("What To Learn") end, 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 = { learn_npcs = {
type = "toggle", type = "toggle",
order = 2.1, order = 2.1,
@@ -87,6 +87,9 @@ function QuestieOptionsDefaults:Load()
learnerBroadcast = true, learnerBroadcast = true,
enableMapIcons = true, enableMapIcons = true,
enableMiniMapIcons = 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, questieShutUp = false,
bugWorkarounds = true, bugWorkarounds = true,
hideIconsOnContinents = false, hideIconsOnContinents = false,
+7 -2
View File
@@ -1581,8 +1581,13 @@ function QuestieQuest:PopulateObjective(quest, objectiveIndex, objective, blockI
objectiveCenter = { x = x, y = y } objectiveCenter = { x = x, y = y }
end end
-- Filter static spawns if prioritizeMyData is enabled and we have high-confidence learned data -- Filter static spawns only when the learner is allowed to influence display.
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 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) local zone, _ = next(zones)
while zone do while zone do
local suppressed = (objectiveData.Type == "monster" and QuestieDB.GetSuppressedNPCs(zone)) or (objectiveData.Type == "object" and QuestieDB.GetSuppressedObjects(zone)) local suppressed = (objectiveData.Type == "monster" and QuestieDB.GetSuppressedNPCs(zone)) or (objectiveData.Type == "object" and QuestieDB.GetSuppressedObjects(zone))
+36 -2
View File
@@ -172,14 +172,23 @@ monster = function(npcId, objective)
end end
local isLearned = false 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 -- Learner safety net: when prioritizeMyData is enabled and the Learner has
-- verified spawn data for this NPC, prefer it over compiled DB spawns. -- verified spawn data for this NPC, prefer it over compiled DB spawns.
-- This catches edge cases where the npcDataOverrides chain doesn't fully -- This catches edge cases where the npcDataOverrides chain doesn't fully
-- replace retail positions (e.g. format migration gaps, timing issues). -- 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 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] local learnedNpc = ld.npcs and ld.npcs[npcId]
if learnedNpc then if learnedNpc then
local learnedSpawns = learnedNpc[7] local learnedSpawns = learnedNpc[7]
@@ -253,6 +262,30 @@ object = function(objectId, objective)
spawns = {} spawns = {}
end 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 ---@type SpawnListObject
local retObject = { local retObject = {
@@ -263,6 +296,7 @@ object = function(objectId, objective)
GetIconScale = _GetIconScaleForObject, GetIconScale = _GetIconScaleForObject,
IconScale = _GetIconScaleForObject(), IconScale = _GetIconScaleForObject(),
TooltipKey = "o_" .. objectId, TooltipKey = "o_" .. objectId,
isLearned = isLearned,
} }
return { return {
+177 -22
View File
@@ -16,6 +16,9 @@ local ZoneDB = QuestieLoader:ImportModule("ZoneDB")
local _Learner = QuestieLearner.private or {} local _Learner = QuestieLearner.private or {}
local GetDataSourceMode
local DeepCopy
local floor = math.floor local floor = math.floor
local abs = math.abs local abs = math.abs
local time = time local time = time
@@ -32,6 +35,10 @@ local string_len = string.len
local string_upper = string.upper local string_upper = string.upper
local function IsAscensionProtected(dbType, id, key) 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 local protected = QuestieDB
and QuestieDB.ascensionOverrideKeys and QuestieDB.ascensionOverrideKeys
and QuestieDB.ascensionOverrideKeys[dbType] and QuestieDB.ascensionOverrideKeys[dbType]
@@ -44,6 +51,42 @@ local function HasAscensionQuestObjectiveData(questId)
return IsAscensionProtected("QUEST", questId, 10) return IsAscensionProtected("QUEST", questId, 10)
end 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) local function NormalizeSpawnZoneKey(zoneKey)
-- Convert raw area IDs (e.g. 3431 from GetAreaID()) to the canonical map IDs -- 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). -- used by AscensionDB and the rendering system (e.g. 1241 for Sunstrider Isle).
@@ -226,6 +269,39 @@ local function DeepCopy(value)
return copy return copy
end 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 -- Returns the grid-bucket key for a coordinate so nearby points share the same slot
local function CoordBucket(x, y) local function CoordBucket(x, y)
return floor(x / COORD_GRID) * COORD_GRID, floor(y / COORD_GRID) * COORD_GRID return floor(x / COORD_GRID) * COORD_GRID, floor(y / COORD_GRID) * COORD_GRID
@@ -323,6 +399,7 @@ local function EnsureLearnedData()
learnObjects = true, learnObjects = true,
minConfidencePins = 1, minConfidencePins = 1,
prioritizeMyData = true, prioritizeMyData = true,
dataSourceMode = "auto",
staleThreshold = 90, -- days staleThreshold = 90, -- days
pruneVerified = false, -- protect verified data by default pruneVerified = false, -- protect verified data by default
performanceMode = "balanced", performanceMode = "balanced",
@@ -347,6 +424,18 @@ local function EnsureLearnedData()
if s.learnObjects == nil then s.learnObjects = true end if s.learnObjects == nil then s.learnObjects = true end
if s.minConfidencePins == nil then s.minConfidencePins = 1 end if s.minConfidencePins == nil then s.minConfidencePins = 1 end
if s.prioritizeMyData == nil then s.prioritizeMyData = true 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 if s.staleThreshold == nil then
s.staleThreshold = 90 s.staleThreshold = 90
end end
@@ -389,6 +478,30 @@ function QuestieLearner:GetSettings()
return Questie.dbLearner.global.settings return Questie.dbLearner.global.settings
end 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) local function GetLearnerSetting(key, defaultValue)
if Questie and Questie.dbLearner and Questie.dbLearner.global and Questie.dbLearner.global.settings then if Questie and Questie.dbLearner and Questie.dbLearner.global and Questie.dbLearner.global.settings then
local value = Questie.dbLearner.global.settings[key] local value = Questie.dbLearner.global.settings[key]
@@ -787,6 +900,9 @@ local function _FlushNpcLiveUpdates()
end end
local function _QueueNpcLiveUpdate(npcId) local function _QueueNpcLiveUpdate(npcId)
if not (QuestieLearner and QuestieLearner.IsLearnerLiveEnabled and QuestieLearner:IsLearnerLiveEnabled()) then
return
end
_Learner.pendingNpcLiveUpdates = _Learner.pendingNpcLiveUpdates or {} _Learner.pendingNpcLiveUpdates = _Learner.pendingNpcLiveUpdates or {}
_Learner.pendingNpcLiveUpdates[npcId] = true _Learner.pendingNpcLiveUpdates[npcId] = true
@@ -861,10 +977,11 @@ local function CrossLinkAfterNPC(npcId)
local learned = _GetDB() local learned = _GetDB()
local npcData = learned.npcs[npcId] local npcData = learned.npcs[npcId]
if not npcData then return end 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 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 -- Quest starters: quest[2][1] lists NPCs that start this quest
if qData[2] and qData[2][1] then if qData[2] and qData[2][1] then
@@ -931,9 +1048,10 @@ local function CrossLinkAfterQuest(questId)
local learned = _GetDB() local learned = _GetDB()
local qData = learned.quests[questId] local qData = learned.quests[questId]
if not qData then return end if not qData then return end
local qOvr = QuestieDB and QuestieDB.questDataOverrides local liveEnabled = QuestieLearner and QuestieLearner.IsLearnerLiveEnabled and QuestieLearner:IsLearnerLiveEnabled()
local npcOvr = QuestieDB and QuestieDB.npcDataOverrides local qOvr = liveEnabled and QuestieDB and QuestieDB.questDataOverrides or nil
local objOvr = QuestieDB and QuestieDB.objectDataOverrides 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] -- Starter NPCs: quest[2][1] → npc[10]
if qData[2] and qData[2][1] then if qData[2] and qData[2][1] then
@@ -973,7 +1091,7 @@ local function CrossLinkAfterQuest(questId)
if iData then if iData then
if not iData[5] then if not iData[5] then
iData[5] = questId iData[5] = questId
if QuestieDB and QuestieDB.itemDataOverrides then if liveEnabled and QuestieDB and QuestieDB.itemDataOverrides then
local ovr = QuestieDB.itemDataOverrides[qData[11]] or {} local ovr = QuestieDB.itemDataOverrides[qData[11]] or {}
QuestieDB.itemDataOverrides[qData[11]] = ovr QuestieDB.itemDataOverrides[qData[11]] = ovr
if not ovr[5] then ovr[5] = questId end if not ovr[5] then ovr[5] = questId end
@@ -1003,8 +1121,9 @@ local function CrossLinkAfterObject(objectId)
local learned = _GetDB() local learned = _GetDB()
local objData = learned.objects[objectId] local objData = learned.objects[objectId]
if not objData then return end if not objData then return end
local objOvr = QuestieDB and QuestieDB.objectDataOverrides local liveEnabled = QuestieLearner and QuestieLearner.IsLearnerLiveEnabled and QuestieLearner:IsLearnerLiveEnabled()
local qOvr = QuestieDB and QuestieDB.questDataOverrides 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 for questId, qData in pairs(learned.quests) do
-- Object starters: quest[2][2] -- Object starters: quest[2][2]
@@ -1054,7 +1173,8 @@ local function CrossLinkAfterItem(itemId)
local learned = _GetDB() local learned = _GetDB()
local iData = learned.items[itemId] local iData = learned.items[itemId]
if not iData then return end 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 -- If this item starts a quest (item[5]=startQuest), ensure that quest knows
-- about it via quest[2][3] (starter items slot) -- about it via quest[2][3] (starter items slot)
@@ -1062,7 +1182,7 @@ local function CrossLinkAfterItem(itemId)
if qData[11] == itemId then if qData[11] == itemId then
if not iData[5] then if not iData[5] then
iData[5] = questId iData[5] = questId
if QuestieDB and QuestieDB.itemDataOverrides then if liveEnabled and QuestieDB and QuestieDB.itemDataOverrides then
local ovr = QuestieDB.itemDataOverrides[itemId] or {} local ovr = QuestieDB.itemDataOverrides[itemId] or {}
QuestieDB.itemDataOverrides[itemId] = ovr QuestieDB.itemDataOverrides[itemId] = ovr
if not ovr[5] then ovr[5] = questId end if not ovr[5] then ovr[5] = questId end
@@ -1090,9 +1210,10 @@ end
local function CrossLinkAfterQuestGiver(questId, entityId, typeSlot, isStart) local function CrossLinkAfterQuestGiver(questId, entityId, typeSlot, isStart)
local learned = _GetDB() local learned = _GetDB()
local qData = learned.quests[questId] local qData = learned.quests[questId]
local npcOvr = QuestieDB and QuestieDB.npcDataOverrides local liveEnabled = QuestieLearner and QuestieLearner.IsLearnerLiveEnabled and QuestieLearner:IsLearnerLiveEnabled()
local objOvr = QuestieDB and QuestieDB.objectDataOverrides local npcOvr = liveEnabled and QuestieDB and QuestieDB.npcDataOverrides or nil
local qOvr = QuestieDB and QuestieDB.questDataOverrides local objOvr = liveEnabled and QuestieDB and QuestieDB.objectDataOverrides or nil
local qOvr = liveEnabled and QuestieDB and QuestieDB.questDataOverrides or nil
if typeSlot == 1 then if typeSlot == 1 then
-- NPC ↔ quest -- 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 -- Live injection is intentionally batched: repeated kill/log/loot events for
-- the same NPC update saved evidence immediately, then flush QuestieDB once. -- the same NPC update saved evidence immediately, then flush QuestieDB once.
if self:IsLearnerLiveEnabled() then
_QueueNpcLiveUpdate(npcId) _QueueNpcLiveUpdate(npcId)
end
if isNew then if isNew then
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] New NPC learned:", npcId, name or "?") 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 existing.mc = (existing.mc or 0) + 1
-- Live injection into questDataOverrides so GetQuest works without reload -- 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] local ovr = QuestieDB.questDataOverrides[questId]
if not ovr then if not ovr then
QuestieDB.questDataOverrides[questId] = existing QuestieDB.questDataOverrides[questId] = existing
@@ -1633,7 +1756,7 @@ function QuestieLearner:LearnQuestGiver(questId, entityId, entityType, isStart)
table.insert(list, entityId) table.insert(list, entityId)
-- Live injection into questDataOverrides so starters/finishers take effect without reload -- 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 {} local ovr = QuestieDB.questDataOverrides[questId] or {}
QuestieDB.questDataOverrides[questId] = ovr QuestieDB.questDataOverrides[questId] = ovr
ovr[field] = ovr[field] or {} ovr[field] = ovr[field] or {}
@@ -1699,7 +1822,7 @@ function QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText, objectiv
end end
-- 2. Apply to live questDataOverrides immediately (no reload needed) -- 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 {} local ovr = QuestieDB.questDataOverrides[questId] or {}
QuestieDB.questDataOverrides[questId] = ovr QuestieDB.questDataOverrides[questId] = ovr
ovr[10] = ovr[10] or {} ovr[10] = ovr[10] or {}
@@ -1720,12 +1843,14 @@ function QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText, objectiv
end end
-- 3. Re-process the quest so PopulateObjective registers tooltips & map pins -- 3. Re-process the quest so PopulateObjective registers tooltips & map pins
if self:IsLearnerLiveEnabled() then
_RefreshActiveQuestPins({ [questId] = true }) _RefreshActiveQuestPins({ [questId] = true })
end
-- 3. Register with tooltip system immediately. Preserve the objective icon so -- 3. Register with tooltip system immediately. Preserve the objective icon so
-- nameplates can render the correct learned slay/loot/talk marker. -- nameplates can render the correct learned slay/loot/talk marker.
local QuestieTooltips = QuestieLoader:ImportModule("QuestieTooltips") 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 objectiveIcon
local QuestLogCache = QuestieLoader:ImportModule("QuestLogCache") local QuestLogCache = QuestieLoader:ImportModule("QuestLogCache")
local objectives = QuestLogCache and QuestLogCache.GetQuestObjectives and QuestLogCache.GetQuestObjectives(questId) 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 existing.mc = (existing.mc or 0) + 1
-- Live injection into itemDataOverrides so QueryItemSingle works without reload -- 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] local ovr = QuestieDB.itemDataOverrides[itemId]
if not ovr then if not ovr then
QuestieDB.itemDataOverrides[itemId] = existing QuestieDB.itemDataOverrides[itemId] = existing
@@ -1815,7 +1940,7 @@ function QuestieLearner:LearnItemDrop(itemId, npcId)
table.insert(existing[2], npcId) table.insert(existing[2], npcId)
-- Live injection: sync drop list to itemDataOverrides -- 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 {} local ovr = QuestieDB.itemDataOverrides[itemId] or {}
QuestieDB.itemDataOverrides[itemId] = ovr QuestieDB.itemDataOverrides[itemId] = ovr
ovr[2] = ovr[2] or {} ovr[2] = ovr[2] or {}
@@ -1863,7 +1988,7 @@ function QuestieLearner:LearnObject(objectId, name)
existing.mc = (existing.mc or 0) + 1 existing.mc = (existing.mc or 0) + 1
-- Live injection into objectDataOverrides so QueryObjectSingle works without reload -- 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] local ovr = QuestieDB.objectDataOverrides[objectId]
if not ovr then if not ovr then
QuestieDB.objectDataOverrides[objectId] = existing QuestieDB.objectDataOverrides[objectId] = existing
@@ -1938,6 +2063,13 @@ function QuestieLearner:InjectLearnedData()
return return
end 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 local learned = Questie.dbLearner.global
-- Normalize malformed saved variables before any migration or injection. -- Normalize malformed saved variables before any migration or injection.
-- This prevents old learner rows from carrying numeric spawn fields -- This prevents old learner rows from carrying numeric spawn fields
@@ -2444,6 +2576,7 @@ function QuestieLearner:ClearAllData()
Questie.dbLearner.global.quests = {} Questie.dbLearner.global.quests = {}
Questie.dbLearner.global.items = {} Questie.dbLearner.global.items = {}
Questie.dbLearner.global.objects = {} Questie.dbLearner.global.objects = {}
self:ApplyDataSourceMode()
Questie:Print("Cleared all learned data.") Questie:Print("Cleared all learned data.")
end end
@@ -3000,6 +3133,20 @@ function QuestieLearner:OnLootOpened()
for j = 1, sourceCount, 2 do for j = 1, sourceCount, 2 do
local sourceGuid = sources[j] local sourceGuid = sources[j]
local sourceQty = sources[j + 1] 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) TraceLearnerEntity("loot_source", sourceGuid, nil, sourceQty, lootName)
if type(sourceGuid) == "string" then if type(sourceGuid) == "string" then
local sourceId, sourceType = GetIdAndTypeFromGUID(sourceGuid) local sourceId, sourceType = GetIdAndTypeFromGUID(sourceGuid)
@@ -3037,6 +3184,14 @@ function QuestieLearner:OnGameObjectUsed(objectId)
if not objectId or objectId <= 0 then return end if not objectId or objectId <= 0 then return end
local objectName = ResolveObjectName(objectId) local objectName = ResolveObjectName(objectId)
Questie:Debug(
Questie.DEBUG_DEVELOP,
"[QuestieLearner:GameObjectUsedTrace]",
"id=",
tostring(objectId),
"name=",
tostring(objectName)
)
TraceLearnerEntity("gameobject_used", nil, "GameObject", objectId, objectName) TraceLearnerEntity("gameobject_used", nil, "GameObject", objectId, objectName)
self:LearnObject(objectId, objectName) self:LearnObject(objectId, objectName)
end end
@@ -3345,7 +3500,7 @@ function QuestieLearner:OnCombatLogEvent(timestamp, eventType, srcGUID, srcName,
if guidSpawns then if guidSpawns then
local count = 0 local count = 0
for _ in pairs(guidSpawns) do count = count + 1 end for _ in pairs(guidSpawns) do count = count + 1 end
if count >= 1 then if self:IsLearnerLiveEnabled() and count >= 1 then
_MergeSpawnEvidence(npcId) _MergeSpawnEvidence(npcId)
end end
end end
@@ -3824,7 +3979,7 @@ function QuestieLearner:Initialize()
EnsureLearnedData() EnsureLearnedData()
QuestieLearner.data = Questie.dbLearner.global QuestieLearner.data = Questie.dbLearner.global
self:RegisterEvents() self:RegisterEvents()
self:InjectLearnedData() self:ApplyDataSourceMode()
_RegisterLearnedSpawnTooltipHook() _RegisterLearnedSpawnTooltipHook()
local QuestieLearnerComms = QuestieLoader:ImportModule("QuestieLearnerComms") local QuestieLearnerComms = QuestieLoader:ImportModule("QuestieLearnerComms")
+2 -1
View File
@@ -253,7 +253,8 @@ function QuestieTooltips:GetTooltip(key)
if (not QuestieTooltips.lookupByKey[key]) then if (not QuestieTooltips.lookupByKey[key]) then
local QuestieLearner = QuestieLoader:ImportModule("QuestieLearner") local QuestieLearner = QuestieLoader:ImportModule("QuestieLearner")
local QuestLogCache = QuestieLoader:ImportModule("QuestLogCache") 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 -- Try to find in learned NPCs or objects
local id = tonumber(key:sub(3)) local id = tonumber(key:sub(3))
if id then if id then
@@ -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)