From ccdfa525cfa7c9fcc26d4aee2ce11c8392242e4a Mon Sep 17 00:00:00 2001 From: Xurkon Date: Sat, 6 Jun 2026 10:21:36 -0500 Subject: [PATCH] fix: gate learner item learning to quest items --- Modules/QuestieLearner.lua | 104 ++++++++++++++++++-- Tests/QuestieLearnerDataSourceMode_spec.lua | 24 ++++- Tests/QuestieLearner_performance_spec.lua | 34 +++++-- 3 files changed, 142 insertions(+), 20 deletions(-) diff --git a/Modules/QuestieLearner.lua b/Modules/QuestieLearner.lua index 2ffd1dd..9256aaf 100644 --- a/Modules/QuestieLearner.lua +++ b/Modules/QuestieLearner.lua @@ -1195,12 +1195,21 @@ local function CrossLinkAfterQuest(questId) if qData[11] and qData[11] > 0 then local iData = learned.items[qData[11]] if iData then + iData.questRelevant = true if not iData[5] then iData[5] = questId if liveEnabled and QuestieDB and QuestieDB.itemDataOverrides then local ovr = QuestieDB.itemDataOverrides[qData[11]] or {} QuestieDB.itemDataOverrides[qData[11]] = ovr + if ovr[1] == nil and iData[1] ~= nil and not IsAscensionProtected("ITEM", qData[11], 1) then + ovr[1] = iData[1] + end if not ovr[5] then ovr[5] = questId end + for k, v in pairs(iData) do + if k ~= 1 and k ~= 5 and k ~= "mc" and ovr[k] == nil and not IsAscensionProtected("ITEM", qData[11], k) then + ovr[k] = v + end + end end end end @@ -1212,6 +1221,7 @@ local function CrossLinkAfterQuest(questId) local itemId = entry[1] local iData = learned.items[itemId] if iData and iData[2] then + iData.questRelevant = true for _, dropNpcId in ipairs(iData[2]) do _AddToQuestObjective(qData, 1, dropNpcId, nil, qOvr, questId) end @@ -2097,11 +2107,81 @@ end -- Item learning ------------------------------------------------------------------------ +local function IsQuestRelevantItem(itemId, itemClass) + local learned = Questie and Questie.dbLearner and Questie.dbLearner.global + if not learned or not learned.quests then + return false + end + + if itemClass == 12 then + return true + end + + for _, qData in pairs(learned.quests) do + if qData[11] == itemId then + return true + end + if qData[10] and qData[10][3] then + for _, entry in ipairs(qData[10][3]) do + if entry[1] == itemId then + return true + end + end + end + if qData[2] and qData[2][3] then + for _, entry in ipairs(qData[2][3]) do + if entry[1] == itemId then + return true + end + end + end + end + + return false +end + +local function HasQuestReferences(itemId) + local learned = Questie and Questie.dbLearner and Questie.dbLearner.global + if not learned or not learned.quests then + return false + end + + local itemData = learned.items and learned.items[itemId] + if itemData and itemData.questRelevant then + return true + end + + for _, qData in pairs(learned.quests) do + if qData[11] == itemId then + return true + end + if qData[10] and qData[10][3] then + for _, entry in ipairs(qData[10][3]) do + if entry[1] == itemId then + return true + end + end + end + if qData[2] and qData[2][3] then + for _, entry in ipairs(qData[2][3]) do + if entry[1] == itemId then + return true + end + end + end + end + + return false +end + function QuestieLearner:LearnItem(itemId, name, itemLevel, requiredLevel, itemClass, itemSubClass) if not self:IsEnabled() then return end if not Questie.dbLearner.global.settings.learnItems then return end itemId = tonumber(itemId) if not itemId or itemId <= 0 then return end + if not IsQuestRelevantItem(itemId, itemClass) then + return false + end local existing = Questie.dbLearner.global.items[itemId] local isNew = existing == nil @@ -2110,6 +2190,10 @@ function QuestieLearner:LearnItem(itemId, name, itemLevel, requiredLevel, itemCl Questie.dbLearner.global.items[itemId] = existing end + if itemClass == 12 or HasQuestReferences(itemId) then + existing.questRelevant = true + end + if name and not existing[1] then existing[1] = name end if itemLevel and itemLevel > 0 and not existing[9] then existing[9] = itemLevel end if requiredLevel and requiredLevel > 0 and not existing[10] then existing[10] = requiredLevel end @@ -2138,6 +2222,7 @@ function QuestieLearner:LearnItem(itemId, name, itemLevel, requiredLevel, itemCl CrossLinkAfterItem(itemId) end _Learner:BroadcastIfCommsAvailable("ITEM", itemId, existing) + return true end function QuestieLearner:LearnItemDrop(itemId, npcId) @@ -2713,9 +2798,11 @@ function QuestieLearner:InjectLearnedData() if type(itemId) == "string" and iid then itemIdsToFix[itemId] = iid end - if not QuestieDB.itemDataOverrides[iid or itemId] then - QuestieDB.itemDataOverrides[iid or itemId] = data - itemCount = itemCount + 1 + if HasQuestReferences(iid or itemId) then + if not QuestieDB.itemDataOverrides[iid or itemId] then + QuestieDB.itemDataOverrides[iid or itemId] = data + itemCount = itemCount + 1 + end end end for old, new in pairs(itemIdsToFix) do @@ -3474,8 +3561,8 @@ function QuestieLearner:OnLootOpened() if itemId and itemId > 0 then local itemName, _, _, itemLevel, requiredLevel, _, _, _, _, _, _, itemClassId, itemSubClassId = GetItemInfo(link) if itemName then - self:LearnItem(itemId, itemName, itemLevel, requiredLevel, itemClassId, itemSubClassId) - if npcId then self:LearnItemDrop(itemId, npcId) end + local learnedItem = self:LearnItem(itemId, itemName, itemLevel, requiredLevel, itemClassId, itemSubClassId) + if learnedItem and npcId then self:LearnItemDrop(itemId, npcId) end else -- GetItemInfo returned nil; queue for retry (class check happens on retry) table.insert(_Learner.pendingItemLinks, { link = link, itemId = itemId, npcId = npcId }) @@ -3571,8 +3658,8 @@ function QuestieLearner:OnGetItemInfoReceived(itemId) if entry.itemId == itemId then local itemName, _, _, itemLevel, requiredLevel, _, _, _, _, _, _, itemClassId, itemSubClassId = GetItemInfo(entry.link) if itemName then - self:LearnItem(itemId, itemName, itemLevel, requiredLevel, itemClassId, itemSubClassId) - if entry.npcId then self:LearnItemDrop(itemId, entry.npcId) end + local learnedItem = self:LearnItem(itemId, itemName, itemLevel, requiredLevel, itemClassId, itemSubClassId) + if learnedItem and entry.npcId then self:LearnItemDrop(itemId, entry.npcId) end else table.insert(remaining, entry) -- still not cached, keep end @@ -4540,6 +4627,9 @@ function QuestieLearner:_ApplyIncomingNetworkMerge(typ, id, d, op) store = Questie.dbLearner.global.quests elseif typ == "ITEM" then if not Questie.dbLearner.global.settings.learnItems then return false end + if not ((type(d) == "table" and d.questRelevant) or HasQuestReferences(id)) then + return false + end store = Questie.dbLearner.global.items elseif typ == "OBJECT" then if not Questie.dbLearner.global.settings.learnObjects then return false end diff --git a/Tests/QuestieLearnerDataSourceMode_spec.lua b/Tests/QuestieLearnerDataSourceMode_spec.lua index 31afbc0..ab82617 100644 --- a/Tests/QuestieLearnerDataSourceMode_spec.lua +++ b/Tests/QuestieLearnerDataSourceMode_spec.lua @@ -289,24 +289,38 @@ describe("QuestieLearner GUID and loot learning", function() assert.equals("Creature", unitType) end) - it("learns loot item drop sources even when class metadata is not quest-item shaped", function() + it("ignores non-quest loot items and only learns quest-item drops", function() local learnedItemId = nil local dropItemId = nil local dropNpcId = nil QuestieLearner.LearnItem = function(self, itemId, name, itemLevel, requiredLevel, itemClassId, itemSubClassId) - learnedItemId = itemId + if itemClassId == 12 then + learnedItemId = itemId + return true + end + return false end QuestieLearner.LearnItemDrop = function(self, itemId, npcId) dropItemId = itemId dropNpcId = npcId end + local originalGetItemInfo = _G.GetItemInfo + _G.GetItemInfo = function(link) + if link == "item:20470" then + return "Quest Token", nil, nil, 1, 1, nil, nil, nil, nil, nil, nil, 1, 0 + end + return originalGetItemInfo(link) + end + QuestieLearner:OnLootOpened() - assert.equals(20470, learnedItemId) - assert.equals(20470, dropItemId) - assert.equals(15297, dropNpcId) + assert.is_nil(learnedItemId) + assert.is_nil(dropItemId) + assert.is_nil(dropNpcId) + + _G.GetItemInfo = originalGetItemInfo end) end) diff --git a/Tests/QuestieLearner_performance_spec.lua b/Tests/QuestieLearner_performance_spec.lua index 7d395eb..97cef50 100644 --- a/Tests/QuestieLearner_performance_spec.lua +++ b/Tests/QuestieLearner_performance_spec.lua @@ -77,6 +77,7 @@ describe("QuestieLearner kill-path batching", function() npcCache = { [1001] = { name = "Cached Boar" }, }, + itemCache = {}, } QuestiePlayer.currentQuestlog = {} @@ -141,20 +142,37 @@ describe("QuestieLearner kill-path batching", function() assert.equals(1, table.getn(Questie.dbLearner.global.npcs[4001][7][44])) end) - it("records item drop sources even when the item class is not available on first pass", function() + it("ignores non-quest loot items so they do not pollute learner state", function() Questie.dbLearner.global.settings.dataSourceMode = "learner" - QuestieDB.private.itemCache = { - [2301] = { cached = true }, - } + QuestieDB.private.itemCache = {} - QuestieLearner:LearnItem(2301, "Quest Shard", 1, 1, 1, 0) - QuestieLearner:LearnItemDrop(2301, 7301) + local learned = QuestieLearner:LearnItem(2301, "Quest Shard", 1, 1, 1, 0) - assert.is_table(Questie.dbLearner.global.items[2301][2]) - assert.equals(7301, Questie.dbLearner.global.items[2301][2][1]) + assert.is_false(learned) + assert.is_nil(Questie.dbLearner.global.items[2301]) assert.is_nil(QuestieDB.private.itemCache[2301]) end) + it("keeps quest item drop sources when the item is a quest item", function() + Questie.dbLearner.global.settings.dataSourceMode = "learner" + local learned = QuestieLearner:LearnItem(2302, "Quest Shard", 1, 1, 12, 0) + assert.is_true(learned) + assert.is_true(Questie.dbLearner.global.items[2302].questRelevant) + + QuestieLearner:LearnItemDrop(2302, 7301) + + assert.is_table(Questie.dbLearner.global.items[2302][2]) + assert.equals(7301, Questie.dbLearner.global.items[2302][2][1]) + end) + + it("rejects non-quest item network merges without quest references", function() + Questie.dbLearner.global.settings.dataSourceMode = "learner" + local changed = QuestieLearner:_ApplyIncomingNetworkMerge("ITEM", 2401, { [1] = "Arcane Sliver" }, "NEW") + + assert.is_false(changed) + assert.is_nil(Questie.dbLearner.global.items[2401]) + end) + it("clears cached quest data when learner adds questgiver links", function() Questie.dbLearner.global.settings.dataSourceMode = "learner" QuestieDB.private.questCache = {