diff --git a/Modules/Network/QuestieLearnerComms.lua b/Modules/Network/QuestieLearnerComms.lua index 58412c9..4804ab0 100644 --- a/Modules/Network/QuestieLearnerComms.lua +++ b/Modules/Network/QuestieLearnerComms.lua @@ -141,6 +141,26 @@ local function IsDuplicateMessage(serializedData) return false end +local function GetLearnerSettings() + if Questie and Questie.dbLearner and Questie.dbLearner.global and Questie.dbLearner.global.settings then + return Questie.dbLearner.global.settings + end + return {} +end + +local function GetCommsTuning() + local intensity = GetLearnerSettings().learnerCommsIntensity or "normal" + if intensity == "off" then + return false, 0, 999999, 0, 0 + elseif intensity == "low" then + return true, 4, 6.0, 2, 1 + elseif intensity == "fast" then + return true, 15, 1.5, 10, 4 + end + + return true, 9, 3.5, 6, 2 +end + function QuestieLearnerComms:Initialize() DebugLog("DEVELOP", "Initializing QuestieLearnerComms") @@ -195,6 +215,8 @@ function _QuestieLearnerComms:ProcessReinforcement() end function QuestieLearnerComms:BroadcastLearnedData(op, entityType, entityId, data) + local commsEnabled = GetCommsTuning() + if not commsEnabled then return end if not data or type(data) ~= "table" then return end -- 1. Create Payload (sanitize data to remove functions before serialization) @@ -226,11 +248,20 @@ function QuestieLearnerComms:BroadcastLearnedData(op, entityType, entityId, data end function _QuestieLearnerComms:QueueMessage(encodedMessage) + local commsEnabled = GetCommsTuning() + if not commsEnabled then return end rateLimitQueueTail = rateLimitQueueTail + 1 rateLimitQueue[rateLimitQueueTail] = encodedMessage end function _QuestieLearnerComms:ProcessQueues() + local commsEnabled, tunedBucketCapacity, tunedMinChatInterval, normalIncomingCount, combatIncomingCount = GetCommsTuning() + bucketCapacity = tunedBucketCapacity + tokenRefillRate = bucketCapacity / bucketWindow + minChatInterval = tunedMinChatInterval + currentTokens = math_min(bucketCapacity, currentTokens) + if not commsEnabled then return end + -- 1. Refill Tokens local now = GetTime() local elapsed = now - lastTokenUpdate @@ -260,7 +291,7 @@ function _QuestieLearnerComms:ProcessQueues() end -- 3. Process Incoming Queue (Combat Aware) - local processCount = InCombatLockdown() and 2 or 6 + local processCount = InCombatLockdown() and combatIncomingCount or normalIncomingCount for i = 1, processCount do if incomingMessageQueueHead > incomingMessageQueueTail then break end local rawMsg = incomingMessageQueue[incomingMessageQueueHead] @@ -278,6 +309,8 @@ end local frame = CreateFrame("Frame") frame:RegisterEvent("CHAT_MSG_CHANNEL") frame:SetScript("OnEvent", function(self, event, msg, sender, _, _, _, _, _, channelId, channelName) + local commsEnabled = GetCommsTuning() + if not commsEnabled then return end if channelName == hiddenChannelName and sender ~= UnitName("player") then incomingMessageQueueTail = incomingMessageQueueTail + 1 incomingMessageQueue[incomingMessageQueueTail] = {text = msg, sender = sender} @@ -285,6 +318,8 @@ frame:SetScript("OnEvent", function(self, event, msg, sender, _, _, _, _, _, cha end) function QuestieLearnerComms:OnCommReceived(prefix, message, distribution, sender) + local commsEnabled = GetCommsTuning() + if not commsEnabled then return end if prefix == addonPrefix and sender ~= UnitName("player") then incomingMessageQueueTail = incomingMessageQueueTail + 1 incomingMessageQueue[incomingMessageQueueTail] = {text = message, sender = sender} diff --git a/Modules/Options/AdvancedTab/QuestieOptionsAdvanced.lua b/Modules/Options/AdvancedTab/QuestieOptionsAdvanced.lua index a62e10d..c655f50 100644 --- a/Modules/Options/AdvancedTab/QuestieOptionsAdvanced.lua +++ b/Modules/Options/AdvancedTab/QuestieOptionsAdvanced.lua @@ -24,6 +24,57 @@ QuestieOptions.tabs.advanced = {} local optionsDefaults = QuestieOptionsDefaults:Load() local _GetLanguages +local function GetLearnerSettings() + Questie.dbLearner = Questie.dbLearner or {} + Questie.dbLearner.global = Questie.dbLearner.global or {} + Questie.dbLearner.global.settings = Questie.dbLearner.global.settings or {} + local settings = Questie.dbLearner.global.settings + if settings.performanceMode == nil then + settings.performanceMode = "balanced" + end + if settings.pinRefreshDelay == nil then + settings.pinRefreshDelay = 0.5 + end + if settings.pinRefreshMode == nil then + settings.pinRefreshMode = "batched" + end + if settings.liveNpcUpdateDelay == nil then + settings.liveNpcUpdateDelay = 0.5 + end + if settings.learnerCommsIntensity == nil then + settings.learnerCommsIntensity = "normal" + end + if settings.minConfidencePins == nil then + settings.minConfidencePins = 1 + end + return settings +end + +local function ApplyLearnerPerformancePreset(mode) + local settings = GetLearnerSettings() + settings.performanceMode = mode + + if mode == "realtime" then + settings.pinRefreshDelay = 0.1 + settings.pinRefreshMode = "immediate" + settings.liveNpcUpdateDelay = 0.25 + settings.learnerCommsIntensity = "fast" + settings.minConfidencePins = 1 + elseif mode == "low" then + settings.pinRefreshDelay = 2.0 + settings.pinRefreshMode = "batched" + settings.liveNpcUpdateDelay = 2.0 + settings.learnerCommsIntensity = "low" + settings.minConfidencePins = 3 + elseif mode == "balanced" then + settings.pinRefreshDelay = 0.5 + settings.pinRefreshMode = "batched" + settings.liveNpcUpdateDelay = 0.5 + settings.learnerCommsIntensity = "normal" + settings.minConfidencePins = 1 + end +end + function QuestieOptions.tabs.advanced:Initialize() -- This needs to be called inside of the Init process for l10n to be fully loaded StaticPopupDialogs["QUESTIE_LANG_CHANGED_RELOAD"] = { @@ -157,6 +208,120 @@ function QuestieOptions.tabs.advanced:Initialize() }, }, + learnerPerformanceSpacer = QuestieOptionsUtils:Spacer(1.9), + learnerPerformanceHeader = { + type = "header", + order = 2, + name = function() return l10n('QuestieLearner Performance'); end, + }, + learnerPerformanceMode = { + type = "select", + order = 2.1, + values = { + realtime = l10n("Realtime"), + balanced = l10n("Balanced"), + low = l10n("Low Impact"), + manual = l10n("Manual"), + }, + style = "dropdown", + name = function() return l10n('Performance Mode'); end, + desc = function() return l10n('Controls how aggressively QuestieLearner updates learned pins, live data, and learner comms. Low Impact is recommended for heavy activity zones or low-end computers.'); end, + get = function() return GetLearnerSettings().performanceMode or "balanced" end, + set = function(_, value) + ApplyLearnerPerformancePreset(value) + end, + }, + learnerPinRefreshMode = { + type = "select", + order = 2.2, + values = { + immediate = l10n("Immediate"), + batched = l10n("Batched"), + manual = l10n("Manual / Reload"), + }, + style = "dropdown", + name = function() return l10n('Pin Refresh Behavior'); end, + desc = function() return l10n('Controls when learned pins refresh after QuestieLearner records new data. Manual / Reload records data but avoids live pin redraws until reload or another Questie refresh.'); end, + get = function() return GetLearnerSettings().pinRefreshMode or "batched" end, + set = function(_, value) + local settings = GetLearnerSettings() + settings.pinRefreshMode = value + settings.performanceMode = "manual" + end, + }, + learnerPinRefreshDelay = { + type = "range", + order = 2.3, + name = function() return l10n('Pin Refresh Delay'); end, + desc = function() return l10n('Seconds to wait before refreshing learned quest pins after learner activity. Higher values reduce stutter during kill or loot bursts.'); end, + min = 0.1, + max = 5, + step = 0.1, + width = 1.5, + get = function() return GetLearnerSettings().pinRefreshDelay or 0.5 end, + set = function(_, value) + local settings = GetLearnerSettings() + settings.pinRefreshDelay = value + settings.performanceMode = "manual" + end, + }, + learnerLiveNpcUpdateDelay = { + type = "range", + order = 2.4, + name = function() return l10n('Live NPC Update Delay'); end, + desc = function() return l10n('Seconds to batch learned NPC live database updates. Higher values reduce work during combat and crowded zones.'); end, + min = 0.25, + max = 5, + step = 0.25, + width = 1.5, + get = function() return GetLearnerSettings().liveNpcUpdateDelay or 0.5 end, + set = function(_, value) + local settings = GetLearnerSettings() + settings.liveNpcUpdateDelay = value + settings.performanceMode = "manual" + end, + }, + learnerMinConfidencePins = { + type = "range", + order = 2.5, + name = function() return l10n('Minimum Kills Before Learned Pins'); end, + desc = function() return l10n('How many matching NPC sightings are needed before QuestieLearner shows learned pins. Higher values reduce one-off pin churn.'); end, + min = 1, + max = 10, + step = 1, + width = 1.5, + get = function() return GetLearnerSettings().minConfidencePins or 1 end, + set = function(_, value) + local settings = GetLearnerSettings() + settings.minConfidencePins = value + settings.performanceMode = "manual" + end, + }, + learnerCommsIntensity = { + type = "select", + order = 2.6, + values = { + off = l10n("Off"), + low = l10n("Low"), + normal = l10n("Normal"), + fast = l10n("Fast"), + }, + style = "dropdown", + name = function() return l10n('Learner Comms Intensity'); end, + desc = function() return l10n('Controls how much learner data Questie processes and sends through learner comms. Lower values reduce CPU and chat-channel work.'); end, + get = function() return GetLearnerSettings().learnerCommsIntensity or "normal" end, + set = function(_, value) + local settings = GetLearnerSettings() + settings.learnerCommsIntensity = value + settings.performanceMode = "manual" + if value == "off" then + Questie.db.profile.learnerBroadcast = false + elseif Questie.db.profile.learnerBroadcast == false then + Questie.db.profile.learnerBroadcast = true + end + end, + }, + Spacer_A = QuestieOptionsUtils:Spacer(2.9), locale_header = { type = "header", diff --git a/Modules/Options/QuestieOptionsDefaults.lua b/Modules/Options/QuestieOptionsDefaults.lua index f206e88..b900e22 100644 --- a/Modules/Options/QuestieOptionsDefaults.lua +++ b/Modules/Options/QuestieOptionsDefaults.lua @@ -77,6 +77,7 @@ function QuestieOptionsDefaults:Load() enableTooltipsQuestLevel = true, showQuestXpAtMaxLevel = true, enableTooltipsNextInChain = true, + learnerBroadcast = true, enableMapIcons = true, enableMiniMapIcons = true, questieShutUp = false, diff --git a/Modules/QuestieLearner.lua b/Modules/QuestieLearner.lua index 7305fcc..73c9149 100644 --- a/Modules/QuestieLearner.lua +++ b/Modules/QuestieLearner.lua @@ -321,6 +321,11 @@ local function EnsureLearnedData() prioritizeMyData = true, staleThreshold = 90, -- days pruneVerified = false, -- protect verified data by default + performanceMode = "balanced", + pinRefreshDelay = 0.5, + pinRefreshMode = "batched", + liveNpcUpdateDelay = 0.5, + learnerCommsIntensity = "normal", } else -- Backfill sub-tables that may be missing from older SavedVariables @@ -337,6 +342,27 @@ local function EnsureLearnedData() if s.learnObjects == nil then s.learnObjects = true end if s.minConfidencePins == nil then s.minConfidencePins = 1 end if s.prioritizeMyData == nil then s.prioritizeMyData = true end + if s.staleThreshold == nil then + s.staleThreshold = 90 + end + if s.pruneVerified == nil then + s.pruneVerified = false + end + if s.performanceMode == nil then + s.performanceMode = "balanced" + end + if s.pinRefreshDelay == nil then + s.pinRefreshDelay = 0.5 + end + if s.pinRefreshMode == nil then + s.pinRefreshMode = "batched" + end + if s.liveNpcUpdateDelay == nil then + s.liveNpcUpdateDelay = 0.5 + end + if s.learnerCommsIntensity == nil then + s.learnerCommsIntensity = "normal" + end end return true end @@ -355,6 +381,16 @@ function QuestieLearner:GetSettings() return Questie.dbLearner.global.settings end +local function GetLearnerSetting(key, defaultValue) + if Questie and Questie.dbLearner and Questie.dbLearner.global and Questie.dbLearner.global.settings then + local value = Questie.dbLearner.global.settings[key] + if value ~= nil then + return value + end + end + return defaultValue +end + ------------------------------------------------------------------------ -- Cross-link engine -- After ANY entity is learned, scan all other learned data and stitch @@ -427,6 +463,7 @@ local function _GetDB() return Questie.dbLearner.global end -- Called after cross-linking so map pins refresh immediately. local _pendingQuestPinRefreshes = {} local _pendingQuestPinRefreshTimer = nil +local _pendingQuestFrameUnloads = {} local function _FlushActiveQuestPins() local questIdSet = _pendingQuestPinRefreshes @@ -439,14 +476,24 @@ local function _FlushActiveQuestPins() for questId in pairs(questIdSet) do if QuestiePlayer.currentQuestlog[questId] then + if QuestieMap and QuestieMap.UnloadQuestFrames and _pendingQuestFrameUnloads[questId] then + QuestieMap:UnloadQuestFrames(questId) + end QuestieQuest:UpdateQuest(questId) end end + _pendingQuestFrameUnloads = {} end local function _RefreshActiveQuestPins(questIdSet) -- Skip scheduling when the set is empty (avoids no-op timer callbacks) if not next(questIdSet) then return end + if GetLearnerSetting("pinRefreshMode", "batched") == "manual" then + for questId in pairs(questIdSet) do + _pendingQuestFrameUnloads[questId] = nil + end + return + end for questId in pairs(questIdSet) do _pendingQuestPinRefreshes[questId] = true end @@ -458,7 +505,7 @@ local function _RefreshActiveQuestPins(questIdSet) local timer = (C_Timer) or (QuestieCompat and QuestieCompat.C_Timer) if timer and timer.After then _pendingQuestPinRefreshTimer = true - timer.After(0.15, _FlushActiveQuestPins) + timer.After(GetLearnerSetting("pinRefreshDelay", 0.5), _FlushActiveQuestPins) else _FlushActiveQuestPins() end @@ -577,8 +624,10 @@ local function _InvalidateSpawnListsForNPC(npcId) and (now - _invalidateDebounce[debounceKey]) < debounceWindow if not suppressUnload then _invalidateDebounce[debounceKey] = now - if QuestieMap and QuestieMap.UnloadQuestFrames then + if GetLearnerSetting("pinRefreshMode", "batched") == "immediate" and QuestieMap and QuestieMap.UnloadQuestFrames then QuestieMap:UnloadQuestFrames(questId) + else + _pendingQuestFrameUnloads[questId] = true end end questsToRefresh[questId] = true -- always refresh when data changed @@ -607,7 +656,6 @@ end -- 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 @@ -676,7 +724,7 @@ local function _QueueNpcLiveUpdate(npcId) local timer = QuestieCompat and QuestieCompat.C_Timer if timer and timer.After then _Learner.pendingNpcLiveUpdateTimer = true - timer.After(LIVE_NPC_UPDATE_DELAY, _FlushNpcLiveUpdates) + timer.After(GetLearnerSetting("liveNpcUpdateDelay", 0.5), _FlushNpcLiveUpdates) else _FlushNpcLiveUpdates() end @@ -3684,6 +3732,13 @@ local function _ValidateLearnedSpawnData(data) end function _Learner:BroadcastIfCommsAvailable(typ, id, data) + if Questie and Questie.db and Questie.db.profile and Questie.db.profile.learnerBroadcast == false then + return + end + if GetLearnerSetting("learnerCommsIntensity", "normal") == "off" then + return + end + local QuestieLearnerComms = QuestieLoader:ImportModule("QuestieLearnerComms") if not (QuestieLearnerComms and QuestieLearnerComms.BroadcastLearnedData) then return @@ -3720,14 +3775,19 @@ function _Learner:BroadcastIfCommsAvailable(typ, id, data) if timer and timer.After then _Learner.pendingBroadcastTimer = true - timer.After(2, FlushBroadcasts) + local delay = 2 + local intensity = GetLearnerSetting("learnerCommsIntensity", "normal") + if intensity == "low" then + delay = 4 + elseif intensity == "fast" then + delay = 1 + end + timer.After(delay, FlushBroadcasts) else FlushBroadcasts() end end -local NETWORK_MERGE_DELAY = 0.5 - local function _QueueIncomingNetworkMerge(typ, id, data, op) _Learner.pendingNetworkMerges = _Learner.pendingNetworkMerges or {} local key = typ .. ":" .. tostring(id) @@ -3760,7 +3820,7 @@ local function _QueueIncomingNetworkMerge(typ, id, data, op) if timer and timer.After then _Learner.pendingNetworkMergeTimer = true - timer.After(NETWORK_MERGE_DELAY, FlushNetworkMerges) + timer.After(GetLearnerSetting("liveNpcUpdateDelay", 0.5), FlushNetworkMerges) else FlushNetworkMerges() end