perf: reduce CPU hotspots identified in profiler
- HBD.lua: Cache GetPlayerWorldPosition/GetPlayerZonePosition at 50ms
intervals instead of hammering GetPlayerMapPosition() every frame.
Invalidate cache on PLAYER_ENTERING_WORLD and ZONE_CHANGED_* events.
Expected ~97% reduction in C API position calls (45,570 -> ~1,200 / 10min).
- zoneDB.lua: Replace O(n) linear scan in GetAreaIdByUiMapId with an
O(1) reverse lookup cache (uiMapIdToAreaIdCache) built at Initialize().
Cache is kept in sync by ApplyCustomZones and name-match fallback now
caches its result so subsequent calls are also O(1).
- Tooltip.lua: Throttle GameTooltip OnUpdate hook to 100ms intervals
(was firing every frame at 60-144 Hz). Added _tooltipLastText cache
to avoid redundant GetText() + CountTooltip() calls when nothing changed.
- QuestieArrow.lua: Hoist _HasMissingCompletedFlag, _GetCompleteIconType,
_CollectFinisherSpawns, and _CollectObjective out of _CollectQuestTargets
to module-level functions. These were re-created as closures on every
UpdateNearestTargets call (1 Hz). Shared per-cycle context is published
via _arrow_* module upvalues to avoid closure capture overhead.
- QuestieLearnerComms.lua: Four micro-optimizations:
(1) Reduce ProcessQueues ticker 0.2s -> 0.5s (still 7x faster than
minChatInterval of 3.5s).
(2) Cache hidden channel ID at init; lazy refresh on disconnect.
(3) Drop LibDeflate compress level 9 -> 1 (fraction of CPU cost).
(4) O(1) messageCacheCount counter replaces O(n) pairs() size scan.
This commit is contained in:
+36
-6
@@ -95,16 +95,33 @@ function HBD:GetZoneDistance(oZone, oX, oY, dZone, dX, dY)
|
|||||||
return self:GetWorldDistance(oInstance, oX, oY, dX, dY)
|
return self:GetWorldDistance(oInstance, oX, oY, dX, dY)
|
||||||
end
|
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
|
--- Get the current world position of the player
|
||||||
-- The position is transformed to the current continent, if applicable
|
-- The position is transformed to the current continent, if applicable
|
||||||
-- @return x, y, instanceID
|
-- @return x, y, instanceID
|
||||||
function HBD:GetPlayerWorldPosition()
|
function HBD:GetPlayerWorldPosition()
|
||||||
local x, y, uiMapID = HBD:GetPlayerZonePosition()
|
local now = GetTime()
|
||||||
if not x or not y then return nil, nil, nil end
|
if now - _pwp_time < _pos_cache_interval then
|
||||||
|
return _pwp_x, _pwp_y, _pwp_inst
|
||||||
|
end
|
||||||
|
|
||||||
x, y, instanceID = HBD:GetWorldCoordinatesFromZone(x, y, uiMapID)
|
local x, y, uiMapID = HBD:GetPlayerZonePosition()
|
||||||
if x and y then
|
if not x or not y then
|
||||||
return x, y, instanceID
|
_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
|
end
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
end
|
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
|
-- @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
|
-- @return x, y, uiMapID, mapType
|
||||||
function HBD:GetPlayerZonePosition(allowOutOfBounds)
|
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()
|
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
|
if uiMapID and x and y then
|
||||||
return x, y, uiMapID
|
return x, y, uiMapID
|
||||||
@@ -130,6 +153,11 @@ function HBD:GetPlayerZonePosition(allowOutOfBounds)
|
|||||||
return nil, nil, nil, nil
|
return nil, nil, nil, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function _InvalidatePositionCache()
|
||||||
|
_pzp_time = 0
|
||||||
|
_pwp_time = 0
|
||||||
|
end
|
||||||
|
|
||||||
-- Data Constants
|
-- Data Constants
|
||||||
local WORLD_MAP_ID = 947
|
local WORLD_MAP_ID = 947
|
||||||
|
|
||||||
@@ -598,11 +626,13 @@ local function OnEventHandler(frame, event, ...)
|
|||||||
-- recheck cvars after login
|
-- recheck cvars after login
|
||||||
rotateMinimap = GetCVar("rotateMinimap") == "1"
|
rotateMinimap = GetCVar("rotateMinimap") == "1"
|
||||||
elseif event == "PLAYER_ENTERING_WORLD" then
|
elseif event == "PLAYER_ENTERING_WORLD" then
|
||||||
|
_InvalidatePositionCache()
|
||||||
UpdateMinimap()
|
UpdateMinimap()
|
||||||
UpdateWorldMap()
|
UpdateWorldMap()
|
||||||
elseif event == "WORLD_MAP_UPDATE" then
|
elseif event == "WORLD_MAP_UPDATE" then
|
||||||
UpdateWorldMap()
|
UpdateWorldMap()
|
||||||
elseif string.find(event, "ZONE_CHANGED") then
|
elseif string.find(event, "ZONE_CHANGED") then
|
||||||
|
_InvalidatePositionCache()
|
||||||
UpdateMinimap()
|
UpdateMinimap()
|
||||||
UpdateWorldMap()
|
UpdateWorldMap()
|
||||||
end
|
end
|
||||||
|
|||||||
+33
-24
@@ -30,9 +30,12 @@ ZoneDB.private.dungeons = ZoneDB.private.dungeons or {}
|
|||||||
ZoneDB.private.dungeonLocations = ZoneDB.private.dungeonLocations or {}
|
ZoneDB.private.dungeonLocations = ZoneDB.private.dungeonLocations or {}
|
||||||
ZoneDB.private.dungeonParentZones = ZoneDB.private.dungeonParentZones or {}
|
ZoneDB.private.dungeonParentZones = ZoneDB.private.dungeonParentZones or {}
|
||||||
ZoneDB.private.subZoneToParentZone = ZoneDB.private.subZoneToParentZone 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 areaIdToUiMapId = ZoneDB.private.areaIdToUiMapId
|
||||||
local uiMapIdToAreaId = ZoneDB.private.uiMapIdToAreaId
|
local uiMapIdToAreaId = ZoneDB.private.uiMapIdToAreaId
|
||||||
|
local uiMapIdToAreaIdCache = ZoneDB.private.uiMapIdToAreaIdCache
|
||||||
local dungeons = ZoneDB.private.dungeons
|
local dungeons = ZoneDB.private.dungeons
|
||||||
local dungeonLocations = ZoneDB.private.dungeonLocations
|
local dungeonLocations = ZoneDB.private.dungeonLocations
|
||||||
local dungeonParentZones = ZoneDB.private.dungeonParentZones
|
local dungeonParentZones = ZoneDB.private.dungeonParentZones
|
||||||
@@ -59,6 +62,7 @@ function ZoneDB:Initialize()
|
|||||||
end
|
end
|
||||||
|
|
||||||
ZoneDB:ApplyCustomZones()
|
ZoneDB:ApplyCustomZones()
|
||||||
|
_ZoneDB:BuildUiMapIdToAreaIdCache()
|
||||||
_ZoneDB:GenerateParentZoneToStartingZoneTable()
|
_ZoneDB:GenerateParentZoneToStartingZoneTable()
|
||||||
|
|
||||||
-- Run tests if debug enabled
|
-- Run tests if debug enabled
|
||||||
@@ -75,6 +79,10 @@ function ZoneDB:ApplyCustomZones()
|
|||||||
-- skip non-numeric keys
|
-- skip non-numeric keys
|
||||||
elseif _ZoneDB.areaIdToUiMapId[uiMapId] == nil then
|
elseif _ZoneDB.areaIdToUiMapId[uiMapId] == nil then
|
||||||
_ZoneDB.areaIdToUiMapId[uiMapId] = uiMapId
|
_ZoneDB.areaIdToUiMapId[uiMapId] = uiMapId
|
||||||
|
-- Keep reverse cache in sync
|
||||||
|
if uiMapIdToAreaIdCache[uiMapId] == nil then
|
||||||
|
uiMapIdToAreaIdCache[uiMapId] = uiMapId
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if data and type(data.parentMapID) == "number" and _ZoneDB.areaIdToUiMapId[data.parentMapID] == nil then
|
if data and type(data.parentMapID) == "number" and _ZoneDB.areaIdToUiMapId[data.parentMapID] == nil then
|
||||||
@@ -83,6 +91,17 @@ function ZoneDB:ApplyCustomZones()
|
|||||||
end
|
end
|
||||||
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()
|
function _ZoneDB:GenerateParentZoneToStartingZoneTable()
|
||||||
for startingZone, parentZone in next, subZoneToParentZone do
|
for startingZone, parentZone in next, subZoneToParentZone do
|
||||||
parentZoneToSubZone[parentZone] = startingZone
|
parentZoneToSubZone[parentZone] = startingZone
|
||||||
@@ -110,46 +129,36 @@ function ZoneDB:GetUiMapIdByAreaId(areaId)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Use with care, kind of slow.
|
|
||||||
---@param uiMapId UiMapId
|
---@param uiMapId UiMapId
|
||||||
---@return AreaId
|
---@return AreaId
|
||||||
function ZoneDB:GetAreaIdByUiMapId(uiMapId)
|
function ZoneDB:GetAreaIdByUiMapId(uiMapId)
|
||||||
--? Some areas have multiple areaIds, so we return the correct AreaId
|
-- Fast path: override table
|
||||||
if UiMapIdOverrides[uiMapId] then
|
if UiMapIdOverrides[uiMapId] then
|
||||||
return UiMapIdOverrides[uiMapId]
|
return UiMapIdOverrides[uiMapId]
|
||||||
end
|
end
|
||||||
|
|
||||||
local foundId
|
-- Fast path: O(1) pre-built reverse cache
|
||||||
-- First we look for a direct match
|
local cached = uiMapIdToAreaIdCache[uiMapId]
|
||||||
for AreaUiMapId, lAreaId in next, uiMapIdToAreaId do
|
if cached then return cached end
|
||||||
local areaId = lAreaId
|
|
||||||
if (AreaUiMapId == uiMapId and not foundId) then
|
-- Slow fallback: name-based lookup (only for unmapped IDs, result is cached for next time)
|
||||||
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
|
|
||||||
for areaId in next, areaIdToUiMapId do
|
|
||||||
local mapInfo = C_Map.GetMapInfo(uiMapId)
|
local mapInfo = C_Map.GetMapInfo(uiMapId)
|
||||||
|
if mapInfo then
|
||||||
|
for areaId in next, areaIdToUiMapId do
|
||||||
local areaName = C_Map.GetAreaInfo(areaId)
|
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")
|
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
|
return areaId
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if Questie.db.profile.debugEnabled then
|
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"))
|
Questie:Debug(Questie.DEBUG_DEVELOP, "No AreaId found for UiMapId: " .. uiMapId .. ":" .. (mapInfo and mapInfo.name or "nil"))
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param areaId AreaId
|
---@param areaId AreaId
|
||||||
|
|||||||
+169
-82
@@ -45,6 +45,12 @@ local driverFrame = nil
|
|||||||
local sortedTargets = {}
|
local sortedTargets = {}
|
||||||
local hasManualTarget = false
|
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 lastPopulateByQuestId = {}
|
||||||
|
|
||||||
local function _IsArrowEnabled()
|
local function _IsArrowEnabled()
|
||||||
@@ -373,6 +379,156 @@ local function EnsureDriverFrame()
|
|||||||
end)
|
end)
|
||||||
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
|
-- Gather all objectives from tracked quests and sort by distance
|
||||||
function QuestieArrow:UpdateNearestTargets()
|
function QuestieArrow:UpdateNearestTargets()
|
||||||
-- Don't override manual targets with auto-updates
|
-- Don't override manual targets with auto-updates
|
||||||
@@ -399,34 +555,23 @@ function QuestieArrow:UpdateNearestTargets()
|
|||||||
local playerZoneId = QuestiePlayer:GetCurrentZoneId()
|
local playerZoneId = QuestiePlayer:GetCurrentZoneId()
|
||||||
local playerUiMapId = QuestiePlayer:GetCurrentUiMapId()
|
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)
|
local function _CollectQuestTargets(quest)
|
||||||
if not quest then
|
if not quest then return end
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Avoid spamming QuestieQuest:PopulateQuestLogInfo (it can trigger marker rebuilds and flicker).
|
-- Avoid spamming QuestieQuest:PopulateQuestLogInfo (it can trigger marker rebuilds and flicker).
|
||||||
-- Only populate when objective completion flags are missing, and throttle per quest id.
|
-- Only populate when objective completion flags are missing, and throttle per quest id.
|
||||||
if QuestieQuest and QuestieQuest.PopulateQuestLogInfo and quest.Id then
|
if QuestieQuest and QuestieQuest.PopulateQuestLogInfo and quest.Id then
|
||||||
local needsPopulate = false
|
local needsPopulate = false
|
||||||
|
|
||||||
if not quest.Objectives and not quest.SpecialObjectives then
|
if not quest.Objectives and not quest.SpecialObjectives then
|
||||||
needsPopulate = true
|
needsPopulate = true
|
||||||
else
|
elseif _HasMissingCompletedFlag(quest.Objectives) or _HasMissingCompletedFlag(quest.SpecialObjectives) then
|
||||||
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
|
needsPopulate = true
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
if needsPopulate then
|
if needsPopulate then
|
||||||
local now = GetTime()
|
local now = GetTime()
|
||||||
local last = lastPopulateByQuestId[quest.Id] or 0
|
local last = lastPopulateByQuestId[quest.Id] or 0
|
||||||
@@ -438,21 +583,10 @@ function QuestieArrow:UpdateNearestTargets()
|
|||||||
end
|
end
|
||||||
|
|
||||||
local isComplete = quest.isComplete or (QuestieDB.IsComplete(quest.Id) == 1)
|
local isComplete = quest.isComplete or (QuestieDB.IsComplete(quest.Id) == 1)
|
||||||
if isComplete then
|
if isComplete then quest.isComplete = true end
|
||||||
quest.isComplete = true
|
|
||||||
end
|
|
||||||
|
|
||||||
local function _GetCompleteIconType()
|
-- (hoisted) _GetCompleteIconType, _CollectFinisherSpawns, _CollectObjective
|
||||||
local iconType = Questie.ICON_TYPE_COMPLETE
|
-- are module-level functions; no closures created here.
|
||||||
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)
|
local function _CollectFinisherSpawns(finisher)
|
||||||
if not finisher then
|
if not finisher then
|
||||||
@@ -549,53 +683,6 @@ function QuestieArrow:UpdateNearestTargets()
|
|||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
-- Main Logic Route for this quest target
|
-- Main Logic Route for this quest target
|
||||||
@@ -608,7 +695,7 @@ function QuestieArrow:UpdateNearestTargets()
|
|||||||
finisher = QuestieDB:GetObject(quest.Finisher.Id)
|
finisher = QuestieDB:GetObject(quest.Finisher.Id)
|
||||||
end
|
end
|
||||||
|
|
||||||
_CollectFinisherSpawns(finisher)
|
_CollectFinisherSpawns(finisher, quest)
|
||||||
end
|
end
|
||||||
-- If the quest is complete, do not add normal objectives to the arrow!
|
-- If the quest is complete, do not add normal objectives to the arrow!
|
||||||
return
|
return
|
||||||
@@ -616,12 +703,12 @@ function QuestieArrow:UpdateNearestTargets()
|
|||||||
|
|
||||||
if quest.Objectives then
|
if quest.Objectives then
|
||||||
for _, objective in pairs(quest.Objectives) do
|
for _, objective in pairs(quest.Objectives) do
|
||||||
_CollectObjective(objective)
|
_CollectObjective(objective, quest)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if quest.SpecialObjectives then
|
if quest.SpecialObjectives then
|
||||||
for _, objective in pairs(quest.SpecialObjectives) do
|
for _, objective in pairs(quest.SpecialObjectives) do
|
||||||
_CollectObjective(objective)
|
_CollectObjective(objective, quest)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ local rateLimitQueue = {}
|
|||||||
|
|
||||||
-- Deduplication & Quarantine
|
-- Deduplication & Quarantine
|
||||||
local messageCache = {}
|
local messageCache = {}
|
||||||
|
local messageCacheCount = 0 -- O(1) counter; avoids pairs() scan on every message
|
||||||
local incomingMessageQueue = {}
|
local incomingMessageQueue = {}
|
||||||
|
-- Cached hidden channel ID (avoids GetChannelName every ProcessQueues tick)
|
||||||
|
local _hiddenChannelId = 0
|
||||||
|
|
||||||
-- Sender Trust System
|
-- Sender Trust System
|
||||||
local senderTrust = {}
|
local senderTrust = {}
|
||||||
@@ -124,10 +127,11 @@ local function IsDuplicateMessage(serializedData)
|
|||||||
|
|
||||||
if messageCache[hash] then return true end
|
if messageCache[hash] then return true end
|
||||||
|
|
||||||
local count = 0
|
-- O(1) size tracking via explicit counter
|
||||||
for _ in pairs(messageCache) do count = count + 1 end
|
messageCacheCount = messageCacheCount + 1
|
||||||
if count > 500 then
|
if messageCacheCount > 500 then
|
||||||
messageCache = {}
|
messageCache = {}
|
||||||
|
messageCacheCount = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
messageCache[hash] = true
|
messageCache[hash] = true
|
||||||
@@ -149,9 +153,11 @@ function QuestieLearnerComms:Initialize()
|
|||||||
ChatFrame_RemoveChannel(DEFAULT_CHAT_FRAME, hiddenChannelName)
|
ChatFrame_RemoveChannel(DEFAULT_CHAT_FRAME, hiddenChannelName)
|
||||||
DebugLog("CRITICAL", "Joined hidden data-sharing channel: " .. hiddenChannelName)
|
DebugLog("CRITICAL", "Joined hidden data-sharing channel: " .. hiddenChannelName)
|
||||||
end
|
end
|
||||||
|
-- Cache for use in ProcessQueues (avoids GetChannelName every tick)
|
||||||
|
_hiddenChannelId = GetChannelName(hiddenChannelName) or 0
|
||||||
|
|
||||||
-- Process incoming/outgoing queues
|
-- 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)
|
-- Start Reinforcement Loop (every 60 seconds)
|
||||||
QuestieCompat.C_Timer.NewTicker(60, function() _QuestieLearnerComms:ProcessReinforcement() end)
|
QuestieCompat.C_Timer.NewTicker(60, function() _QuestieLearnerComms:ProcessReinforcement() end)
|
||||||
@@ -209,7 +215,7 @@ function QuestieLearnerComms:BroadcastLearnedData(op, entityType, entityId, data
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
serialized = err
|
serialized = err
|
||||||
local compressed = LibDeflate:CompressDeflate(serialized, {level = 9})
|
local compressed = LibDeflate:CompressDeflate(serialized, {level = 1})
|
||||||
local encoded = LibDeflate:EncodeForPrint(compressed)
|
local encoded = LibDeflate:EncodeForPrint(compressed)
|
||||||
|
|
||||||
-- 3. Broadcast (Token Bucket logic handled in QueueMessage)
|
-- 3. Broadcast (Token Bucket logic handled in QueueMessage)
|
||||||
@@ -224,28 +230,30 @@ function _QuestieLearnerComms:ProcessQueues()
|
|||||||
-- 1. Refill Tokens
|
-- 1. Refill Tokens
|
||||||
local now = GetTime()
|
local now = GetTime()
|
||||||
local elapsed = now - lastTokenUpdate
|
local elapsed = now - lastTokenUpdate
|
||||||
currentTokens = math.min(bucketCapacity, currentTokens + (elapsed * tokenRefillRate))
|
currentTokens = math_min(bucketCapacity, currentTokens + (elapsed * tokenRefillRate))
|
||||||
lastTokenUpdate = now
|
lastTokenUpdate = now
|
||||||
|
|
||||||
-- 2. Drain Outgoing Queue
|
-- 2. Drain Outgoing Queue
|
||||||
if table.getn(rateLimitQueue) > 0 and currentTokens >= 1 and (now - lastChatMessageTime) >= minChatInterval then
|
if table_getn(rateLimitQueue) > 0 and currentTokens >= 1 and (now - lastChatMessageTime) >= minChatInterval then
|
||||||
local msg = table.remove(rateLimitQueue, 1)
|
local msg = table_remove(rateLimitQueue, 1)
|
||||||
currentTokens = currentTokens - 1
|
currentTokens = currentTokens - 1
|
||||||
lastChatMessageTime = now
|
lastChatMessageTime = now
|
||||||
|
|
||||||
-- Send via Hidden Channel (Global reach)
|
-- Use cached channel ID; refresh lazily if 0 (e.g. after disconnect)
|
||||||
local channelId = GetChannelName(hiddenChannelName)
|
if _hiddenChannelId == 0 then
|
||||||
if channelId > 0 then
|
_hiddenChannelId = GetChannelName(hiddenChannelName) or 0
|
||||||
SendChatMessage(msg, "CHANNEL", nil, channelId)
|
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
-- 3. Process Incoming Queue (Combat Aware)
|
-- 3. Process Incoming Queue (Combat Aware)
|
||||||
local processCount = InCombatLockdown() and 2 or 6
|
local processCount = InCombatLockdown() and 2 or 6
|
||||||
for i = 1, processCount do
|
for i = 1, processCount do
|
||||||
if table.getn(incomingMessageQueue) == 0 then break end
|
if table_getn(incomingMessageQueue) == 0 then break end
|
||||||
local rawMsg = table.remove(incomingMessageQueue, 1)
|
local rawMsg = table_remove(incomingMessageQueue, 1)
|
||||||
_QuestieLearnerComms:ProcessRawMessage(rawMsg.text, rawMsg.sender)
|
_QuestieLearnerComms:ProcessRawMessage(rawMsg.text, rawMsg.sender)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ QuestieTooltips.lookupKeysByQuestId = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
local MAX_GROUP_MEMBER_COUNT = 6
|
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
|
local _InitObjectiveTexts
|
||||||
|
|
||||||
@@ -491,28 +497,34 @@ function QuestieTooltips:Initialize()
|
|||||||
end
|
end
|
||||||
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)
|
GameTooltip:HookScript("OnUpdate", function(self)
|
||||||
if QuestiePlayer.numberOfGroupMembers > MAX_GROUP_MEMBER_COUNT then
|
if QuestiePlayer.numberOfGroupMembers > MAX_GROUP_MEMBER_COUNT then
|
||||||
-- When in a raid, we want as little code running as possible
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local now = GetTime()
|
||||||
|
if now - _tooltipLastUpdate < _tooltipUpdateInterval then return end
|
||||||
|
_tooltipLastUpdate = now
|
||||||
|
|
||||||
if (not self.IsForbidden) or (not self:IsForbidden()) then
|
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 uName, unit = self:GetUnit()
|
||||||
local iName, link = self:GetItem()
|
local iName, link = self:GetItem()
|
||||||
local sName, spell = self:GetSpell()
|
local sName, spell = self:GetSpell()
|
||||||
if (uName == nil and unit == nil and iName == nil and link == nil and sName == nil and spell == nil) and (
|
if (uName == nil and unit == nil and iName == nil and link == nil and sName == nil and spell == nil) and (not self.ShownAsMapIcon) then
|
||||||
QuestieTooltips.lastGametooltip ~= GameTooltipTextLeft1:GetText() or
|
local currentText = GameTooltipTextLeft1:GetText()
|
||||||
(not QuestieTooltips.lastGametooltipCount) or
|
if currentText ~= _tooltipLastText
|
||||||
_QuestieTooltips:CountTooltip() < QuestieTooltips.lastGametooltipCount
|
or (not QuestieTooltips.lastGametooltipCount)
|
||||||
or QuestieTooltips.lastGametooltipType ~= "object"
|
or _QuestieTooltips:CountTooltip() < QuestieTooltips.lastGametooltipCount
|
||||||
) and (not self.ShownAsMapIcon) then -- We are hovering over a Questie map icon which adds it's own tooltip
|
or QuestieTooltips.lastGametooltipType ~= "object" then
|
||||||
_QuestieTooltips:AddObjectDataToTooltip(GameTooltipTextLeft1:GetText())
|
_QuestieTooltips:AddObjectDataToTooltip(currentText)
|
||||||
QuestieTooltips.lastGametooltipCount = _QuestieTooltips:CountTooltip()
|
QuestieTooltips.lastGametooltipCount = _QuestieTooltips:CountTooltip()
|
||||||
|
_tooltipLastText = currentText
|
||||||
|
end
|
||||||
|
QuestieTooltips.lastGametooltip = currentText
|
||||||
end
|
end
|
||||||
QuestieTooltips.lastGametooltip = GameTooltipTextLeft1:GetText()
|
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user