fix: prefer ascension tooltip data

This commit is contained in:
Xurkon
2026-06-04 22:36:42 -05:00
parent c85370de0b
commit d1f79fd73e
4 changed files with 731 additions and 2 deletions
+618
View File
@@ -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)