perf: batch learner updates and remove Turtle support
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
-- Shim for Lua 5.2+ (Retail) where table.getn was removed.
|
||||
-- We cannot use the '#' length operator directly because Lua 5.0 (Turtle WoW)
|
||||
-- We cannot use the '#' length operator directly because Lua 5.0
|
||||
-- will trigger a compile-time syntax error parsing the file.
|
||||
-- Using loadstring bypasses the 5.0 compiler and safely injects the # operator in 5.2+.
|
||||
if not table.getn then
|
||||
@@ -19,7 +19,7 @@ if not math.mod then
|
||||
end
|
||||
end
|
||||
|
||||
-- Shim for Lua 5.0 (Turtle WoW) where string.match is missing.
|
||||
-- Shim for Lua 5.0 where string.match is missing.
|
||||
-- Supports up to 5 captures (sufficient for all Questie uses).
|
||||
if not string.match then
|
||||
string.match = function(str, pattern, init)
|
||||
@@ -41,7 +41,7 @@ if not string.gmatch then
|
||||
string.gmatch = string.gfind
|
||||
end
|
||||
|
||||
-- Shim for Lua 5.0 (Turtle WoW) where select() was not yet implemented.
|
||||
-- Shim for Lua 5.0 where select() was not yet implemented.
|
||||
-- Fix #7: The original used a `while n > 0` loop that never decremented n,
|
||||
-- making the loop body run exactly once before returning. Use a plain
|
||||
-- sequential block instead so the intent is obvious.
|
||||
@@ -136,4 +136,3 @@ function Questie:Colorize(str, color)
|
||||
-- Initial stub: just return the string without color or with basic color
|
||||
return str
|
||||
end
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ function QuestiePluginAPI:HasPendingPlugins()
|
||||
return self.pendingPluginsCount > 0
|
||||
end
|
||||
|
||||
--- Returns the flavor key of the loaded DB plugin ("WotLK", "Classic", "TBC", "Turtle", "Ascension", etc.)
|
||||
--- Returns the flavor key of the loaded DB plugin ("WotLK", "Classic", "TBC", "Ascension", etc.)
|
||||
---@return string|nil
|
||||
function QuestiePluginAPI:GetLoadedFlavor()
|
||||
return self.loadedDBFlavor
|
||||
@@ -276,7 +276,7 @@ function QuestiePlugin:InjectUiMapData(customUiMapData)
|
||||
end
|
||||
|
||||
--- Signals that the plugin has finished loading. This automatically cleans up necessary caches.
|
||||
---@param flavorKey string|nil Optional flavor label e.g. "WotLK", "Classic", "Turtle"
|
||||
---@param flavorKey string|nil Optional flavor label e.g. "WotLK", "Classic", "Ascension"
|
||||
function QuestiePlugin:FinishLoading(flavorKey)
|
||||
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
|
||||
if not QuestieDB then
|
||||
|
||||
@@ -95,7 +95,7 @@ function QuestieOptions.tabs.credits:Initialize()
|
||||
thanks = {
|
||||
type = "description",
|
||||
order = 1,
|
||||
name = function() return 'The Turtle WoW, Project Ascension, and Project Ebonhold communities for their ongoing support and feedback.'; end,
|
||||
name = function() return 'The Project Ascension, Project Ebonhold, and custom server communities for their ongoing support and feedback.'; end,
|
||||
fontSize = "medium",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -20,7 +20,6 @@ end
|
||||
|
||||
local function GetServer()
|
||||
if Questie.IsAscension then return "Ascension" end
|
||||
if Questie.IsTurtle then return "Turtle" end
|
||||
if Questie.IsEbonhold then return "Ebonhold" end
|
||||
if Questie.IsEra then return "Era" end
|
||||
if Questie.Is335 then return "WotLK" end
|
||||
|
||||
@@ -241,7 +241,7 @@ end
|
||||
-- old Lua 5.0 host with no secure-call protection), addon code can still call it
|
||||
-- but it will simply be a no-op that prints a warning rather than injecting taint.
|
||||
if not hooksecurefunc then
|
||||
-- Lua 5.0 hosts (Turtle WoW pre-2.0 or custom servers) have no secure-call model,
|
||||
-- Lua 5.0 hosts and older custom servers have no secure-call model,
|
||||
-- so raw-hooking is equivalent to what Blizzard would do internally anyway.
|
||||
-- Use a local to avoid polluting _G unnecessarily.
|
||||
local function _rawHook(arg1, arg2, arg3)
|
||||
@@ -278,7 +278,7 @@ end
|
||||
if C_Timer then
|
||||
QuestieCompat.C_Timer = C_Timer
|
||||
else
|
||||
-- C_Timer polyfill for Lua 5.0/5.1 clients that don't have it (e.g. Turtle WoW pre-2.0).
|
||||
-- C_Timer polyfill for Lua 5.0/5.1 clients that don't have it.
|
||||
-- Only stored in QuestieCompat namespace, NOT in bare _G.
|
||||
local TickerFrame = CreateFrame("Frame")
|
||||
local tickers = {}
|
||||
@@ -626,4 +626,3 @@ end
|
||||
QuestieCompat.LibUIDropDownMenu = QuestieCompat.LibUIDropDownMenu or {}
|
||||
QuestieCompat.LibUIDropDownMenu.UIDropDownMenu_Menu_NewSize = function()
|
||||
end
|
||||
|
||||
|
||||
@@ -279,7 +279,7 @@ QuestieInit.Stages[1] = function() -- run as a coroutine
|
||||
needsCompilation = (not dbIsCompiled) or (QuestieLib:GetAddonVersionString() ~= dbCompiledOnVersion) or (l10n:GetUILocale() ~= dbCompiledLang) or (Questie.db.global.dbCompiledExpansion ~= WOW_PROJECT_ID)
|
||||
|
||||
-- Custom servers or presence of DB plugins: always defer to Stage3 to wait for plugin data injection
|
||||
if Questie.IsAscension or Questie.IsEbonhold or Questie.IsValanior or Questie.IsTurtle or QuestieServer:IsAnyDBPluginEnabled() then
|
||||
if Questie.IsAscension or Questie.IsEbonhold or Questie.IsValanior or QuestieServer:IsAnyDBPluginEnabled() then
|
||||
compilationDeferred = true
|
||||
l10n:Initialize()
|
||||
coYield()
|
||||
@@ -390,7 +390,7 @@ QuestieInit.Stages[3] = function() -- run as a coroutine
|
||||
|
||||
-- Always re-compile on custom servers to pick up QuestieLearner changes from SavedVariables,
|
||||
-- or if compilation was explicitly deferred/needed.
|
||||
local isCustomServer = Questie.IsAscension or Questie.IsEbonhold or Questie.IsValanior or Questie.IsTurtle or QuestieServer:IsAnyDBPluginEnabled()
|
||||
local isCustomServer = Questie.IsAscension or Questie.IsEbonhold or Questie.IsValanior or QuestieServer:IsAnyDBPluginEnabled()
|
||||
if isCustomServer or needsCompilation or (not Questie.db.global.dbIsCompiled) then
|
||||
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieInit:Stage3] Starting compilation (Server=" .. tostring(isCustomServer) .. ", Needed=" .. tostring(needsCompilation) .. ")")
|
||||
if not QuestieDB.questData then
|
||||
|
||||
+137
-47
@@ -210,6 +210,16 @@ local function CopyWithoutField(data, skippedKey)
|
||||
return copy
|
||||
end
|
||||
|
||||
local function DeepCopy(value)
|
||||
if type(value) ~= "table" then return value end
|
||||
|
||||
local copy = {}
|
||||
for key, child in pairs(value) do
|
||||
copy[key] = DeepCopy(child)
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
-- Returns the grid-bucket key for a coordinate so nearby points share the same slot
|
||||
local function CoordBucket(x, y)
|
||||
return floor(x / COORD_GRID) * COORD_GRID, floor(y / COORD_GRID) * COORD_GRID
|
||||
@@ -565,6 +575,89 @@ local function _InvalidateSpawnListsForNPC(npcId)
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- Live learner update batching
|
||||
--
|
||||
-- Kill/loot bursts can call LearnNPC multiple times for the same NPC:
|
||||
-- combat-log kill, quest-log objective progress, and loot correlation may all
|
||||
-- arrive within a few frames. Saved learner evidence is updated immediately,
|
||||
-- but live QuestieDB override/cache invalidation is batched so the large DB
|
||||
-- layer is not churned on every single kill.
|
||||
local LIVE_NPC_UPDATE_DELAY = 0.5
|
||||
|
||||
local function _ApplyNpcLiveUpdate(npcId)
|
||||
local existing = Questie.dbLearner
|
||||
and Questie.dbLearner.global
|
||||
and Questie.dbLearner.global.npcs
|
||||
and Questie.dbLearner.global.npcs[npcId]
|
||||
if not existing then return false end
|
||||
|
||||
local threshold = (Questie.dbLearner.global.settings and Questie.dbLearner.global.settings.minConfidencePins) or MIN_CONFIDENCE_PINS
|
||||
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
|
||||
|
||||
local ovr = QuestieDB.npcDataOverrides[npcId]
|
||||
if not ovr then
|
||||
if IsAscensionProtected("NPC", npcId, 7) then
|
||||
QuestieDB.npcDataOverrides[npcId] = DeepCopy(CopyWithoutField(existing, 7))
|
||||
else
|
||||
QuestieDB.npcDataOverrides[npcId] = DeepCopy(existing)
|
||||
end
|
||||
else
|
||||
-- Merge: fill missing fields; also overwrite empty-string names.
|
||||
for k, v in pairs(existing) do
|
||||
if k ~= 7 and not IsAscensionProtected("NPC", npcId, k) and (ovr[k] == nil or (k == 1 and ovr[k] == "")) then
|
||||
ovr[k] = DeepCopy(v)
|
||||
end
|
||||
end
|
||||
-- Always merge spawn coords.
|
||||
if existing[7] and not IsAscensionProtected("NPC", npcId, 7) then
|
||||
ovr[7] = ovr[7] or {}
|
||||
for zid, coords in pairs(existing[7]) do
|
||||
ovr[7][zid] = ovr[7][zid] or {}
|
||||
for _, coord in ipairs(coords) do
|
||||
InsertIfNewBucket(ovr[7][zid], coord[1], coord[2], GetCoordGridForZone(zid))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Clear the compiled DB cache once per flush so GetNPC rebuilds with the
|
||||
-- latest coalesced override data instead of once per kill.
|
||||
if QuestieDB.private and QuestieDB.private.npcCache then
|
||||
QuestieDB.private.npcCache[npcId] = nil
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function _FlushNpcLiveUpdates()
|
||||
local pending = _Learner.pendingNpcLiveUpdates
|
||||
_Learner.pendingNpcLiveUpdates = {}
|
||||
_Learner.pendingNpcLiveUpdateTimer = nil
|
||||
|
||||
for npcId in pairs(pending) do
|
||||
if _ApplyNpcLiveUpdate(npcId) then
|
||||
_InvalidateSpawnListsForNPC(npcId)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function _QueueNpcLiveUpdate(npcId)
|
||||
_Learner.pendingNpcLiveUpdates = _Learner.pendingNpcLiveUpdates or {}
|
||||
_Learner.pendingNpcLiveUpdates[npcId] = true
|
||||
|
||||
if _Learner.pendingNpcLiveUpdateTimer then return end
|
||||
|
||||
local timer = QuestieCompat and QuestieCompat.C_Timer
|
||||
if timer and timer.After then
|
||||
_Learner.pendingNpcLiveUpdateTimer = true
|
||||
timer.After(LIVE_NPC_UPDATE_DELAY, _FlushNpcLiveUpdates)
|
||||
else
|
||||
_FlushNpcLiveUpdates()
|
||||
end
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------
|
||||
-- CrossLinkAfterNPC: called when a new NPC is first learned.
|
||||
-- Scans all learned quests for any reference to this npcId and stitches
|
||||
@@ -887,48 +980,14 @@ function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionS
|
||||
existing.ls = time() -- Update last seen
|
||||
existing.mc = (existing.mc or 0) + 1
|
||||
|
||||
local threshold = (Questie.dbLearner.global.settings and Questie.dbLearner.global.settings.minConfidencePins) or MIN_CONFIDENCE_PINS
|
||||
|
||||
-- Live injection: update npcDataOverrides only if confidence threshold is met
|
||||
-- and only if the NPC has actual spawn data from kills (not just player-position fallback)
|
||||
if existing.mc >= threshold and QuestieDB and QuestieDB.npcDataOverrides and existing[7] and next(existing[7]) then
|
||||
local ovr = QuestieDB.npcDataOverrides[npcId]
|
||||
if not ovr then
|
||||
if IsAscensionProtected("NPC", npcId, 7) then
|
||||
QuestieDB.npcDataOverrides[npcId] = CopyWithoutField(existing, 7)
|
||||
else
|
||||
QuestieDB.npcDataOverrides[npcId] = existing
|
||||
end
|
||||
else
|
||||
-- Merge: fill missing fields; also overwrite empty-string names
|
||||
for k, v in pairs(existing) do
|
||||
if not IsAscensionProtected("NPC", npcId, k) and (ovr[k] == nil or (k == 1 and ovr[k] == "")) then ovr[k] = v end
|
||||
end
|
||||
-- Always merge spawn coords
|
||||
if existing[7] and not IsAscensionProtected("NPC", npcId, 7) then
|
||||
ovr[7] = ovr[7] or {}
|
||||
for zid, coords in pairs(existing[7]) do
|
||||
ovr[7][zid] = ovr[7][zid] or {}
|
||||
for _, coord in ipairs(coords) do
|
||||
InsertIfNewBucket(ovr[7][zid], coord[1], coord[2], GetCoordGridForZone(zid))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Clear the compiled DB cache so GetNPC rebuilds with the new override data.
|
||||
if QuestieDB.private and QuestieDB.private.npcCache then
|
||||
QuestieDB.private.npcCache[npcId] = nil
|
||||
end
|
||||
end
|
||||
-- Live injection is intentionally batched: repeated kill/log/loot events for
|
||||
-- the same NPC update saved evidence immediately, then flush QuestieDB once.
|
||||
_QueueNpcLiveUpdate(npcId)
|
||||
|
||||
if isNew then
|
||||
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] New NPC learned:", npcId, name or "?")
|
||||
CrossLinkAfterNPC(npcId)
|
||||
end
|
||||
-- Existing NPC got new spawn data: invalidate cached spawnLists
|
||||
-- for any active quest objective that references this NPC so the
|
||||
-- map system rebuilds them with fresh data on next update.
|
||||
_InvalidateSpawnListsForNPC(npcId)
|
||||
_Learner:BroadcastIfCommsAvailable("NPC", npcId, existing)
|
||||
end
|
||||
|
||||
@@ -1196,9 +1255,7 @@ local function _MergeSpawnEvidence(npcId)
|
||||
"duplicates", duplicates,
|
||||
"zone", tostring(topEvidence.zoneId))
|
||||
|
||||
if QuestieDB.private and QuestieDB.private.npcCache then
|
||||
QuestieDB.private.npcCache[npcId] = nil
|
||||
end
|
||||
_QueueNpcLiveUpdate(npcId)
|
||||
|
||||
return promoted > 0 or duplicates > 0
|
||||
end
|
||||
@@ -1287,10 +1344,8 @@ local function _MergeSpawnEvidence(npcId)
|
||||
"zone " .. topEvidence.zoneId .. " at " .. floor(topPct + 0.5) .. "% confidence",
|
||||
spawned and "SPAM" or "IGNORED_DUPLICATE")
|
||||
|
||||
-- Clear npcCache so GetNPC returns fresh data
|
||||
if QuestieDB.private and QuestieDB.private.npcCache then
|
||||
QuestieDB.private.npcCache[npcId] = nil
|
||||
end
|
||||
-- Clear/rebuild cache through the same coalesced path used by kill learning.
|
||||
_QueueNpcLiveUpdate(npcId)
|
||||
|
||||
return true
|
||||
end
|
||||
@@ -3568,9 +3623,44 @@ end
|
||||
|
||||
function _Learner:BroadcastIfCommsAvailable(typ, id, data)
|
||||
local QuestieLearnerComms = QuestieLoader:ImportModule("QuestieLearnerComms")
|
||||
if QuestieLearnerComms and QuestieLearnerComms.BroadcastLearnedData then
|
||||
local op = (data.mc and data.mc > 1) and "UPDATE" or "NEW"
|
||||
QuestieLearnerComms:BroadcastLearnedData(op, typ, id, data)
|
||||
if not (QuestieLearnerComms and QuestieLearnerComms.BroadcastLearnedData) then
|
||||
return
|
||||
end
|
||||
|
||||
_Learner.pendingBroadcasts = _Learner.pendingBroadcasts or {}
|
||||
local key = typ .. ":" .. tostring(id)
|
||||
local op = (data.mc and data.mc > 1) and "UPDATE" or "NEW"
|
||||
local existingPending = _Learner.pendingBroadcasts[key]
|
||||
_Learner.pendingBroadcasts[key] = {
|
||||
typ = typ,
|
||||
id = id,
|
||||
data = data,
|
||||
-- Preserve the first-discovery signal while still sending the latest
|
||||
-- coalesced payload for the entity.
|
||||
op = (existingPending and existingPending.op == "NEW") and "NEW" or op,
|
||||
}
|
||||
|
||||
if _Learner.pendingBroadcastTimer then return end
|
||||
|
||||
local timer = QuestieCompat and QuestieCompat.C_Timer
|
||||
local function FlushBroadcasts()
|
||||
local pending = _Learner.pendingBroadcasts
|
||||
_Learner.pendingBroadcasts = {}
|
||||
_Learner.pendingBroadcastTimer = nil
|
||||
|
||||
local comms = QuestieLoader:ImportModule("QuestieLearnerComms")
|
||||
if not (comms and comms.BroadcastLearnedData) then return end
|
||||
|
||||
for _, entry in pairs(pending) do
|
||||
comms:BroadcastLearnedData(entry.op, entry.typ, entry.id, entry.data)
|
||||
end
|
||||
end
|
||||
|
||||
if timer and timer.After then
|
||||
_Learner.pendingBroadcastTimer = true
|
||||
timer.After(2, FlushBroadcasts)
|
||||
else
|
||||
FlushBroadcasts()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ end
|
||||
local function GetServerKey()
|
||||
if QuestieServer then
|
||||
if Questie.IsAscension then return "Ascension" end
|
||||
if Questie.IsTurtle then return "Turtle" end
|
||||
if Questie.IsEbonhold then return "Ebonhold" end
|
||||
if Questie.IsEra then return "Era" end
|
||||
if Questie.Is335 then return "WotLK" end
|
||||
|
||||
@@ -17,11 +17,6 @@ Questie.IsWotlk = (WOW_PROJECT_ID == WOW_PROJECT_WRATH_CLASSIC)
|
||||
Questie.IsTBC = (WOW_PROJECT_ID == WOW_PROJECT_BURNING_CRUSADE_CLASSIC)
|
||||
Questie.IsClassicEra = (WOW_PROJECT_ID == WOW_PROJECT_CLASSIC)
|
||||
|
||||
-- Try GetBuildInfo for Turtle WoW (Interface: 11200, no WOW_PROJECT globals)
|
||||
local _, _, _, tocVersion = GetBuildInfo()
|
||||
Questie.IsTurtle = (not Questie.IsRetail and not Questie.IsWotlk and not Questie.IsTBC and
|
||||
not Questie.IsClassicEra and tocVersion and tocVersion < 20000)
|
||||
|
||||
-- Custom server detection
|
||||
Questie.IsEbonhold = false
|
||||
Questie.IsAscension = false
|
||||
@@ -41,7 +36,6 @@ local function GetExpectedPluginFlavor()
|
||||
if Questie.IsAscension then return "AscensionDB", "Questie-X-AscensionDB" end
|
||||
if Questie.IsEbonhold then return "EbonholdDB", "Questie-X-EbonholdDB" end
|
||||
if Questie.IsValanior then return "ValaniorDB", "Questie-X-ValaniorDB" end
|
||||
if Questie.IsTurtle then return "TurtleDB", "Questie-X-TurtleDB" end
|
||||
if Questie.IsWotlk then return "WotLKDB", "Questie-X-WotLKDB" end
|
||||
if Questie.IsTBC then return "TBCDB", "Questie-X-TBCDB" end
|
||||
if Questie.IsClassicEra then return "ClassicDB", "Questie-X-ClassicDB" end
|
||||
@@ -53,7 +47,7 @@ function QuestieServer:Init()
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieServer] Realm:", realmName)
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieServer] WOW_PROJECT_ID:", tostring(WOW_PROJECT_ID))
|
||||
Questie:Debug(Questie.DEBUG_INFO, "[QuestieServer] IsWotlk:", tostring(Questie.IsWotlk), "IsTBC:", tostring(Questie.IsTBC),
|
||||
"IsClassicEra:", tostring(Questie.IsClassicEra), "IsTurtle:", tostring(Questie.IsTurtle),
|
||||
"IsClassicEra:", tostring(Questie.IsClassicEra),
|
||||
"IsAscension:", tostring(Questie.IsAscension), "IsEbonhold:", tostring(Questie.IsEbonhold))
|
||||
end
|
||||
|
||||
@@ -64,7 +58,6 @@ function QuestieServer:IsAnyDBPluginEnabled()
|
||||
"Questie-X-WotLKDB",
|
||||
"Questie-X-ClassicDB",
|
||||
"Questie-X-TBCDB",
|
||||
"Questie-X-TurtleDB",
|
||||
"Questie-X-AscensionDB",
|
||||
"Questie-X-EbonholdDB",
|
||||
"Questie-X-ValaniorDB",
|
||||
|
||||
Reference in New Issue
Block a user