feat: refine learner data source and pin rendering
This commit is contained in:
@@ -10,6 +10,11 @@ local HBD = QuestieCompat.HBD or LibStub("HereBeDragonsQuestie-2.0")
|
||||
|
||||
local ZOOM_MODIFIER = 1;
|
||||
|
||||
-- Pins whose world-coordinate distance is within this epsilon are treated as the
|
||||
-- same physical spot and always merged, independent of the clustering range. This
|
||||
-- is the baseline pin deduplication that runs even when clustering is turned off.
|
||||
QuestieMap.utils.COINCIDENT_EPSILON = 0.2;
|
||||
|
||||
-- All the speed we can get is worth it.
|
||||
local tinsert = table.insert
|
||||
local next = next
|
||||
@@ -146,7 +151,11 @@ function QuestieMap.utils:CalcHotzones(points, rangeR, count)
|
||||
-- Do not cluster icons if they have no coordinates
|
||||
and aX ~= 0 and aY ~= 0 and point2.worldX ~= 0 and point2.worldY ~= 0 then
|
||||
local distance = QuestieLib:Euclid(aX, aY, point2.worldX, point2.worldY)
|
||||
if (distance < movingRange) then
|
||||
-- Always deduplicate pins that sit on (essentially) the same spot,
|
||||
-- even when clustering is disabled (movingRange == 0). This keeps two
|
||||
-- icons from stacking on an identical coordinate regardless of the
|
||||
-- clusterDensityAggressiveness / clusterLevelHotzone settings.
|
||||
if (distance < movingRange) or (distance <= QuestieMap.utils.COINCIDENT_EPSILON) then
|
||||
point2.touched = true
|
||||
tinsert(notes, point2)
|
||||
end
|
||||
|
||||
@@ -19,6 +19,8 @@ local IsleOfQuelDanas = QuestieLoader:ImportModule("IsleOfQuelDanas");
|
||||
local l10n = QuestieLoader:ImportModule("l10n")
|
||||
---@type QuestieCompat
|
||||
local QuestieCompat = QuestieLoader:ImportModule("QuestieCompat")
|
||||
---@type QuestieDB
|
||||
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
|
||||
|
||||
QuestieOptions.tabs.advanced = {}
|
||||
local optionsDefaults = QuestieOptionsDefaults:Load()
|
||||
@@ -50,6 +52,9 @@ local function GetLearnerSettings()
|
||||
if settings.minConfidencePins == nil then
|
||||
settings.minConfidencePins = 1
|
||||
end
|
||||
if settings.spawnDedupRadius == nil then
|
||||
settings.spawnDedupRadius = 4.0
|
||||
end
|
||||
return settings
|
||||
end
|
||||
|
||||
@@ -168,6 +173,22 @@ function QuestieOptions.tabs.advanced:Initialize()
|
||||
QuestieOptionsUtils.DetermineTheme()
|
||||
end,
|
||||
},
|
||||
clusterDensityAggressiveness = {
|
||||
type = "range",
|
||||
order = 1.41,
|
||||
name = function() return l10n('Dense pin clustering aggressiveness'); end,
|
||||
desc = function() return l10n('How aggressively crowded kill objectives are consolidated into fewer pins. 0 shows every pin; higher values tighten clustering where many pins share a zone. Coincident pins are always deduplicated.'); end,
|
||||
width = 1.5,
|
||||
disabled = function() return (not Questie.db.profile.enabled); end,
|
||||
min = 0,
|
||||
max = 100,
|
||||
step = 5,
|
||||
get = function(info) return QuestieOptions:GetProfileValue(info); end,
|
||||
set = function(info, value)
|
||||
QuestieOptions:SetProfileValue(info, value)
|
||||
QuestieOptionsUtils:Delay(0.5, QuestieOptions.ClusterRedraw, l10n('Setting dense pin clustering aggressiveness to %s : Redrawing!', value))
|
||||
end,
|
||||
},
|
||||
quelDanasSpacer1 = QuestieOptionsUtils:Spacer(1.45, (not Questie.IsTBC)),
|
||||
npcrules_group = {
|
||||
type = "group",
|
||||
@@ -323,6 +344,26 @@ function QuestieOptions.tabs.advanced:Initialize()
|
||||
settings.performanceMode = "manual"
|
||||
end,
|
||||
},
|
||||
learnerSpawnDedupRadius = {
|
||||
type = "range",
|
||||
order = 2.55,
|
||||
name = function() return l10n('Spawn Pin Dedup Radius'); end,
|
||||
desc = function() return l10n('How close two learned kill positions must be (in map %) to merge into a single pin. Higher values show fewer, tighter pins per spawn; 0 shows every distinct position.'); end,
|
||||
min = 0,
|
||||
max = 15,
|
||||
step = 0.5,
|
||||
width = 1.5,
|
||||
get = function() return GetLearnerSettings().spawnDedupRadius or 4.0 end,
|
||||
set = function(_, value)
|
||||
GetLearnerSettings().spawnDedupRadius = value
|
||||
-- Spawn tables are cached per NPC; clear so the new radius is
|
||||
-- applied on the redraw instead of serving stale merged coords.
|
||||
if QuestieDB and QuestieDB.ClearModeCaches then
|
||||
QuestieDB:ClearModeCaches()
|
||||
end
|
||||
QuestieOptionsUtils:Delay(0.5, QuestieQuest.SmoothReset, l10n('Setting spawn pin dedup radius to %s : Redrawing!', value))
|
||||
end,
|
||||
},
|
||||
learnerCommsIntensity = {
|
||||
type = "select",
|
||||
order = 2.6,
|
||||
|
||||
@@ -52,14 +52,18 @@ local function ApplyLearnerMode()
|
||||
QuestieLearner:ApplyDataSourceMode()
|
||||
end
|
||||
|
||||
local QuestieEventHandler = QuestieLoader:ImportModule("QuestieEventHandler")
|
||||
if QuestieEventHandler and QuestieEventHandler.UpdateAllQuests then
|
||||
QuestieEventHandler:UpdateAllQuests()
|
||||
end
|
||||
|
||||
local QuestieTracker = QuestieLoader:ImportModule("QuestieTracker")
|
||||
if QuestieTracker and QuestieTracker.Update then
|
||||
QuestieTracker:Update()
|
||||
-- SmoothReset is the canonical full refresh: it clears all map/minimap notes
|
||||
-- and tooltips, recalculates and redraws available quests, re-updates every
|
||||
-- active quest, and refreshes the tracker. This makes a data-source-mode
|
||||
-- switch (auto/learner/static/none) take effect everywhere in real time.
|
||||
local QuestieQuest = QuestieLoader:ImportModule("QuestieQuest")
|
||||
if QuestieQuest and QuestieQuest.SmoothReset then
|
||||
QuestieQuest:SmoothReset()
|
||||
else
|
||||
local QuestieTracker = QuestieLoader:ImportModule("QuestieTracker")
|
||||
if QuestieTracker and QuestieTracker.Update then
|
||||
QuestieTracker:Update()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -9,6 +9,11 @@ function QuestieOptionsDefaults:Load()
|
||||
ascensionScalingAsked = false,
|
||||
--Ascension
|
||||
clusterLevelHotzone = 50,
|
||||
-- How aggressively dense objectives (many pins in one zone) get
|
||||
-- consolidated. 0 = off (every pin shown); higher tightens the
|
||||
-- clustering range for crowded kill objectives. Coincident pins are
|
||||
-- always deduplicated regardless of this value. See QuestieQuest:_DrawObjectiveIcons.
|
||||
clusterDensityAggressiveness = 35,
|
||||
enableIconLimit = false,
|
||||
iconLimit = 200,
|
||||
availableScale = 1.2,
|
||||
|
||||
@@ -1847,18 +1847,30 @@ _DrawObjectiveIcons = function(questId, iconsToDraw, objective, maxPerType)
|
||||
|
||||
local iconCount, orderedList = _GetIconsSortedByDistance(iconsToDraw)
|
||||
|
||||
-- Dense kill objectives (like Sunstrider Isle mana wyrms) previously used
|
||||
-- a lower clustering hotzone here. Leave the old behavior commented so we
|
||||
-- can restore it quickly if we need to revisit consolidation again.
|
||||
--[[
|
||||
if iconCount >= 20 then
|
||||
range = math.max(6, math.floor(range * 0.25))
|
||||
elseif iconCount >= 12 then
|
||||
range = math.max(10, math.floor(range * 0.4))
|
||||
elseif iconCount >= 6 then
|
||||
range = math.max(16, math.floor(range * 0.65))
|
||||
-- Dense kill objectives (like Sunstrider Isle mana wyrms) consolidate more
|
||||
-- aggressively the more pins share a zone. This is now user-tunable via the
|
||||
-- clusterDensityAggressiveness knob (0 = off / show every pin, 100 = the
|
||||
-- original aggressive consolidation). The per-zone and object overrides below
|
||||
-- still take precedence, and coincident pins are always deduped in CalcHotzones.
|
||||
local densityAggression = Questie.db.profile.clusterDensityAggressiveness or 0
|
||||
if densityAggression > 0 then
|
||||
local factor = densityAggression / 100
|
||||
if factor > 1 then factor = 1 end
|
||||
local baseMult
|
||||
if iconCount >= 20 then
|
||||
baseMult = 0.25
|
||||
elseif iconCount >= 12 then
|
||||
baseMult = 0.4
|
||||
elseif iconCount >= 6 then
|
||||
baseMult = 0.65
|
||||
end
|
||||
if baseMult then
|
||||
-- Interpolate between no reduction (factor 0) and the full base
|
||||
-- multiplier (factor 1) so the knob scales smoothly.
|
||||
local mult = 1 - (1 - baseMult) * factor
|
||||
range = math.max(1, math.floor(range * mult))
|
||||
end
|
||||
end
|
||||
--]]
|
||||
|
||||
if orderedList[1] and orderedList[1].Icon == Questie.ICON_TYPE_OBJECT then -- new clustering / limit code should prevent problems, always show all object notes
|
||||
range = range * 0.2; -- Only use 20% of the default range.
|
||||
|
||||
+71
-28
@@ -409,6 +409,7 @@ local function EnsureLearnedData()
|
||||
learnItems = true,
|
||||
learnObjects = true,
|
||||
minConfidencePins = 1,
|
||||
spawnDedupRadius = 4.0,
|
||||
prioritizeMyData = true,
|
||||
dataSourceMode = "auto",
|
||||
staleThreshold = 90, -- days
|
||||
@@ -434,6 +435,7 @@ local function EnsureLearnedData()
|
||||
if s.learnItems == nil then s.learnItems = true end
|
||||
if s.learnObjects == nil then s.learnObjects = true end
|
||||
if s.minConfidencePins == nil then s.minConfidencePins = 1 end
|
||||
if s.spawnDedupRadius == nil then s.spawnDedupRadius = 4.0 end
|
||||
if s.prioritizeMyData == nil then s.prioritizeMyData = true end
|
||||
if s.dataSourceMode == nil then
|
||||
if s.prioritizeMyData == false then
|
||||
@@ -517,11 +519,16 @@ function QuestieLearner:ApplyDataSourceMode()
|
||||
CaptureStaticOverrideSnapshot()
|
||||
RestoreStaticOverridesForMode()
|
||||
self:InjectLearnedData()
|
||||
if QuestieDB and QuestieDB.private then
|
||||
-- Rebuild every per-entity cache (including the per-zone quest cache) so the
|
||||
-- mode switch takes effect immediately for reads, pins, and zone lookups.
|
||||
if QuestieDB and QuestieDB.ClearModeCaches then
|
||||
QuestieDB:ClearModeCaches()
|
||||
elseif QuestieDB and QuestieDB.private then
|
||||
QuestieDB.private.questCache = {}
|
||||
QuestieDB.private.itemCache = {}
|
||||
QuestieDB.private.npcCache = {}
|
||||
QuestieDB.private.objectCache = {}
|
||||
QuestieDB.private.zoneCache = {}
|
||||
end
|
||||
QuestieLearner.data = Questie.dbLearner.global
|
||||
end
|
||||
@@ -618,27 +625,11 @@ local _pendingQuestFrameUnloads = {}
|
||||
-- caps the worst-case latency: once that many seconds have elapsed since the first
|
||||
-- pending change, the flush fires even if kills are still coming (0 = pure debounce,
|
||||
-- never force). Only "batched" mode debounces; "immediate" flushes on first fire.
|
||||
local function _FlushActiveQuestPins()
|
||||
if GetTime and GetLearnerSetting("pinRefreshMode", "batched") == "batched" then
|
||||
local timer = (C_Timer) or (QuestieCompat and QuestieCompat.C_Timer)
|
||||
local now = GetTime()
|
||||
local delay = GetLearnerSetting("pinRefreshDelay", 0.75)
|
||||
local maxWait = GetLearnerSetting("pinRefreshMaxWait", 5.0)
|
||||
local quiet = now - (_pendingQuestPinLastActivity or now)
|
||||
local waited = now - (_pendingQuestPinFirstDirty or now)
|
||||
if timer and timer.After and quiet < delay and (maxWait <= 0 or waited < maxWait) then
|
||||
local remaining = delay - quiet
|
||||
if maxWait > 0 then
|
||||
local capRemaining = maxWait - waited
|
||||
if capRemaining < remaining then remaining = capRemaining end
|
||||
end
|
||||
if remaining < 0 then remaining = 0 end
|
||||
-- _pendingQuestPinRefreshTimer stays true so concurrent queues don't double-arm.
|
||||
timer.After(remaining, _FlushActiveQuestPins)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Performs the actual pin rebuild for every pending quest. No debounce gate — the
|
||||
-- caller is responsible for deciding when to fire (either the trailing-debounce
|
||||
-- wrapper below, or an immediate flush from the already-debounced NPC live-update
|
||||
-- flush, which makes the redundant second debounce stage unnecessary).
|
||||
local function _DoFlushActiveQuestPins()
|
||||
local questIdSet = _pendingQuestPinRefreshes
|
||||
_pendingQuestPinRefreshes = {}
|
||||
_pendingQuestPinRefreshTimer = nil
|
||||
@@ -664,6 +655,42 @@ local function _FlushActiveQuestPins()
|
||||
_pendingQuestFrameUnloads = {}
|
||||
end
|
||||
|
||||
local function _FlushActiveQuestPins()
|
||||
-- Only defer while there is genuine pending activity. If the timestamps were
|
||||
-- already cleared (e.g. the NPC live-update flush force-flushed the pins via
|
||||
-- _DoFlushActiveQuestPins), a leftover timer must NOT treat the nil timestamp
|
||||
-- as "now" and re-arm forever — it should fall through and flush (a no-op when
|
||||
-- the pending set is empty).
|
||||
if GetTime and GetLearnerSetting("pinRefreshMode", "batched") == "batched"
|
||||
and _pendingQuestPinLastActivity then
|
||||
local timer = (C_Timer) or (QuestieCompat and QuestieCompat.C_Timer)
|
||||
local now = GetTime()
|
||||
local delay = GetLearnerSetting("pinRefreshDelay", 0.75)
|
||||
local maxWait = GetLearnerSetting("pinRefreshMaxWait", 5.0)
|
||||
local quiet = now - _pendingQuestPinLastActivity
|
||||
local waited = now - (_pendingQuestPinFirstDirty or _pendingQuestPinLastActivity)
|
||||
if timer and timer.After and quiet < delay and (maxWait <= 0 or waited < maxWait) then
|
||||
local remaining = delay - quiet
|
||||
if maxWait > 0 then
|
||||
local capRemaining = maxWait - waited
|
||||
if capRemaining < remaining then remaining = capRemaining end
|
||||
end
|
||||
if remaining < 0 then remaining = 0 end
|
||||
-- _pendingQuestPinRefreshTimer stays true so concurrent queues don't double-arm.
|
||||
timer.After(remaining, _FlushActiveQuestPins)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
_DoFlushActiveQuestPins()
|
||||
end
|
||||
|
||||
-- When true, _RefreshActiveQuestPins only accumulates pending quests and does NOT
|
||||
-- arm its own trailing-debounce timer. Set by _FlushNpcLiveUpdates, which already
|
||||
-- debounced via liveNpcUpdateDelay and force-flushes the pins itself afterwards —
|
||||
-- so the second debounce stage would only add redundant latency.
|
||||
local _deferPinRefreshScheduling = false
|
||||
|
||||
local function _RefreshActiveQuestPins(questIdSet)
|
||||
-- Skip scheduling when the set is empty (avoids no-op timer callbacks)
|
||||
if not next(questIdSet) then return end
|
||||
@@ -684,6 +711,11 @@ local function _RefreshActiveQuestPins(questIdSet)
|
||||
_pendingQuestPinFirstDirty = now
|
||||
end
|
||||
|
||||
-- Caller (NPC live-update flush) will force-flush; don't arm a redundant timer.
|
||||
if _deferPinRefreshScheduling then
|
||||
return
|
||||
end
|
||||
|
||||
if _pendingQuestPinRefreshTimer then
|
||||
return
|
||||
end
|
||||
@@ -916,11 +948,17 @@ local function _FlushNpcLiveUpdates()
|
||||
_Learner.pendingNpcLiveUpdateFirstDirty = nil
|
||||
_Learner.pendingNpcLiveUpdateLastActivity = nil
|
||||
|
||||
-- This flush already coalesced kills over liveNpcUpdateDelay. Suppress the
|
||||
-- per-NPC pin-refresh debounce while invalidating, then flush all affected
|
||||
-- quests once, immediately — instead of waiting out a second pinRefreshDelay.
|
||||
_deferPinRefreshScheduling = true
|
||||
for npcId in pairs(pending) do
|
||||
if _ApplyNpcLiveUpdate(npcId) then
|
||||
_InvalidateSpawnListsForNPC(npcId)
|
||||
end
|
||||
end
|
||||
_deferPinRefreshScheduling = false
|
||||
_DoFlushActiveQuestPins()
|
||||
end
|
||||
|
||||
local function _QueueNpcLiveUpdate(npcId)
|
||||
@@ -1496,12 +1534,17 @@ local function _MergeSpawnEvidence(npcId)
|
||||
entry.y = evidenceY
|
||||
|
||||
local rx, ry = NormalizeCoordPair(evidenceX, evidenceY)
|
||||
local key = entry.zoneId .. "|" .. tostring(rx) .. "|" .. tostring(ry)
|
||||
-- DEBUG: log each entry being grouped
|
||||
Questie:Debug(Questie.DEBUG_LEARNER,
|
||||
"[QuestieLearner] _MergeSpawnEvidence GROUPING: spawnUID=", spawnUID,
|
||||
"entry.x=", entry.x, "entry.y=", entry.y,
|
||||
"rx=", rx, "ry=", ry, "key=", key)
|
||||
-- Group kills by a coordinate bucket, not by exact coords. Kill
|
||||
-- evidence is the player's position at kill time, which drifts a
|
||||
-- little every kill, so exact keys would treat each kill as its own
|
||||
-- "location" — producing one pin per kill and never letting any
|
||||
-- single spot accumulate enough evidence to clear the confidence
|
||||
-- threshold. Bucketing collapses repeated kills at the same spawn
|
||||
-- into one location (matching the [7] InsertIfNewBucket behavior).
|
||||
local grid = GetCoordGridForZone(entry.zoneId)
|
||||
local bx = floor(rx / grid)
|
||||
local by = floor(ry / grid)
|
||||
local key = entry.zoneId .. "|" .. bx .. "|" .. by
|
||||
if not evidence[key] then
|
||||
evidence[key] = { zoneId = entry.zoneId, x = rx, y = ry, count = 0 }
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user