fix: let learner turn-ins drive arrow spawns

This commit is contained in:
Xurkon
2026-06-06 11:48:11 -05:00
parent 688e0bc0bf
commit 287751a071
4 changed files with 81 additions and 4 deletions
+1
View File
@@ -17,6 +17,7 @@
### Bug Fixes ### Bug Fixes
- **[QuestieLearner - Turn-In Arrow Spawn Promotion]** Allowed quest-related NPC and object spawns to keep their live learner coordinates even when the static database marks those spawn fields as protected. Quest giver and turn-in locations now have a path to reach the live override tables, so the arrow can point at learner-discovered hand-in targets without relaxing protection for unrelated world spawns.
- **[QuestieLearner - Quest-Only Item Learning]** Hardened item learning so plain junk loot no longer gets recorded as learner state. Only quest-relevant items are accepted now, including items that already have quest references in learned quest data. Quest-item drops still learn and still build source pins, but non-quest loot like generic trade goods and junk no longer pollutes the item learner or source cache. - **[QuestieLearner - Quest-Only Item Learning]** Hardened item learning so plain junk loot no longer gets recorded as learner state. Only quest-relevant items are accepted now, including items that already have quest references in learned quest data. Quest-item drops still learn and still build source pins, but non-quest loot like generic trade goods and junk no longer pollutes the item learner or source cache.
- **[QuestieLearner - Quest-Only Object Learning]** Hardened object learning so non-quest world objects are ignored unless they are explicitly tied to a quest flow. Quest giver, turn-in, objective, and `GAMEOBJECT_USED` promotions now mark the object as quest-relevant before it is stored, and learned object coordinates are only promoted into live object overrides when the object has quest references. This keeps source pins for real quest objects while preventing unrelated world objects from being recorded. - **[QuestieLearner - Quest-Only Object Learning]** Hardened object learning so non-quest world objects are ignored unless they are explicitly tied to a quest flow. Quest giver, turn-in, objective, and `GAMEOBJECT_USED` promotions now mark the object as quest-relevant before it is stored, and learned object coordinates are only promoted into live object overrides when the object has quest references. This keeps source pins for real quest objects while preventing unrelated world objects from being recorded.
- **[QuestieLearner - Quest Object Coordinate Promotion]** When a quest-related object is learned, the learner now preserves the actual interaction coordinates when available instead of relying only on fallback player position. Object objective mapping now reuses the recent object evidence cache so the stored spawn point matches the real interaction location and can render immediately as a map pin. - **[QuestieLearner - Quest Object Coordinate Promotion]** When a quest-related object is learned, the learner now preserves the actual interaction coordinates when available instead of relying only on fallback player position. Object objective mapping now reuses the recent object evidence cache so the stored spawn point matches the real interaction location and can render immediately as a map pin.
+47 -4
View File
@@ -18,6 +18,7 @@ local _Learner = QuestieLearner.private or {}
local GetDataSourceMode local GetDataSourceMode
local DeepCopy local DeepCopy
local HasQuestNpcReferences
local floor = math.floor local floor = math.floor
local abs = math.abs local abs = math.abs
@@ -930,9 +931,10 @@ 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) then if IsAscensionProtected("NPC", npcId, 7) and not allowSpawnMerge 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)
@@ -945,7 +947,7 @@ local function _ApplyNpcLiveUpdate(npcId)
end end
end end
-- Always merge spawn coords. -- Always merge spawn coords.
if existing[7] and not IsAscensionProtected("NPC", npcId, 7) then if existing[7] and (not IsAscensionProtected("NPC", npcId, 7) or allowSpawnMerge) 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 {}
@@ -2228,6 +2230,42 @@ local function HasQuestObjectReferences(objectId)
return false return false
end end
HasQuestNpcReferences = function(npcId)
local learned = Questie and Questie.dbLearner and Questie.dbLearner.global
if not learned or not learned.quests then
return false
end
for _, qData in pairs(learned.quests) do
if qData[2] and qData[2][1] then
for _, entry in ipairs(qData[2][1]) do
local entryId = type(entry) == "table" and entry[1] or entry
if entryId == npcId then
return true
end
end
end
if qData[3] and qData[3][1] then
for _, entry in ipairs(qData[3][1]) do
local entryId = type(entry) == "table" and entry[1] or entry
if entryId == npcId then
return true
end
end
end
if qData[10] and qData[10][1] then
for _, entry in ipairs(qData[10][1]) do
local entryId = type(entry) == "table" and entry[1] or entry
if entryId == npcId then
return true
end
end
end
end
return false
end
function QuestieLearner:LearnItem(itemId, name, itemLevel, requiredLevel, itemClass, itemSubClass) function QuestieLearner:LearnItem(itemId, name, itemLevel, requiredLevel, itemClass, itemSubClass)
if not self:IsEnabled() then return end if not self:IsEnabled() then return end
if not Questie.dbLearner.global.settings.learnItems then return end if not Questie.dbLearner.global.settings.learnItems then return end
@@ -2361,14 +2399,19 @@ function QuestieLearner:LearnObject(objectId, name, spawnX, spawnY, spawnZoneId,
-- Live injection into objectDataOverrides so QueryObjectSingle works without reload -- Live injection into objectDataOverrides so QueryObjectSingle works without reload
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
QuestieDB.objectDataOverrides[objectId] = existing if allowSpawnMerge then
QuestieDB.objectDataOverrides[objectId] = existing
else
QuestieDB.objectDataOverrides[objectId] = DeepCopy(CopyWithoutField(existing, 4))
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) then if existing[4] and (not IsAscensionProtected("OBJECT", objectId, 4) or allowSpawnMerge) 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 {}
+32
View File
@@ -142,6 +142,22 @@ describe("QuestieLearner kill-path batching", function()
assert.equals(1, table.getn(Questie.dbLearner.global.npcs[4001][7][44])) assert.equals(1, table.getn(Questie.dbLearner.global.npcs[4001][7][44]))
end) end)
it("keeps quest-related protected npc spawns available for turn-in arrows", function()
Questie.dbLearner.global.settings.dataSourceMode = "auto"
QuestieDB.ascensionOverrideKeys = QuestieDB.ascensionOverrideKeys or {}
QuestieDB.ascensionOverrideKeys.NPC = QuestieDB.ascensionOverrideKeys.NPC or {}
QuestieDB.ascensionOverrideKeys.NPC[5001] = { [7] = true }
QuestieLearner:LearnQuestGiver(6007, 5001, 1, false)
QuestieLearner:LearnNPC(5001, "Protected Turn-In", nil, nil, nil, nil, 15.5, 25.5, 44)
drainQueuedTimers()
assert.is_table(QuestieDB.npcDataOverrides[5001])
assert.is_table(QuestieDB.npcDataOverrides[5001][7])
assert.is_table(QuestieDB.npcDataOverrides[5001][7][44])
assert.equals(1, table.getn(QuestieDB.npcDataOverrides[5001][7][44]))
end)
it("ignores non-quest loot items so they do not pollute learner state", function() it("ignores non-quest loot items so they do not pollute learner state", function()
Questie.dbLearner.global.settings.dataSourceMode = "learner" Questie.dbLearner.global.settings.dataSourceMode = "learner"
QuestieDB.private.itemCache = {} QuestieDB.private.itemCache = {}
@@ -196,6 +212,22 @@ describe("QuestieLearner kill-path batching", function()
assert.equals(22.2, Questie.dbLearner.global.objects[3302][4][44][1][2]) assert.equals(22.2, Questie.dbLearner.global.objects[3302][4][44][1][2])
end) end)
it("keeps quest-related protected object spawns available for turn-in arrows", function()
Questie.dbLearner.global.settings.dataSourceMode = "auto"
QuestieDB.ascensionOverrideKeys = QuestieDB.ascensionOverrideKeys or {}
QuestieDB.ascensionOverrideKeys.OBJECT = QuestieDB.ascensionOverrideKeys.OBJECT or {}
QuestieDB.ascensionOverrideKeys.OBJECT[5002] = { [4] = true }
QuestieLearner:LearnQuestGiver(6008, 5002, 2, false)
local learned = QuestieLearner:LearnObject(5002, "Protected Object", 21.5, 31.5, 44, true)
assert.is_true(learned)
assert.is_table(QuestieDB.objectDataOverrides[5002])
assert.is_table(QuestieDB.objectDataOverrides[5002][4])
assert.is_table(QuestieDB.objectDataOverrides[5002][4][44])
assert.equals(1, table.getn(QuestieDB.objectDataOverrides[5002][4][44]))
end)
it("rejects non-quest item network merges without quest references", function() it("rejects non-quest item network merges without quest references", function()
Questie.dbLearner.global.settings.dataSourceMode = "learner" Questie.dbLearner.global.settings.dataSourceMode = "learner"
local changed = QuestieLearner:_ApplyIncomingNetworkMerge("ITEM", 2401, { [1] = "Arcane Sliver" }, "NEW") local changed = QuestieLearner:_ApplyIncomingNetworkMerge("ITEM", 2401, { [1] = "Arcane Sliver" }, "NEW")
+1
View File
@@ -178,6 +178,7 @@
<div class="container"> <div class="container">
<h2 id="unreleased-performance-refactor">[Unreleased] &mdash; Performance Refactor Branches</h2> <h2 id="unreleased-performance-refactor">[Unreleased] &mdash; Performance Refactor Branches</h2>
<ul> <ul>
<li><strong>[QuestieLearner &mdash; Turn-In Arrow Spawn Promotion]</strong> Allowed quest-related NPC and object spawns to keep their live learner coordinates even when the static database marks those spawn fields as protected. Quest giver and turn-in locations now have a path to reach the live override tables, so the arrow can point at learner-discovered hand-in targets without relaxing protection for unrelated world spawns.</li>
<li><strong>[QuestieLearner &mdash; Quest-Only Item Learning]</strong> Hardened item learning so plain junk loot no longer gets recorded as learner state. Only quest-relevant items are accepted now, including items that already have quest references in learned quest data. Quest-item drops still learn and still build source pins, but non-quest loot like generic trade goods and junk no longer pollutes the item learner or source cache.</li> <li><strong>[QuestieLearner &mdash; Quest-Only Item Learning]</strong> Hardened item learning so plain junk loot no longer gets recorded as learner state. Only quest-relevant items are accepted now, including items that already have quest references in learned quest data. Quest-item drops still learn and still build source pins, but non-quest loot like generic trade goods and junk no longer pollutes the item learner or source cache.</li>
<li><strong>[QuestieLearner &mdash; Quest-Only Object Learning]</strong> Hardened object learning so non-quest world objects are ignored unless they are explicitly tied to a quest flow. Quest giver, turn-in, objective, and <code>GAMEOBJECT_USED</code> promotions now mark the object as quest-relevant before it is stored, and learned object coordinates are only promoted into live object overrides when the object has quest references. This keeps source pins for real quest objects while preventing unrelated world objects from being recorded.</li> <li><strong>[QuestieLearner &mdash; Quest-Only Object Learning]</strong> Hardened object learning so non-quest world objects are ignored unless they are explicitly tied to a quest flow. Quest giver, turn-in, objective, and <code>GAMEOBJECT_USED</code> promotions now mark the object as quest-relevant before it is stored, and learned object coordinates are only promoted into live object overrides when the object has quest references. This keeps source pins for real quest objects while preventing unrelated world objects from being recorded.</li>
<li><strong>[QuestieLearner &mdash; Quest Object Coordinate Promotion]</strong> When a quest-related object is learned, the learner now preserves the actual interaction coordinates when available instead of relying only on fallback player position. Object objective mapping now reuses the recent object evidence cache so the stored spawn point matches the real interaction location and can render immediately as a map pin.</li> <li><strong>[QuestieLearner &mdash; Quest Object Coordinate Promotion]</strong> When a quest-related object is learned, the learner now preserves the actual interaction coordinates when available instead of relying only on fallback player position. Object objective mapping now reuses the recent object evidence cache so the stored spawn point matches the real interaction location and can render immediately as a map pin.</li>