fix(learner): learner-only mode shows only learner data

Three-mode spawn-merge semantics:
- Learner: discard npcDataOverrides (which AscensionDB's _Asc_MergeInto
  populates with curated coords). GetNPC returns learnerRecord only.
  _ApplyNpcLiveUpdate and object-spawn injection use IsAscensionProtected
  (mode-dependent) so learner coords pass through in learner mode.
- Auto: static DB primary, learner fills gaps. Curated spawns protected.
- Static: base DB only.

Also removes the allowSpawnMerge bypass that was the original hole
letting learner coords leak into curated spawns in auto mode.
This commit is contained in:
Xurkon
2026-06-08 21:48:02 -05:00
parent f9a3301584
commit edbb64fb3d
2 changed files with 33 additions and 11 deletions
+4
View File
@@ -2277,6 +2277,10 @@ function QuestieDB:GetNPC(npcId)
if not rawdata then if not rawdata then
rawdata = override rawdata = override
end end
-- Learner mode: discard any curated coords that AscensionDB's
-- _Asc_MergeInto wrote into npcDataOverrides. The learner's own
-- coords come from rawdata (learnerRecord), not from the override.
override = nil
else else
rawdata = QuestieDB.QueryNPC(npcId, QuestieDB._npcAdapterQueryOrder) rawdata = QuestieDB.QueryNPC(npcId, QuestieDB._npcAdapterQueryOrder)
if not rawdata and learnerRecord and mode == "auto" then if not rawdata and learnerRecord and mode == "auto" then
+29 -11
View File
@@ -97,6 +97,15 @@ local function AscensionOwnsNpcSpawns(npcId)
and QuestieDB.ascensionOverrideKeys["NPC"][npcId][7] == true and QuestieDB.ascensionOverrideKeys["NPC"][npcId][7] == true
end end
-- Mode-independent check for AscensionDB-curated OBJECT spawns ([4]).
local function AscensionOwnsObjectSpawns(objectId)
return QuestieDB
and QuestieDB.ascensionOverrideKeys
and QuestieDB.ascensionOverrideKeys["OBJECT"]
and QuestieDB.ascensionOverrideKeys["OBJECT"][objectId]
and QuestieDB.ascensionOverrideKeys["OBJECT"][objectId][4] == true
end
local function HasAscensionQuestObjectiveData(questId) local function HasAscensionQuestObjectiveData(questId)
return IsAscensionProtected("QUEST", questId, 10) return IsAscensionProtected("QUEST", questId, 10)
end end
@@ -967,23 +976,28 @@ local function _ApplyNpcLiveUpdate(npcId)
if existing.mc < threshold then return false end if existing.mc < threshold then return false end
if not (QuestieDB and QuestieDB.npcDataOverrides and existing[7] and next(existing[7])) then return false end if not (QuestieDB and QuestieDB.npcDataOverrides and existing[7] and next(existing[7])) then return false end
local allowSpawnMerge = existing[7] and next(existing[7]) and HasQuestNpcReferences(npcId)
local ovr = QuestieDB.npcDataOverrides[npcId] local ovr = QuestieDB.npcDataOverrides[npcId]
if not ovr then if not ovr then
if IsAscensionProtected("NPC", npcId, 7) and not allowSpawnMerge then -- Learner mode: IsAscensionProtected returns false, so learner data
-- (including spawns) is used exclusively. Auto/static modes: strip
-- learner spawns for AscensionDB-curated NPCs.
if IsAscensionProtected("NPC", npcId, 7) then
QuestieDB.npcDataOverrides[npcId] = DeepCopy(CopyWithoutField(existing, 7)) QuestieDB.npcDataOverrides[npcId] = DeepCopy(CopyWithoutField(existing, 7))
else else
QuestieDB.npcDataOverrides[npcId] = DeepCopy(existing) QuestieDB.npcDataOverrides[npcId] = DeepCopy(existing)
end end
else else
-- Merge: fill missing fields; also overwrite empty-string names. -- Merge non-spawn fields. IsAscensionProtected is mode-dependent:
-- returns false in learner mode (learner fills all gaps), true in
-- auto/static (curated fields are protected).
for k, v in pairs(existing) do for k, v in pairs(existing) do
if k ~= 7 and not IsAscensionProtected("NPC", npcId, k) and (ovr[k] == nil or (k == 1 and ovr[k] == "")) then if k ~= 7 and not IsAscensionProtected("NPC", npcId, k) and (ovr[k] == nil or (k == 1 and ovr[k] == "")) then
ovr[k] = DeepCopy(v) ovr[k] = DeepCopy(v)
end end
end end
-- Always merge spawn coords. -- Merge spawn coords. allowSpawnMerge bypass removed — it was
if existing[7] and (not IsAscensionProtected("NPC", npcId, 7) or allowSpawnMerge) then -- the original hole letting learner coords leak into curated spawns.
if existing[7] and not IsAscensionProtected("NPC", npcId, 7) then
ovr[7] = ovr[7] or {} ovr[7] = ovr[7] or {}
for zid, coords in pairs(existing[7]) do for zid, coords in pairs(existing[7]) do
ovr[7][zid] = ovr[7][zid] or {} ovr[7][zid] = ovr[7][zid] or {}
@@ -2435,21 +2449,25 @@ function QuestieLearner:LearnObject(objectId, name, spawnX, spawnY, spawnZoneId,
existing.ls = time() -- Update last seen existing.ls = time() -- Update last seen
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.
-- Same three-mode semantics as the NPC path.
if self:IsLearnerLiveEnabled() and QuestieDB and QuestieDB.objectDataOverrides then if self:IsLearnerLiveEnabled() and QuestieDB and QuestieDB.objectDataOverrides then
local allowSpawnMerge = existing.questRelevant or HasQuestObjectReferences(objectId)
local ovr = QuestieDB.objectDataOverrides[objectId] local ovr = QuestieDB.objectDataOverrides[objectId]
if not ovr then if not ovr then
if allowSpawnMerge then -- Learner: use learner data exclusively. Auto/static: strip learner
QuestieDB.objectDataOverrides[objectId] = existing -- spawns for AscensionDB-curated objects.
else if AscensionOwnsObjectSpawns(objectId) then
QuestieDB.objectDataOverrides[objectId] = DeepCopy(CopyWithoutField(existing, 4)) QuestieDB.objectDataOverrides[objectId] = DeepCopy(CopyWithoutField(existing, 4))
else
QuestieDB.objectDataOverrides[objectId] = existing
end end
else else
for k, v in pairs(existing) do for k, v in pairs(existing) do
if ovr[k] == nil and not IsAscensionProtected("OBJECT", objectId, k) then ovr[k] = v end if ovr[k] == nil and not IsAscensionProtected("OBJECT", objectId, k) then ovr[k] = v end
end end
if existing[4] and (not IsAscensionProtected("OBJECT", objectId, 4) or allowSpawnMerge) then -- Merge spawn coords: learner mode bypasses (IsAscensionProtected=false),
-- auto/static protects curated spawns. allowSpawnMerge bypass removed.
if existing[4] and not IsAscensionProtected("OBJECT", objectId, 4) then
ovr[4] = ovr[4] or {} ovr[4] = ovr[4] or {}
for zid, coords in pairs(existing[4]) do for zid, coords in pairs(existing[4]) do
ovr[4][zid] = ovr[4][zid] or {} ovr[4][zid] = ovr[4][zid] or {}