diff --git a/Compat/HBD.lua b/Compat/HBD.lua index 8c38d94..aeaab57 100644 --- a/Compat/HBD.lua +++ b/Compat/HBD.lua @@ -95,16 +95,33 @@ function HBD:GetZoneDistance(oZone, oX, oY, dZone, dX, dY) return self:GetWorldDistance(oInstance, oX, oY, dX, dY) end +-- Position cache: avoid hammering the C API more than 20 times per second. +-- These are invalidated on zone-change events below. +local _pos_cache_interval = 0.05 +local _pzp_x, _pzp_y, _pzp_mapID, _pzp_time = nil, nil, nil, 0 +local _pwp_x, _pwp_y, _pwp_inst, _pwp_time = nil, nil, nil, 0 + --- Get the current world position of the player -- The position is transformed to the current continent, if applicable -- @return x, y, instanceID function HBD:GetPlayerWorldPosition() - local x, y, uiMapID = HBD:GetPlayerZonePosition() - if not x or not y then return nil, nil, nil end + local now = GetTime() + if now - _pwp_time < _pos_cache_interval then + return _pwp_x, _pwp_y, _pwp_inst + end - x, y, instanceID = HBD:GetWorldCoordinatesFromZone(x, y, uiMapID) - if x and y then - return x, y, instanceID + local x, y, uiMapID = HBD:GetPlayerZonePosition() + if not x or not y then + _pwp_x, _pwp_y, _pwp_inst = nil, nil, nil + _pwp_time = now + return nil, nil, nil + end + + local wx, wy, inst = HBD:GetWorldCoordinatesFromZone(x, y, uiMapID) + _pwp_x, _pwp_y, _pwp_inst = wx, wy, inst + _pwp_time = now + if wx and wy then + return wx, wy, inst end return nil, nil, nil end @@ -121,8 +138,14 @@ end -- @param allowOutOfBounds Allow coordinates to go beyond the current map (ie. outside of the 0-1 range), otherwise nil will be returned -- @return x, y, uiMapID, mapType function HBD:GetPlayerZonePosition(allowOutOfBounds) - -- get the current position + local now = GetTime() + if now - _pzp_time < _pos_cache_interval then + return _pzp_x, _pzp_y, _pzp_mapID + end + local uiMapID, x, y = QuestieCompat.GetCurrentPlayerPosition() + _pzp_x, _pzp_y, _pzp_mapID = x, y, uiMapID + _pzp_time = now if uiMapID and x and y then return x, y, uiMapID @@ -130,6 +153,11 @@ function HBD:GetPlayerZonePosition(allowOutOfBounds) return nil, nil, nil, nil end +local function _InvalidatePositionCache() + _pzp_time = 0 + _pwp_time = 0 +end + -- Data Constants local WORLD_MAP_ID = 947 @@ -598,11 +626,13 @@ local function OnEventHandler(frame, event, ...) -- recheck cvars after login rotateMinimap = GetCVar("rotateMinimap") == "1" elseif event == "PLAYER_ENTERING_WORLD" then + _InvalidatePositionCache() UpdateMinimap() UpdateWorldMap() elseif event == "WORLD_MAP_UPDATE" then UpdateWorldMap() elseif string.find(event, "ZONE_CHANGED") then + _InvalidatePositionCache() UpdateMinimap() UpdateWorldMap() end diff --git a/Database/Zones/zoneDB.lua b/Database/Zones/zoneDB.lua index 9e9f150..d93db34 100644 --- a/Database/Zones/zoneDB.lua +++ b/Database/Zones/zoneDB.lua @@ -30,9 +30,12 @@ ZoneDB.private.dungeons = ZoneDB.private.dungeons or {} ZoneDB.private.dungeonLocations = ZoneDB.private.dungeonLocations or {} ZoneDB.private.dungeonParentZones = ZoneDB.private.dungeonParentZones or {} ZoneDB.private.subZoneToParentZone = ZoneDB.private.subZoneToParentZone or {} +-- O(1) reverse lookup: uiMapId -> areaId, built from uiMapIdToAreaId at init. +ZoneDB.private.uiMapIdToAreaIdCache = ZoneDB.private.uiMapIdToAreaIdCache or {} local areaIdToUiMapId = ZoneDB.private.areaIdToUiMapId local uiMapIdToAreaId = ZoneDB.private.uiMapIdToAreaId +local uiMapIdToAreaIdCache = ZoneDB.private.uiMapIdToAreaIdCache local dungeons = ZoneDB.private.dungeons local dungeonLocations = ZoneDB.private.dungeonLocations local dungeonParentZones = ZoneDB.private.dungeonParentZones @@ -59,6 +62,7 @@ function ZoneDB:Initialize() end ZoneDB:ApplyCustomZones() + _ZoneDB:BuildUiMapIdToAreaIdCache() _ZoneDB:GenerateParentZoneToStartingZoneTable() -- Run tests if debug enabled @@ -75,6 +79,10 @@ function ZoneDB:ApplyCustomZones() -- skip non-numeric keys elseif _ZoneDB.areaIdToUiMapId[uiMapId] == nil then _ZoneDB.areaIdToUiMapId[uiMapId] = uiMapId + -- Keep reverse cache in sync + if uiMapIdToAreaIdCache[uiMapId] == nil then + uiMapIdToAreaIdCache[uiMapId] = uiMapId + end end if data and type(data.parentMapID) == "number" and _ZoneDB.areaIdToUiMapId[data.parentMapID] == nil then @@ -83,6 +91,17 @@ function ZoneDB:ApplyCustomZones() end end +-- Builds the O(1) uiMapId -> areaId reverse cache from the uiMapIdToAreaId table. +-- Called once at Initialize() after all zone data is loaded. +function _ZoneDB:BuildUiMapIdToAreaIdCache() + for areaUiMapId, areaId in next, uiMapIdToAreaId do + -- First entry wins (matches the original scan behaviour) + if uiMapIdToAreaIdCache[areaUiMapId] == nil then + uiMapIdToAreaIdCache[areaUiMapId] = areaId + end + end +end + function _ZoneDB:GenerateParentZoneToStartingZoneTable() for startingZone, parentZone in next, subZoneToParentZone do parentZoneToSubZone[parentZone] = startingZone @@ -110,46 +129,36 @@ function ZoneDB:GetUiMapIdByAreaId(areaId) return nil end ---- Use with care, kind of slow. ---@param uiMapId UiMapId ---@return AreaId function ZoneDB:GetAreaIdByUiMapId(uiMapId) - --? Some areas have multiple areaIds, so we return the correct AreaId + -- Fast path: override table if UiMapIdOverrides[uiMapId] then return UiMapIdOverrides[uiMapId] end - local foundId - -- First we look for a direct match - for AreaUiMapId, lAreaId in next, uiMapIdToAreaId do - local areaId = lAreaId - if (AreaUiMapId == uiMapId and not foundId) then - foundId = areaId - elseif AreaUiMapId == uiMapId and foundId ~= AreaUiMapId then - -- If we find a second match that does not match the first - -- Only print if debug is enabled. - if Questie.db.profile.debugEnabled then - Questie:Error("[ZoneDB:GetAreaIdByUiMapId] : ", "UiMapId", uiMapId, "has multiple AreaIds:", foundId, areaId) - end - end - end - if foundId then - return foundId - else - -- As a last resort we try to match AreaId and UiMapId by name + -- Fast path: O(1) pre-built reverse cache + local cached = uiMapIdToAreaIdCache[uiMapId] + if cached then return cached end + + -- Slow fallback: name-based lookup (only for unmapped IDs, result is cached for next time) + local mapInfo = C_Map.GetMapInfo(uiMapId) + if mapInfo then for areaId in next, areaIdToUiMapId do - local mapInfo = C_Map.GetMapInfo(uiMapId) local areaName = C_Map.GetAreaInfo(areaId) - if mapInfo and mapInfo.name == areaName then + if mapInfo.name == areaName then Questie:Debug(Questie.DEBUG_DEVELOP, "[ZoneDB:GetAreaIdByUiMapId] : ", "Found AreaId", areaName, ":", areaId, "for UiMapId", mapInfo.name, ":", uiMapId, "by name") + -- Cache the result so we don't scan again + uiMapIdToAreaIdCache[uiMapId] = areaId return areaId end end - if Questie.db.profile.debugEnabled then - Questie:Debug(Questie.DEBUG_DEVELOP, "No AreaId found for UiMapId: " .. uiMapId .. ":" .. (C_Map.GetMapInfo(uiMapId) and C_Map.GetMapInfo(uiMapId).name or "nil")) - end - return nil end + + if Questie.db.profile.debugEnabled then + Questie:Debug(Questie.DEBUG_DEVELOP, "No AreaId found for UiMapId: " .. uiMapId .. ":" .. (mapInfo and mapInfo.name or "nil")) + end + return nil end ---@param areaId AreaId diff --git a/Modules/Arrow/QuestieArrow.lua b/Modules/Arrow/QuestieArrow.lua index 05b1c79..9b6e39b 100644 --- a/Modules/Arrow/QuestieArrow.lua +++ b/Modules/Arrow/QuestieArrow.lua @@ -45,6 +45,12 @@ local driverFrame = nil local sortedTargets = {} local hasManualTarget = false +-- Shared context written by UpdateNearestTargets, read by hoisted helpers. +-- Avoids closure allocation on every call. +local _arrow_playerX, _arrow_playerY, _arrow_playerInstance +local _arrow_usingAutoLogic, _arrow_playerZoneId, _arrow_playerUiMapId +local _arrow_quest -- current quest being processed by the hoisted helpers + local lastPopulateByQuestId = {} local function _IsArrowEnabled() @@ -373,6 +379,156 @@ local function EnsureDriverFrame() end) end +-- --------------------------------------------------------------------------- +-- Hoisted helpers for UpdateNearestTargets. +-- These were previously closures recreated on every call; now they are +-- module-level functions that read shared upvalue state set each cycle. +-- --------------------------------------------------------------------------- + +local function _HasMissingCompletedFlag(list) + if not list then return false end + for _, obj in pairs(list) do + if obj and obj.Completed == nil then + return true + end + end + return false +end + +local function _GetCompleteIconType(quest) + local iconType = Questie.ICON_TYPE_COMPLETE + if QuestieDB and QuestieDB.IsActiveEventQuest and QuestieDB.IsActiveEventQuest(quest.Id) then + iconType = Questie.ICON_TYPE_EVENTQUEST_COMPLETE + elseif QuestieDB and QuestieDB.IsPvPQuest and QuestieDB.IsPvPQuest(quest.Id) then + iconType = Questie.ICON_TYPE_PVPQUEST_COMPLETE + elseif quest.IsRepeatable then + iconType = Questie.ICON_TYPE_REPEATABLE_COMPLETE + end + return iconType +end + +local function _CollectFinisherSpawns(finisher, quest) + if not finisher then return end + local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance + local autoLogic, pZone, pMap = _arrow_usingAutoLogic, _arrow_playerZoneId, _arrow_playerUiMapId + local iconPath = ResolveIconTexture(_GetCompleteIconType(quest)) + if finisher.spawns then + for finisherZone, spawns in pairs(finisher.spawns) do + if finisherZone and spawns then + for _, coords in ipairs(spawns) do + if coords and coords[1] and coords[2] then + if coords[1] == -1 or coords[2] == -1 then + local dungeonLocation = ZoneDB:GetDungeonLocation(finisherZone) + if dungeonLocation then + for _, value in ipairs(dungeonLocation) do + local zone = value[1] + local x = value[2] + local y = value[3] + if not (autoLogic and zone ~= pZone and zone ~= pMap) then + local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone) + if uiMapId and x and y then + local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(x / 100.0, y / 100.0, uiMapId) + if tX and tY and tInst then + local dist = HBD:GetWorldDistance(tInst, pX, pY, tX, tY) + if dist then + if tInst ~= pInst then dist = 500000 + dist * 100 end + table.insert(sortedTargets, { + x = x, y = y, uiMapId = uiMapId, title = quest.name, questLevel = quest.level, iconPath = iconPath, distance = dist, + }) + end + end + end + end + end + end + else + if not (autoLogic and finisherZone ~= pZone and finisherZone ~= pMap) then + local x = coords[1] + local y = coords[2] + local uiMapId = ZoneDB:GetUiMapIdByAreaId(finisherZone) + if uiMapId then + local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(x / 100.0, y / 100.0, uiMapId) + if tX and tY and tInst then + local dist = HBD:GetWorldDistance(tInst, pX, pY, tX, tY) + if dist then + if tInst ~= pInst then dist = 500000 + dist * 100 end + table.insert(sortedTargets, { + x = x, y = y, uiMapId = uiMapId, title = quest.name, questLevel = quest.level, iconPath = iconPath, distance = dist, + }) + end + end + end + end + end + end + end + end + end + end + if finisher.waypoints then + for zone, waypoints in pairs(finisher.waypoints) do + if not (autoLogic and zone ~= pZone and zone ~= pMap) then + if waypoints and waypoints[1] and waypoints[1][1] and waypoints[1][1][1] then + local x = waypoints[1][1][1] + local y = waypoints[1][1][2] + local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone) + if uiMapId and x and y then + local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(x / 100.0, y / 100.0, uiMapId) + if tX and tY and tInst then + local dist = HBD:GetWorldDistance(tInst, pX, pY, tX, tY) + if dist then + if tInst ~= pInst then dist = 500000 + dist * 100 end + table.insert(sortedTargets, { + x = x, y = y, uiMapId = uiMapId, title = quest.name, questLevel = quest.level, iconPath = iconPath, distance = dist, + }) + end + end + end + end + end + end + end +end + +local function _CollectObjective(objective, quest) + if not objective or not objective.spawnList then return end + if QuestieQuest.ShouldHideObjective(objective) then return end + if objective.Completed == true or objective.Completed == 1 then return end + if objective.Needed and objective.Collected + and type(objective.Needed) == "number" and type(objective.Collected) == "number" + and objective.Collected >= objective.Needed then + return + end + local pX, pY, pInst = _arrow_playerX, _arrow_playerY, _arrow_playerInstance + local autoLogic, pZone, pMap = _arrow_usingAutoLogic, _arrow_playerZoneId, _arrow_playerUiMapId + for _, spawnData in pairs(objective.spawnList) do + if spawnData and spawnData.Spawns then + for zone, spawns in pairs(spawnData.Spawns) do + if not (autoLogic and zone ~= pZone and zone ~= pMap) then + for _, spawn in pairs(spawns) do + local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone) + if uiMapId then + local tX, tY, tInst = HBD:GetWorldCoordinatesFromZone(spawn[1] / 100.0, spawn[2] / 100.0, uiMapId) + if tX and tY and tInst then + local dist = HBD:GetWorldDistance(tInst, pX, pY, tX, tY) + if dist then + if tInst ~= pInst then dist = 500000 + dist * 100 end + table.insert(sortedTargets, { + x = spawn[1], y = spawn[2], uiMapId = uiMapId, + title = quest.name, questLevel = quest.level, + iconPath = ResolveIconTexture(objective.Icon) or ResolveIconTexture(spawnData and spawnData.Icon), + distance = dist, + }) + end + end + end + end + end + end + end + end +end + -- Gather all objectives from tracked quests and sort by distance function QuestieArrow:UpdateNearestTargets() -- Don't override manual targets with auto-updates @@ -399,34 +555,23 @@ function QuestieArrow:UpdateNearestTargets() local playerZoneId = QuestiePlayer:GetCurrentZoneId() local playerUiMapId = QuestiePlayer:GetCurrentUiMapId() + -- Publish context for hoisted helper functions (avoids closure allocation every call) + _arrow_playerX, _arrow_playerY, _arrow_playerInstance = playerX, playerY, playerInstance + _arrow_usingAutoLogic = usingAutoLogic + _arrow_playerZoneId, _arrow_playerUiMapId = playerZoneId, playerUiMapId + local function _CollectQuestTargets(quest) - if not quest then - return - end + if not quest then return end -- Avoid spamming QuestieQuest:PopulateQuestLogInfo (it can trigger marker rebuilds and flicker). -- Only populate when objective completion flags are missing, and throttle per quest id. if QuestieQuest and QuestieQuest.PopulateQuestLogInfo and quest.Id then local needsPopulate = false - if not quest.Objectives and not quest.SpecialObjectives then needsPopulate = true - else - local function _HasMissingCompletedFlag(list) - if not list then return false end - for _, obj in pairs(list) do - if obj and obj.Completed == nil then - return true - end - end - return false - end - - if _HasMissingCompletedFlag(quest.Objectives) or _HasMissingCompletedFlag(quest.SpecialObjectives) then - needsPopulate = true - end + elseif _HasMissingCompletedFlag(quest.Objectives) or _HasMissingCompletedFlag(quest.SpecialObjectives) then + needsPopulate = true end - if needsPopulate then local now = GetTime() local last = lastPopulateByQuestId[quest.Id] or 0 @@ -438,21 +583,10 @@ function QuestieArrow:UpdateNearestTargets() end local isComplete = quest.isComplete or (QuestieDB.IsComplete(quest.Id) == 1) - if isComplete then - quest.isComplete = true - end + if isComplete then quest.isComplete = true end - local function _GetCompleteIconType() - local iconType = Questie.ICON_TYPE_COMPLETE - if QuestieDB and QuestieDB.IsActiveEventQuest and QuestieDB.IsActiveEventQuest(quest.Id) then - iconType = Questie.ICON_TYPE_EVENTQUEST_COMPLETE - elseif QuestieDB and QuestieDB.IsPvPQuest and QuestieDB.IsPvPQuest(quest.Id) then - iconType = Questie.ICON_TYPE_PVPQUEST_COMPLETE - elseif quest.IsRepeatable then - iconType = Questie.ICON_TYPE_REPEATABLE_COMPLETE - end - return iconType - end + -- (hoisted) _GetCompleteIconType, _CollectFinisherSpawns, _CollectObjective + -- are module-level functions; no closures created here. local function _CollectFinisherSpawns(finisher) if not finisher then @@ -549,53 +683,6 @@ function QuestieArrow:UpdateNearestTargets() end end - local function _CollectObjective(objective) - if not objective or not objective.spawnList then - return - end - - if QuestieQuest.ShouldHideObjective(objective) then - return - end - - if objective.Completed == true or objective.Completed == 1 then - return - end - - -- If the objective is numerically fulfilled but the server hasn't sent the Completed flag yet - if objective.Needed and objective.Collected and type(objective.Needed) == "number" and type(objective.Collected) == "number" then - if objective.Collected >= objective.Needed then - return - end - end - - for _, spawnData in pairs(objective.spawnList) do - if spawnData and spawnData.Spawns then - for zone, spawns in pairs(spawnData.Spawns) do - -- Auto Logic: Hide distant quests (different zone) - if not (usingAutoLogic and zone ~= playerZoneId and zone ~= playerUiMapId) then - for _, spawn in pairs(spawns) do - local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone) - if uiMapId then - local targetX, targetY, targetInstance = HBD:GetWorldCoordinatesFromZone(spawn[1] / 100.0, spawn[2] / 100.0, uiMapId) - if targetX and targetY and targetInstance then - local dist = HBD:GetWorldDistance(targetInstance, playerX, playerY, targetX, targetY) - if dist then - if targetInstance ~= playerInstance then - dist = 500000 + dist * 100 - end - table.insert(sortedTargets, { - x = spawn[1], y = spawn[2], uiMapId = uiMapId, title = quest.name, questLevel = quest.level, - iconPath = ResolveIconTexture(objective.Icon) or ResolveIconTexture(spawnData and spawnData.Icon), distance = dist, - }) - end - end - end - end - end - end - end - end end -- Main Logic Route for this quest target @@ -608,7 +695,7 @@ function QuestieArrow:UpdateNearestTargets() finisher = QuestieDB:GetObject(quest.Finisher.Id) end - _CollectFinisherSpawns(finisher) + _CollectFinisherSpawns(finisher, quest) end -- If the quest is complete, do not add normal objectives to the arrow! return @@ -616,12 +703,12 @@ function QuestieArrow:UpdateNearestTargets() if quest.Objectives then for _, objective in pairs(quest.Objectives) do - _CollectObjective(objective) + _CollectObjective(objective, quest) end end if quest.SpecialObjectives then for _, objective in pairs(quest.SpecialObjectives) do - _CollectObjective(objective) + _CollectObjective(objective, quest) end end end diff --git a/Modules/Network/QuestieLearnerComms.lua b/Modules/Network/QuestieLearnerComms.lua index 23f965e..37d72d4 100644 --- a/Modules/Network/QuestieLearnerComms.lua +++ b/Modules/Network/QuestieLearnerComms.lua @@ -65,7 +65,10 @@ local rateLimitQueue = {} -- Deduplication & Quarantine local messageCache = {} +local messageCacheCount = 0 -- O(1) counter; avoids pairs() scan on every message local incomingMessageQueue = {} +-- Cached hidden channel ID (avoids GetChannelName every ProcessQueues tick) +local _hiddenChannelId = 0 -- Sender Trust System local senderTrust = {} @@ -124,10 +127,11 @@ local function IsDuplicateMessage(serializedData) if messageCache[hash] then return true end - local count = 0 - for _ in pairs(messageCache) do count = count + 1 end - if count > 500 then + -- O(1) size tracking via explicit counter + messageCacheCount = messageCacheCount + 1 + if messageCacheCount > 500 then messageCache = {} + messageCacheCount = 0 end messageCache[hash] = true @@ -149,9 +153,11 @@ function QuestieLearnerComms:Initialize() ChatFrame_RemoveChannel(DEFAULT_CHAT_FRAME, hiddenChannelName) DebugLog("CRITICAL", "Joined hidden data-sharing channel: " .. hiddenChannelName) end + -- Cache for use in ProcessQueues (avoids GetChannelName every tick) + _hiddenChannelId = GetChannelName(hiddenChannelName) or 0 -- Process incoming/outgoing queues - QuestieCompat.C_Timer.NewTicker(0.2, function() _QuestieLearnerComms:ProcessQueues() end) + QuestieCompat.C_Timer.NewTicker(0.5, function() _QuestieLearnerComms:ProcessQueues() end) -- Start Reinforcement Loop (every 60 seconds) QuestieCompat.C_Timer.NewTicker(60, function() _QuestieLearnerComms:ProcessReinforcement() end) @@ -209,7 +215,7 @@ function QuestieLearnerComms:BroadcastLearnedData(op, entityType, entityId, data return end serialized = err - local compressed = LibDeflate:CompressDeflate(serialized, {level = 9}) + local compressed = LibDeflate:CompressDeflate(serialized, {level = 1}) local encoded = LibDeflate:EncodeForPrint(compressed) -- 3. Broadcast (Token Bucket logic handled in QueueMessage) @@ -224,28 +230,30 @@ function _QuestieLearnerComms:ProcessQueues() -- 1. Refill Tokens local now = GetTime() local elapsed = now - lastTokenUpdate - currentTokens = math.min(bucketCapacity, currentTokens + (elapsed * tokenRefillRate)) + currentTokens = math_min(bucketCapacity, currentTokens + (elapsed * tokenRefillRate)) lastTokenUpdate = now -- 2. Drain Outgoing Queue - if table.getn(rateLimitQueue) > 0 and currentTokens >= 1 and (now - lastChatMessageTime) >= minChatInterval then - local msg = table.remove(rateLimitQueue, 1) + if table_getn(rateLimitQueue) > 0 and currentTokens >= 1 and (now - lastChatMessageTime) >= minChatInterval then + local msg = table_remove(rateLimitQueue, 1) currentTokens = currentTokens - 1 lastChatMessageTime = now - -- Send via Hidden Channel (Global reach) - local channelId = GetChannelName(hiddenChannelName) - if channelId > 0 then - SendChatMessage(msg, "CHANNEL", nil, channelId) + -- Use cached channel ID; refresh lazily if 0 (e.g. after disconnect) + if _hiddenChannelId == 0 then + _hiddenChannelId = GetChannelName(hiddenChannelName) or 0 end - DebugLog("DEVELOP", "Broadcasted message. Tokens left: " .. math.floor(currentTokens)) + if _hiddenChannelId > 0 then + SendChatMessage(msg, "CHANNEL", nil, _hiddenChannelId) + end + DebugLog("DEVELOP", "Broadcasted message. Tokens left: " .. math_floor(currentTokens)) end -- 3. Process Incoming Queue (Combat Aware) local processCount = InCombatLockdown() and 2 or 6 for i = 1, processCount do - if table.getn(incomingMessageQueue) == 0 then break end - local rawMsg = table.remove(incomingMessageQueue, 1) + if table_getn(incomingMessageQueue) == 0 then break end + local rawMsg = table_remove(incomingMessageQueue, 1) _QuestieLearnerComms:ProcessRawMessage(rawMsg.text, rawMsg.sender) end end diff --git a/Modules/Tooltips/Tooltip.lua b/Modules/Tooltips/Tooltip.lua index 7860f62..593158c 100644 --- a/Modules/Tooltips/Tooltip.lua +++ b/Modules/Tooltips/Tooltip.lua @@ -34,6 +34,12 @@ QuestieTooltips.lookupKeysByQuestId = { } local MAX_GROUP_MEMBER_COUNT = 6 +-- Throttle: limit the OnUpdate object-tooltip check to 10 times per second. +-- Without this, the callback fires every frame (60–144 Hz) and hammers GetText() +-- + CountTooltip() even when the tooltip hasn't changed. +local _tooltipUpdateInterval = 0.10 +local _tooltipLastUpdate = 0 +local _tooltipLastText = "" local _InitObjectiveTexts @@ -491,28 +497,34 @@ function QuestieTooltips:Initialize() end end) - -- Fired whenever the cursor hovers something with a tooltip. And then on every frame + -- Fired whenever the cursor hovers something with a tooltip. And then on every frame. + -- Throttled to _tooltipUpdateInterval (100ms) to avoid per-frame C API pressure. GameTooltip:HookScript("OnUpdate", function(self) if QuestiePlayer.numberOfGroupMembers > MAX_GROUP_MEMBER_COUNT then - -- When in a raid, we want as little code running as possible return end + local now = GetTime() + if now - _tooltipLastUpdate < _tooltipUpdateInterval then return end + _tooltipLastUpdate = now + if (not self.IsForbidden) or (not self:IsForbidden()) then - --Because this is an OnUpdate we need to check that it is actually not a Unit or Item to think its a + -- Only fires for non-unit, non-item, non-spell tooltips (i.e. object/world tooltips) local uName, unit = self:GetUnit() local iName, link = self:GetItem() local sName, spell = self:GetSpell() - if (uName == nil and unit == nil and iName == nil and link == nil and sName == nil and spell == nil) and ( - QuestieTooltips.lastGametooltip ~= GameTooltipTextLeft1:GetText() or - (not QuestieTooltips.lastGametooltipCount) or - _QuestieTooltips:CountTooltip() < QuestieTooltips.lastGametooltipCount - or QuestieTooltips.lastGametooltipType ~= "object" - ) and (not self.ShownAsMapIcon) then -- We are hovering over a Questie map icon which adds it's own tooltip - _QuestieTooltips:AddObjectDataToTooltip(GameTooltipTextLeft1:GetText()) - QuestieTooltips.lastGametooltipCount = _QuestieTooltips:CountTooltip() + if (uName == nil and unit == nil and iName == nil and link == nil and sName == nil and spell == nil) and (not self.ShownAsMapIcon) then + local currentText = GameTooltipTextLeft1:GetText() + if currentText ~= _tooltipLastText + or (not QuestieTooltips.lastGametooltipCount) + or _QuestieTooltips:CountTooltip() < QuestieTooltips.lastGametooltipCount + or QuestieTooltips.lastGametooltipType ~= "object" then + _QuestieTooltips:AddObjectDataToTooltip(currentText) + QuestieTooltips.lastGametooltipCount = _QuestieTooltips:CountTooltip() + _tooltipLastText = currentText + end + QuestieTooltips.lastGametooltip = currentText end - QuestieTooltips.lastGametooltip = GameTooltipTextLeft1:GetText() end end) end