From c85370de0be5a229caa9862c37f177c3daea30cc Mon Sep 17 00:00:00 2001 From: Xurkon Date: Thu, 4 Jun 2026 21:58:54 -0500 Subject: [PATCH 1/2] fix: replace select eight lookups --- Modules/Quest/QuestEventHandler.lua | 3 ++- Modules/QuestiePlayer.lua | 6 ++++-- Modules/Tracker/QuestieTracker.lua | 4 ++-- Tests/AuditFindings_spec.lua | 19 +++++++++++++++---- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Modules/Quest/QuestEventHandler.lua b/Modules/Quest/QuestEventHandler.lua index bf47028..c87b9bd 100644 --- a/Modules/Quest/QuestEventHandler.lua +++ b/Modules/Quest/QuestEventHandler.lua @@ -237,7 +237,8 @@ end ---@param questLogIndex number ---@param questId number function _QuestEventHandler:QuestAccepted(questLogIndex, questId) - questId = questId or select(8, GetQuestLogTitle(questLogIndex)) + local _, _, _, _, _, _, _, questLogQuestId = GetQuestLogTitle(questLogIndex) + questId = questId or questLogQuestId Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] QUEST_ACCEPTED", questLogIndex, questId) if questLog[questId] and questLog[questId].timer then diff --git a/Modules/QuestiePlayer.lua b/Modules/QuestiePlayer.lua index 5f5ba0b..9689051 100644 --- a/Modules/QuestiePlayer.lua +++ b/Modules/QuestiePlayer.lua @@ -120,7 +120,8 @@ function QuestiePlayer:GetCurrentZoneId() return uiMapId end - return ZoneDB.instanceIdToUiMapId[select(8, GetInstanceInfo())] + local _, _, _, _, _, _, _, instanceMapID = GetInstanceInfo() + return ZoneDB.instanceIdToUiMapId[instanceMapID] end function QuestiePlayer:GetCurrentUiMapId() @@ -129,7 +130,8 @@ function QuestiePlayer:GetCurrentUiMapId() if uiMapId then return uiMapId end - return ZoneDB.instanceIdToUiMapId[select(8, GetInstanceInfo())] + local _, _, _, _, _, _, _, instanceMapID = GetInstanceInfo() + return ZoneDB.instanceIdToUiMapId[instanceMapID] end ---@return number diff --git a/Modules/Tracker/QuestieTracker.lua b/Modules/Tracker/QuestieTracker.lua index be3cc90..f456ef1 100644 --- a/Modules/Tracker/QuestieTracker.lua +++ b/Modules/Tracker/QuestieTracker.lua @@ -413,7 +413,7 @@ function QuestieTracker.Initialize() for i = 1, questsWatched do local questIndex = GetQuestIndexForWatch(i) if questIndex then - local questId = select(8, GetQuestLogTitle(questIndex)) + local _, _, _, _, _, _, _, questId = GetQuestLogTitle(questIndex) if questId then tempQuestIDs[i] = questId end @@ -2277,7 +2277,7 @@ function QuestieTracker:HookBaseTracker() -- Intercept and return a Questie boolean value IsQuestWatched = function(index) - local questId = select(8, GetQuestLogTitle(index)) + local _, _, _, _, _, _, _, questId = GetQuestLogTitle(index) if questId == 0 then -- When an objective progresses in TBC "index" is the questId, but when a quest is manually added to the quest watch -- (e.g. shift clicking it in the quest log) "index" is the questLogIndex. diff --git a/Tests/AuditFindings_spec.lua b/Tests/AuditFindings_spec.lua index 0838df2..e7a600c 100644 --- a/Tests/AuditFindings_spec.lua +++ b/Tests/AuditFindings_spec.lua @@ -197,10 +197,21 @@ describe("Audit Pass 9 - file-by-file findings (snapshot at HEAD)", function() assert.is_true(has(d, "if(dbItem and dbItem.name and (not dbItem.Hidden)) then")) end) - it("[N5] select(8, GetInstanceInfo()) left in QuestiePlayer (5.0 rewrite unfinished)", function() - assert.is_true(has(read("Modules/QuestiePlayer.lua"), "select(8, GetInstanceInfo())")) - -- QuestieLearner was rewritten away from it: - assert.is_true(has(read("Modules/QuestieLearner.lua"), "Lua 5.0 compat")) + it("[N5] explicit unpack replaced select(8, ...) in live quest/instance lookups", function() + local player = read("Modules/QuestiePlayer.lua") + local handler = read("Modules/Quest/QuestEventHandler.lua") + local tracker = read("Modules/Tracker/QuestieTracker.lua") + + assert.is_true(has(player, "local _, _, _, _, _, _, _, instanceMapID = GetInstanceInfo()")) + assert.is_false(has(player, "select(8, GetInstanceInfo())")) + + assert.is_true(has(handler, "local _, _, _, _, _, _, _, questLogQuestId = GetQuestLogTitle(questLogIndex)")) + assert.is_false(has(handler, "select(8, GetQuestLogTitle(questLogIndex))")) + + assert.is_true(has(tracker, "local _, _, _, _, _, _, _, questId = GetQuestLogTitle(questIndex)")) + assert.is_true(has(tracker, "local _, _, _, _, _, _, _, questId = GetQuestLogTitle(index)")) + assert.is_false(has(tracker, "select(8, GetQuestLogTitle(questIndex))")) + assert.is_false(has(tracker, "select(8, GetQuestLogTitle(index))")) end) it("[9.1->11.1] CORRECTED: % modulo IS used (the 'avoided' claim was wrong)", function() From d1f79fd73e96477aa4ef8f05d57293edc9774ec6 Mon Sep 17 00:00:00 2001 From: Xurkon Date: Thu, 4 Jun 2026 22:36:42 -0500 Subject: [PATCH 2/2] fix: prefer ascension tooltip data --- Modules/QuestieLearner.lua | 6 +- Modules/Tooltips/Tooltip.lua | 43 +- Tests/QuestieLearner_performance_spec.lua | 618 ++++++++++++++++++++++ Tests/QuestieTooltip_precedence_spec.lua | 66 +++ 4 files changed, 731 insertions(+), 2 deletions(-) create mode 100644 Tests/QuestieLearner_performance_spec.lua create mode 100644 Tests/QuestieTooltip_precedence_spec.lua diff --git a/Modules/QuestieLearner.lua b/Modules/QuestieLearner.lua index 54c6b9e..a3b85f5 100644 --- a/Modules/QuestieLearner.lua +++ b/Modules/QuestieLearner.lua @@ -40,6 +40,10 @@ local function IsAscensionProtected(dbType, id, key) return protected and protected[key] == true end +local function HasAscensionQuestObjectiveData(questId) + return IsAscensionProtected("QUEST", questId, 10) +end + local function NormalizeSpawnZoneKey(zoneKey) -- 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). @@ -1651,7 +1655,7 @@ function QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText, objectiv -- 3. Register with tooltip system immediately. Preserve the objective icon so -- nameplates can render the correct learned slay/loot/talk marker. local QuestieTooltips = QuestieLoader:ImportModule("QuestieTooltips") - if QuestieTooltips and QuestieTooltips.RegisterObjectiveTooltip then + if QuestieTooltips and QuestieTooltips.RegisterObjectiveTooltip and not HasAscensionQuestObjectiveData(questId) then local objectiveIcon local QuestLogCache = QuestieLoader:ImportModule("QuestLogCache") local objectives = QuestLogCache and QuestLogCache.GetQuestObjectives and QuestLogCache.GetQuestObjectives(questId) diff --git a/Modules/Tooltips/Tooltip.lua b/Modules/Tooltips/Tooltip.lua index 56ca3fc..eca076e 100644 --- a/Modules/Tooltips/Tooltip.lua +++ b/Modules/Tooltips/Tooltip.lua @@ -43,6 +43,31 @@ local _tooltipLastText = "" local _InitObjectiveTexts +local function _GetQuestObjectiveSummary(questId) + if not QuestieDB or not QuestieDB.GetQuest then + return nil + end + + local quest = QuestieDB:GetQuest(questId) + if not quest or not quest.ObjectiveData then + return nil + end + + local summary = {} + for _, objective in ipairs(quest.ObjectiveData) do + local text = objective and (objective.Text or objective.Description) + if type(text) == "string" and text ~= "" then + tinsert(summary, text) + end + end + + if table.getn(summary) == 0 then + return nil + end + + return summary +end + ---@param questId number ---@param key string monster: m_, items: i_, objects: o_ + string name of the objective ---@param objective table @@ -205,7 +230,7 @@ function QuestieTooltips:GetTooltip(key) return nil end - if QuestiePlayer.numberOfGroupMembers > MAX_GROUP_MEMBER_COUNT then + if (QuestiePlayer.numberOfGroupMembers or 0) > MAX_GROUP_MEMBER_COUNT then return nil -- temporary disable tooltips in raids, we should make a proper fix end @@ -345,11 +370,27 @@ elseif key:sub(1,2) == "o_" then if QuestieTooltips.lookupByKey[key] then local playerName = UnitName("player") + local hasObjectiveEntries = false + for _, tooltip in next, QuestieTooltips.lookupByKey[key] do + if not tooltip.name then + hasObjectiveEntries = true + break + end + end + for k, tooltip in next, QuestieTooltips.lookupByKey[key] do if tooltip.name then if Questie.db.profile.showQuestsInNpcTooltip then local questString = QuestieLib:GetColoredQuestName(tooltip.questId, Questie.db.profile.enableTooltipsQuestLevel, true, true) tinsert(tooltipLines, questString) + if not hasObjectiveEntries then + local objectiveSummary = _GetQuestObjectiveSummary(tooltip.questId) + if objectiveSummary then + for _, objectiveText in ipairs(objectiveSummary) do + tinsert(tooltipLines, " |cFFcbcbcb" .. objectiveText .. "|r") + end + end + end end else local objective = tooltip.objective diff --git a/Tests/QuestieLearner_performance_spec.lua b/Tests/QuestieLearner_performance_spec.lua new file mode 100644 index 0000000..f9b3b6c --- /dev/null +++ b/Tests/QuestieLearner_performance_spec.lua @@ -0,0 +1,618 @@ +describe("QuestieLearner kill-path batching", function() + local queuedTimers + local broadcasts + local QuestieLearner + + before_each(function() + dofile("Tests/wow_api_mock.lua") + + queuedTimers = {} + broadcasts = {} + QuestieCompat.C_Timer.After = function(_, fn) + queuedTimers[table.getn(queuedTimers) + 1] = fn + end + + local originalImportModule = QuestieLoader.ImportModule + QuestieLoader.ImportModule = function(self, name) + if name == "QuestieLearnerComms" then + return { + BroadcastLearnedData = function(_, op, typ, id, data) + broadcasts[table.getn(broadcasts) + 1] = { + op = op, + typ = typ, + id = id, + data = data, + } + end, + } + end + return originalImportModule(self, name) + end + + Questie.dbLearner.global.settings.enabled = true + Questie.dbLearner.global.settings.learnNpcs = true + Questie.dbLearner.global.settings.learnQuests = true + Questie.dbLearner.global.settings.learnItems = true + Questie.dbLearner.global.settings.learnObjects = true + Questie.dbLearner.global.settings.minConfidencePins = 1 + Questie.dbLearner.global.npcs = {} + Questie.dbLearner.global.quests = {} + Questie.dbLearner.global.items = {} + Questie.dbLearner.global.objects = {} + QuestieDB.npcDataOverrides = {} + QuestieDB.questDataOverrides = {} + QuestieDB.itemDataOverrides = {} + QuestieDB.objectDataOverrides = {} + QuestieDB.private = { + npcCache = { + [1001] = { name = "Cached Boar" }, + }, + } + QuestiePlayer.currentQuestlog = {} + + QuestieLearner = dofile("Modules/QuestieLearner.lua") + end) + + it("coalesces repeated NPC live updates instead of invalidating DB cache per kill", function() + QuestieLearner:LearnNPC(1001, "Laggy Boar", nil, nil, nil, nil, 41.25, 52.5, 44) + QuestieLearner:LearnNPC(1001, "Laggy Boar", nil, nil, nil, nil, 41.35, 52.6, 44) + + assert.is_nil(QuestieDB.npcDataOverrides[1001]) + assert.is_table(QuestieDB.private.npcCache[1001]) + assert.equals(2, table.getn(queuedTimers)) + + queuedTimers[1]() + + assert.is_table(QuestieDB.npcDataOverrides[1001]) + assert.is_nil(QuestieDB.private.npcCache[1001]) + assert.is_table(QuestieDB.npcDataOverrides[1001][7]) + + local flushedSpawnCount = table.getn(QuestieDB.npcDataOverrides[1001][7][44]) + + QuestieLearner:LearnNPC(1001, "Laggy Boar", nil, nil, nil, nil, 80.0, 80.0, 44) + + assert.equals(flushedSpawnCount, table.getn(QuestieDB.npcDataOverrides[1001][7][44])) + assert.equals(3, table.getn(queuedTimers)) + end) + + it("coalesces repeated quest-pin refreshes into one flush", function() + local updateCount = 0 + local originalUpdateQuest = QuestieQuest.UpdateQuest + QuestieQuest.UpdateQuest = function(_, questId) + updateCount = updateCount + 1 + end + + QuestiePlayer.currentQuestlog = { [5001] = true } + Questie.dbLearner.global.quests = {} + QuestieDB.questDataOverrides = {} + + QuestieLearner:LearnQuestObjectiveNPC(5001, 7001, "Pin Boar slain", 1) + QuestieLearner:LearnQuestObjectiveNPC(5001, 7001, "Pin Boar slain", 1) + + assert.is_true(table.getn(queuedTimers) >= 1) + + for i = 1, table.getn(queuedTimers) do + queuedTimers[i]() + end + + assert.equals(1, updateCount) + + QuestieQuest.UpdateQuest = originalUpdateQuest + end) + + it("suppresses an already queued pin refresh after switching to manual mode", function() + local updateCount = 0 + local originalUpdateQuest = QuestieQuest.UpdateQuest + QuestieQuest.UpdateQuest = function(_, questId) + updateCount = updateCount + 1 + end + + QuestiePlayer.currentQuestlog = { [5003] = true } + Questie.dbLearner.global.quests = {} + QuestieDB.questDataOverrides = {} + + QuestieLearner:LearnQuestObjectiveNPC(5003, 7003, "Manual Boar slain", 1) + + assert.is_true(table.getn(queuedTimers) >= 1) + + Questie.dbLearner.global.settings.pinRefreshMode = "manual" + queuedTimers[1]() + + assert.equals(0, updateCount) + + QuestieQuest.UpdateQuest = originalUpdateQuest + end) + + it("does not learn or refresh pins from bystander UNIT_DIED combat-log events", function() + QuestiePlayer.currentQuestlog = { [5002] = true } + QuestieDB.GetQuest = function() + return { + Objectives = { + { + Id = 7002, + Index = 1, + spawnList = { [7002] = true }, + AlreadySpawned = {}, + }, + }, + } + end + + QuestieLearner:OnCombatLogEvent( + 1234, + "UNIT_DIED", + nil, + nil, + nil, + "Creature-0-0-0-0-7002-0000000001", + "Bystander Boar", + nil + ) + + assert.is_nil(Questie.dbLearner.global.npcs[7002]) + assert.is_nil(QuestieDB.npcDataOverrides[7002]) + assert.equals(0, table.getn(queuedTimers)) + end) + + it("still learns and batches local PARTY_KILL combat-log events", function() + QuestieLearner:OnCombatLogEvent( + 1234, + "PARTY_KILL", + UnitGUID("player"), + UnitName("player"), + nil, + "Creature-0-0-0-0-7003-0000000001", + "Tagged Boar", + nil + ) + + assert.is_table(Questie.dbLearner.global.npcs[7003]) + assert.equals("Tagged Boar", Questie.dbLearner.global.npcs[7003][1]) + assert.equals(2, table.getn(queuedTimers)) + end) + + it("cross-links quest giver NPCs and objects learned after the quest without duplicate pin flushes", function() + local updateCount = 0 + local originalUpdateQuest = QuestieQuest.UpdateQuest + QuestieQuest.UpdateQuest = function(_, questId) + updateCount = updateCount + 1 + end + Questie.db.profile.learnerBroadcast = false + QuestiePlayer.currentQuestlog = { [6001] = true } + + QuestieLearner:LearnQuest(6001, { + [1] = "Cross Link Givers", + [2] = { [1] = { 7101 }, [2] = { 8101 } }, + [3] = { [1] = { 7102 }, [2] = { 8102 } }, + }) + + queuedTimers = {} + + QuestieLearner:LearnNPC(7101, "Starter Guard", nil, nil, nil, nil, 10, 20, 44) + QuestieLearner:LearnNPC(7102, "Finisher Guard", nil, nil, nil, nil, 30, 40, 44) + QuestieLearner:LearnObject(8101, "Starter Chest") + QuestieLearner:LearnObject(8102, "Finisher Chest") + + assert.same({ 6001 }, Questie.dbLearner.global.npcs[7101][10]) + assert.same({ 6001 }, Questie.dbLearner.global.npcs[7102][11]) + assert.same({ 6001 }, Questie.dbLearner.global.objects[8101][2]) + assert.same({ 6001 }, Questie.dbLearner.global.objects[8102][3]) + assert.same({ 6001 }, QuestieDB.npcDataOverrides[7101][10]) + assert.same({ 6001 }, QuestieDB.npcDataOverrides[7102][11]) + assert.same({ 6001 }, QuestieDB.objectDataOverrides[8101][2]) + assert.same({ 6001 }, QuestieDB.objectDataOverrides[8102][3]) + + for i = 1, table.getn(queuedTimers) do + queuedTimers[i]() + end + + assert.equals(1, updateCount) + + QuestieQuest.UpdateQuest = originalUpdateQuest + end) + + it("cross-links item objective drop NPCs into quest creature objectives", function() + Questie.db.profile.learnerBroadcast = false + QuestiePlayer.currentQuestlog = { [6002] = true } + + QuestieLearner:LearnQuest(6002, { + [1] = "Cross Link Drops", + [10] = { + [3] = { + { 2201, 0, 1, "Collect one tusk" }, + }, + }, + }) + QuestieLearner:LearnItem(2201, "Quest Tusk", 1, 1, 12, 0) + QuestieLearner:LearnItemDrop(2201, 7201) + + assert.equals(7201, Questie.dbLearner.global.quests[6002][10][1][1][1]) + assert.equals(7201, QuestieDB.questDataOverrides[6002][10][1][1][1]) + + QuestieLearner:LearnItemDrop(2201, 7201) + + assert.equals(1, table.getn(Questie.dbLearner.global.quests[6002][10][1])) + assert.equals(1, table.getn(QuestieDB.questDataOverrides[6002][10][1])) + end) + + it("defers learner objective tooltip registration to protected AscensionDB quest data", function() + Questie.db.profile.learnerBroadcast = false + QuestiePlayer.currentQuestlog = {} + QuestieDB.ascensionOverrideKeys = { + QUEST = { + [8334] = { + [10] = true, + }, + }, + } + _G._lastRegisteredTooltip = nil + + QuestieLearner:LearnQuestObjectiveNPC(8334, 15271, "Tender slain", 1) + + assert.is_table(Questie.dbLearner.global.quests[8334]) + assert.is_nil(_G._lastRegisteredTooltip) + end) + + it("coalesces repeated learner broadcasts into one latest update", function() + QuestieLearner:LearnItem(2001, "Quest Tusk", 1, 1, 12, 0) + QuestieLearner:LearnItem(2001, "Quest Tusk", 1, 1, 12, 0) + + assert.equals(0, table.getn(broadcasts)) + assert.equals(1, table.getn(queuedTimers)) + + queuedTimers[1]() + + assert.equals(1, table.getn(broadcasts)) + assert.equals("NEW", broadcasts[1].op) + assert.equals("ITEM", broadcasts[1].typ) + assert.equals(2001, broadcasts[1].id) + end) + + it("coalesces repeated inbound network merges into one inject pass", function() + local injectCount = 0 + local originalInject = QuestieLearner.InjectLearnedData + QuestieLearner.InjectLearnedData = function(self) + injectCount = injectCount + 1 + QuestieLearner.data = Questie.dbLearner.global + end + + QuestieLearner:HandleNetworkData("NPC", 3001, { [1] = "Net Boar", [7] = { [44] = { { 11, 22 } } } }, "NEW") + QuestieLearner:HandleNetworkData("NPC", 3001, { [1] = "Net Boar", [7] = { [44] = { { 11, 22 }, { 33, 44 } } } }, "UPDATE") + + assert.equals(0, injectCount) + assert.equals(1, table.getn(queuedTimers)) + + queuedTimers[1]() + + assert.equals(1, injectCount) + assert.equals("Net Boar", Questie.dbLearner.global.npcs[3001][1]) + assert.equals(2, table.getn(Questie.dbLearner.global.npcs[3001][7][44])) + + QuestieLearner.InjectLearnedData = originalInject + end) + + it("forwards OnEvent payload arguments to learner handlers", function() + local originalCreateFrame = _G.CreateFrame + local originalStrsplit = _G.strsplit + local createdFrame = nil + local captured = { + questTurnedIn = nil, + questAccepted = nil, + combatLog = nil, + itemInfoReceived = nil, + questTrackingCleared = nil, + } + + _G.CreateFrame = function() + createdFrame = { + RegisterEvent = function() end, + SetScript = function(self, scriptName, fn) + if scriptName == "OnEvent" then + self._onEvent = fn + end + end, + } + return createdFrame + end + _G.strsplit = function(sep, str) + local parts = {} + local pattern = string.format("([^%s]+)", sep) + for part in string.gmatch(str, pattern) do + parts[#parts + 1] = part + end + return unpack(parts) + end + + QuestieLearner = dofile("Modules/QuestieLearner.lua") + local originalOnQuestTurnedIn = QuestieLearner.OnQuestTurnedIn + local originalOnQuestAccepted = QuestieLearner.OnQuestAccepted + local originalOnCombatLogEvent = QuestieLearner.OnCombatLogEvent + local originalOnGetItemInfoReceived = QuestieLearner.OnGetItemInfoReceived + local originalClearQuestObjectiveTracking = QuestieLearner.ClearQuestObjectiveTracking + + QuestieLearner.OnQuestTurnedIn = function(self, questId, npcId, questFlags) + captured.questTurnedIn = { questId, npcId, questFlags } + end + QuestieLearner.OnQuestAccepted = function(self, questId, questGiver) + captured.questAccepted = { questId, questGiver } + end + QuestieLearner.OnCombatLogEvent = function(self, timestamp, eventType, srcGUID, srcName, srcFlags, dstGUID, dstName, dstFlags, spellId, spellName) + captured.combatLog = { timestamp, eventType, srcGUID, srcName, srcFlags, dstGUID, dstName, dstFlags, spellId, spellName } + end + QuestieLearner.OnGetItemInfoReceived = function(self, itemId) + captured.itemInfoReceived = { itemId } + end + QuestieLearner.ClearQuestObjectiveTracking = function(self, questId) + captured.questTrackingCleared = { questId } + end + + QuestieLearner:RegisterEvents() + + local frame = createdFrame + frame._onEvent(frame, "QUEST_TURNED_IN", 101, 202, 303) + frame._onEvent(frame, "QUEST_ACCEPTED", 404, 505) + frame._onEvent(frame, "COMBAT_LOG_EVENT_UNFILTERED", 1, "SPELL_DAMAGE", "src-guid", "Src", 2, "dst-guid", "Dst", 4, 777, "Fireball") + frame._onEvent(frame, "GET_ITEM_INFO_RECEIVED", 888) + frame._onEvent(frame, "QUEST_REMOVED", 999) + + assert.same({ 101, 202, 303 }, captured.questTurnedIn) + assert.same({ 404, 505 }, captured.questAccepted) + assert.same({ 1, "SPELL_DAMAGE", "src-guid", "Src", 2, "dst-guid", "Dst", 4, 777, "Fireball" }, captured.combatLog) + assert.same({ 888 }, captured.itemInfoReceived) + assert.same({ 999 }, captured.questTrackingCleared) + + QuestieLearner.OnQuestTurnedIn = originalOnQuestTurnedIn + QuestieLearner.OnQuestAccepted = originalOnQuestAccepted + QuestieLearner.OnCombatLogEvent = originalOnCombatLogEvent + QuestieLearner.OnGetItemInfoReceived = originalOnGetItemInfoReceived + QuestieLearner.ClearQuestObjectiveTracking = originalClearQuestObjectiveTracking + _G.CreateFrame = originalCreateFrame + _G.strsplit = originalStrsplit + end) +end) + +describe("QuestieLearnerComms queue draining", function() + local sentMessages + local processedMessages + local QuestieLearnerComms + local currentTime + + before_each(function() + dofile("Tests/wow_api_mock.lua") + + sentMessages = {} + processedMessages = {} + currentTime = 1000 + + local originalImportModule = QuestieLoader.ImportModule + QuestieLoader.ImportModule = function(self, name) + if name == "QuestieLearner" then + return _G.QuestieLearner + end + return originalImportModule(self, name) + end + QuestieLoader.CreateModule = function(self, name) + _G[name] = { private = {} } + return _G[name] + end + + _G.LibStub = function(name, silent) + if name == "AceComm-3.0" then + return { RegisterComm = function() end } + elseif name == "LibDeflate" then + return { + CompressDeflate = function(_, s) return s end, + EncodeForPrint = function(_, s) return s end, + DecodeForPrint = function(_, s) return s end, + DecompressDeflate = function(_, s) return s end, + } + elseif name == "AceSerializer-3.0" then + return { + Serialize = function(_, payload) + return payload.op .. ":" .. tostring(payload.id) + end, + Deserialize = function(_, serialized) + local op, id = string.match(serialized, "([^:]+):(.+)") + return true, { + _ver = 2, + op = op, + typ = "NPC", + id = tonumber(id) or id, + d = { [1] = "net" }, + } + end, + } + elseif name == "XXH_Lua_Lib" then + return nil + elseif name == "HereBeDragonsQuestie-2.0" then + return {} + end + return {} + end + + _G.QuestieLearner = { + HandleNetworkData = function(_, typ, id, d, op) + processedMessages[table.getn(processedMessages) + 1] = { + typ = typ, + id = id, + op = op, + } + end, + } + + _G.GetChannelName = function() return 1 end + _G.JoinPermanentChannel = function() end + _G.ChatFrame_RemoveChannel = function() end + _G.DEFAULT_CHAT_FRAME = { GetID = function() return 1 end } + _G.SendChatMessage = function(msg, mode, nilarg, channel) + sentMessages[table.getn(sentMessages) + 1] = msg + end + _G.InCombatLockdown = function() return false end + _G.random = function() return 0 end + _G.GetTime = function() + return currentTime + end + QuestieCompat.C_Timer.NewTicker = function(delay, fn) + return { Cancel = function() end } + end + + dofile("Modules/Network/QuestieLearnerComms.lua") + QuestieLearnerComms = _G.QuestieLearnerComms + QuestieLearnerComms:Initialize() + end) + + it("drains outgoing and incoming queues in FIFO order without front-removal", function() + QuestieLearnerComms:BroadcastLearnedData("NEW", "NPC", 101, { foo = "a" }) + QuestieLearnerComms:BroadcastLearnedData("UPDATE", "NPC", 102, { foo = "b" }) + + QuestieLearnerComms:OnCommReceived("QuestieLearner", "NEW:201", "CHANNEL", "Alice") + QuestieLearnerComms:OnCommReceived("QuestieLearner", "UPDATE:202", "CHANNEL", "Bob") + + QuestieLearnerComms.private:ProcessQueues() + currentTime = currentTime + 4 + QuestieLearnerComms.private:ProcessQueues() + currentTime = currentTime + 4 + QuestieLearnerComms.private:ProcessQueues() + + assert.equals(2, table.getn(sentMessages)) + assert.equals("NEW:101", sentMessages[1]) + assert.equals("UPDATE:102", sentMessages[2]) + assert.equals(2, table.getn(processedMessages)) + assert.equals(201, processedMessages[1].id) + assert.equals(202, processedMessages[2].id) + end) +end) + +describe("QuestieComms packet sizing", function() + local serializeCount + local sendCount + local QuestieComms + + before_each(function() + dofile("Tests/wow_api_mock.lua") + + serializeCount = 0 + sendCount = 0 + + local originalCreateModule = QuestieLoader.CreateModule + local originalImportModule = QuestieLoader.ImportModule + QuestieLoader.CreateModule = function(self, name) + _G[name] = { private = {} } + return _G[name] + end + QuestieLoader.ImportModule = function(self, name) + if name == "QuestieSerializer" then + return { + Serialize = function(_, payload) + serializeCount = serializeCount + 1 + return "packet:" .. tostring(payload.id or payload[1] or "x") + end, + Deserialize = function() return true, {} end, + } + end + return originalImportModule(self, name) + end + + _G.LibStub = function(name, silent) + if name == "AceComm-3.0" then + return { RegisterComm = function() end } + elseif name == "LibDeflate" then + return { + CompressDeflate = function(_, s) return s end, + EncodeForPrint = function(_, s) return s end, + DecodeForPrint = function(_, s) return s end, + DecompressDeflate = function(_, s) return s end, + } + elseif name == "AceSerializer-3.0" then + return { + Serialize = function(_, payload) return "packet:" .. tostring(payload.id or payload[1] or "x") end, + Deserialize = function() return true, {} end, + } + elseif name == "XXH_Lua_Lib" then + return nil + elseif name == "HereBeDragonsQuestie-2.0" then + return {} + end + return {} + end + + _G.QuestiePlayer.GetGroupType = function() return "party" end + _G.QuestiePlayer.numberOfGroupMembers = 5 + _G.QuestieDB.QuestPointers = { + [101] = true, + [102] = true, + [103] = true, + } + _G.QuestieDB.QueryQuestSingle = function() + return 0 + end + _G.QuestieDB.GetQuest = function(questId) + return { + Objectives = { + [1] = { Id = questId * 10 }, + }, + } + end + _G.QuestLogCache.questLog_DO_NOT_MODIFY = { + [101] = { questTag = "Normal" }, + [102] = { questTag = "Normal" }, + [103] = { questTag = "Normal" }, + } + _G.QuestLogCache.GetQuestObjectives = function() + return { + { type = "monster", numFulfilled = 0, numRequired = 1 }, + } + end + _G.ZoneDB.GetUiMapIdByAreaId = function() + return 1 + end + _G.HBD = { + GetZoneDistance = function() return 1 end, + GetPlayerZone = function() return 1 end, + } + _G.GetChannelName = function() return 1 end + _G.SendChatMessage = function() end + _G.Questie.SendCommMessage = function() + sendCount = sendCount + 1 + end + _G.UnitInBattleground = function() return false end + _G.random = function() return 0 end + _G.GetTime = function() return 0 end + _G.tinsert = table.insert + QuestieCompat.C_Timer.After = function(_, fn) end + QuestieCompat.C_Timer.NewTicker = function(_, fn) + return { Cancel = function() end } + end + + QuestieComms = dofile("Modules/Network/QuestieComms.lua") + QuestieComms.private.CreatePacket = function() + return { + data = {}, + write = function() end, + } + end + + serializeCount = 0 + end) + + it("serializes each quest once while packing broadcast blocks", function() + QuestieComms.private:BroadcastQuestLog("QC_ID_BROADCAST_FULL_QUESTLIST", "WHISPER", "Tester") + assert.equals(3, serializeCount) + + serializeCount = 0 + QuestieComms.private:BroadcastQuestLogV2("QC_ID_BROADCAST_FULL_QUESTLIST", "WHISPER", "Tester") + assert.equals(3, serializeCount) + end) + + it("suppresses outgoing QuestieComms immediately when disabled", function() + Questie.db.profile.questieCommsEnabled = false + + local packet = QuestieComms.private:CreatePacket(QuestieComms.private.QC_ID_BROADCAST_QUEST_REMOVE) + packet.data.writeMode = QuestieComms.private.QC_WRITE_ALLGROUP + packet.data.priority = "NORMAL" + packet.data.id = 101 + packet:write() + + assert.equals(0, sendCount) + end) +end) diff --git a/Tests/QuestieTooltip_precedence_spec.lua b/Tests/QuestieTooltip_precedence_spec.lua new file mode 100644 index 0000000..f442cdb --- /dev/null +++ b/Tests/QuestieTooltip_precedence_spec.lua @@ -0,0 +1,66 @@ +describe("Questie tooltip precedence", function() + local function contains(lines, needle) + for _, line in ipairs(lines or {}) do + if string.find(line, needle, 1, true) then + return true + end + end + return false + end + + before_each(function() + dofile("Tests/wow_api_mock.lua") + + local originalImportModule = QuestieLoader.ImportModule + QuestieLoader.ImportModule = function(self, name) + if name == "QuestieLib" then + return { + GetColoredQuestName = function(_, questId) + return "Quest " .. tostring(questId) + end, + Colorize = function(_, text) + return text + end, + } + end + return originalImportModule(self, name) + end + + Questie.db.profile.showQuestsInNpcTooltip = true + Questie.db.profile.enableTooltipsQuestLevel = false + Questie.db.profile.enableTooltipsNPCID = false + QuestiePlayer.numberOfGroupMembers = 0 + QuestieCompat.IsInGroup = function() return false end + QuestieCompat.UnitInParty = function() return false end + + QuestieDB.GetQuest = function(_, questId) + if questId ~= 8334 then + return nil + end + + return { + ObjectiveData = { + [1] = { Type = "monster", Id = 15271, Text = "Tender slain" }, + [2] = { Type = "monster", Id = 15294, Text = "Feral Tender slain" }, + }, + } + end + + dofile("Modules/Tooltips/Tooltip.lua") + QuestieTooltips.lookupByKey["m_15271"] = { + ["8334 Tender 15271"] = { + questId = 8334, + name = "Tender", + starterId = 15271, + }, + } + end) + + it("adds AscensionDB objective text under quest titles when no objective tooltip is registered", function() + local lines = QuestieTooltips:GetTooltip("m_15271") + + assert.is_true(contains(lines, "Quest 8334")) + assert.is_true(contains(lines, "Tender slain")) + assert.is_true(contains(lines, "Feral Tender slain")) + end) +end)