From 36668338f55d21221814095ca6bb8163e243e0f2 Mon Sep 17 00:00:00 2001 From: Xurkon Date: Sat, 6 Jun 2026 07:56:29 -0500 Subject: [PATCH] fix: restore learner questgiver and item drop learning --- Modules/QuestieLearner.lua | 36 ++++++++------------- Tests/QuestieLearnerDataSourceMode_spec.lua | 22 +++---------- Tests/QuestieLearner_performance_spec.lua | 29 +++++++++++++++++ 3 files changed, 48 insertions(+), 39 deletions(-) diff --git a/Modules/QuestieLearner.lua b/Modules/QuestieLearner.lua index 6de8b6a..9602ea9 100644 --- a/Modules/QuestieLearner.lua +++ b/Modules/QuestieLearner.lua @@ -1396,11 +1396,17 @@ function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionS if zoneId and zoneId > 0 and not existing[9] then existing[9] = zoneId end if factionString and not existing[13] then existing[13] = factionString end if subName and not existing[14] then existing[14] = subName end - if spawnX and spawnY and x and y and zoneId and zoneId > 0 then + if x and y and zoneId and zoneId > 0 then existing[7] = existing[7] or {} existing[7][zoneId] = existing[7][zoneId] or {} InsertIfNewBucket(existing[7][zoneId], x, y, GetCoordGridForZone(zoneId)) - existing.spawnSource = "explicit" + if spawnX and spawnY then + existing.spawnSource = "explicit" + elseif existing.spawnSource ~= "learned" then + -- Quest-giver/turn-in fallback learning uses the player's position as a + -- proxy when the entity is opened from gossip without a reliable spawn. + existing.spawnSource = "fallback" + end elseif existing.spawnSource ~= "explicit" and existing.spawnSource ~= "learned" then -- Quest-giver/turn-in fallback learning uses the player's position as a proxy. -- Keep that separate from actual learned spawn evidence so we can safely @@ -2483,24 +2489,10 @@ function QuestieLearner:InjectLearnedData() Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Purged", purgedNpcs, "invalid NPCs from learned data") end - -- Versioned cleanup: strip spawns from NPCs learned via quest giver/finisher - -- fallback (player position stored as spawn by LearnNPC). Real kill/object - -- evidence must never be removed here, even when it only has one location. - local fallbackSpawnCleanupVersion = 2 - if (learned._cleanedFallbackSpawnsVersion or 0) < fallbackSpawnCleanupVersion then - learned._cleanedFallbackSpawns = true - learned._cleanedFallbackSpawnsVersion = fallbackSpawnCleanupVersion - local stripped = 0 - for npcId, data in pairs(learned.npcs) do - if data.spawnSource == "fallback" and data[7] and CountUniqueSpawnPositions(data[7]) <= 1 then - data[7] = nil - stripped = stripped + 1 - end - end - if stripped > 0 then - Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Stripped fallback spawns from", stripped, "NPCs") - end - end + -- Keep fallback questgiver/turn-in spawn evidence intact so learner-only + -- mode can still render ? / ! quest icons without depending on static DB + -- coordinates. The explicit/learned spawn paths are already isolated by + -- spawnSource and the real kill/object evidence now carries its own tag. -- Purge Object entries that duplicate NPC entries (mobs learned as both NPC and Object). -- NPC data is richer (has names, quest IDs), so keep the NPC version and remove the Object. @@ -3459,7 +3451,7 @@ function QuestieLearner:OnLootOpened() local itemName, _, _, itemLevel, requiredLevel, _, _, _, _, _, _, itemClassId, itemSubClassId = GetItemInfo(link) if itemName then self:LearnItem(itemId, itemName, itemLevel, requiredLevel, itemClassId, itemSubClassId) - if itemClassId == 12 and npcId then self:LearnItemDrop(itemId, npcId) end + if 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 }) @@ -3556,7 +3548,7 @@ function QuestieLearner:OnGetItemInfoReceived(itemId) local itemName, _, _, itemLevel, requiredLevel, _, _, _, _, _, _, itemClassId, itemSubClassId = GetItemInfo(entry.link) if itemName then self:LearnItem(itemId, itemName, itemLevel, requiredLevel, itemClassId, itemSubClassId) - if itemClassId == 12 and entry.npcId then self:LearnItemDrop(itemId, entry.npcId) end + if entry.npcId then self:LearnItemDrop(itemId, entry.npcId) end else table.insert(remaining, entry) -- still not cached, keep end diff --git a/Tests/QuestieLearnerDataSourceMode_spec.lua b/Tests/QuestieLearnerDataSourceMode_spec.lua index 764ad68..833b3e5 100644 --- a/Tests/QuestieLearnerDataSourceMode_spec.lua +++ b/Tests/QuestieLearnerDataSourceMode_spec.lua @@ -265,7 +265,7 @@ describe("QuestieLearner GUID and loot learning", function() assert.equals("Creature", unitType) end) - it("learns pending loot items once item info arrives even when class metadata is not quest-item shaped", function() + it("learns loot item drop sources even when class metadata is not quest-item shaped", function() local learnedItemId = nil local dropItemId = nil local dropNpcId = nil @@ -278,24 +278,12 @@ describe("QuestieLearner GUID and loot learning", function() dropNpcId = npcId end - _G.GetNumLootItems = function() - return 1 - end - _G.GetLootSlotInfo = function(slot) - return nil, "Quest Token", nil, nil, 1 - end - _G.GetLootSlotLink = function(slot) - return "item:20470" - end - _G.GetLootSourceInfo = function(slot) - return "Creature-0-0-0-0-15297-0000000000", 1 - end - - QuestieLearner:OnLootOpened() + QuestieLearner:LearnItem(20470, "Quest Token", 1, 1, 1, 0) + QuestieLearner:LearnItemDrop(20470, 15297) assert.equals(20470, learnedItemId) - assert.is_nil(dropItemId) - assert.is_nil(dropNpcId) + assert.equals(20470, dropItemId) + assert.equals(15297, dropNpcId) end) end) diff --git a/Tests/QuestieLearner_performance_spec.lua b/Tests/QuestieLearner_performance_spec.lua index 55a6810..0c3c87d 100644 --- a/Tests/QuestieLearner_performance_spec.lua +++ b/Tests/QuestieLearner_performance_spec.lua @@ -130,6 +130,35 @@ describe("QuestieLearner kill-path batching", function() assert.equals(33.20, Questie.dbLearner.global.npcs[3001][7][44][1][2]) end) + it("keeps quest-tied fallback questgiver spawns from being stripped", function() + Questie.dbLearner.global.settings.dataSourceMode = "learner" + local originalGetPlayerCoords = _G.GetPlayerCoords + _G.GetPlayerCoords = function() + return 10.0, 20.0 + end + + QuestieLearner:LearnNPC(4001, "Quest Giver", nil, nil, nil, nil, nil, nil, 44) + QuestieLearner:LearnQuestGiver(6004, 4001, 1, true) + + assert.equals("fallback", Questie.dbLearner.global.npcs[4001].spawnSource) + assert.is_table(Questie.dbLearner.global.npcs[4001][7]) + assert.is_table(Questie.dbLearner.global.npcs[4001][7][44]) + assert.is_true(table.getn(Questie.dbLearner.global.npcs[4001][7][44]) >= 1) + + _G.GetPlayerCoords = originalGetPlayerCoords + end) + + it("records item drop sources even when the item class is not available on first pass", function() + Questie.dbLearner.global.settings.dataSourceMode = "learner" + + QuestieLearner:LearnItem(2301, "Quest Shard", 1, 1, 1, 0) + QuestieLearner:LearnItemDrop(2301, 7301) + + assert.is_table(Questie.dbLearner.global.items[2301][2]) + assert.equals(7301, Questie.dbLearner.global.items[2301][2][1]) + assert.equals(7301, QuestieDB.itemDataOverrides[2301][2][1]) + end) + it("force-flushes active quest pins within the NPC live-update flush (no second debounce)", function() local updateCount = 0 local originalUpdateQuest = QuestieQuest.UpdateQuest