feat: refine learner data source and pin rendering
This commit is contained in:
@@ -19,6 +19,10 @@
|
||||
- **[Error Suppression - Debug Modes]** Moved missing quest and other non-fatal database/error spam out of normal chat output and into Questie debug-critical/developer output. Fatal startup failures remain loud.
|
||||
- **[Tooltip Data Precedence]** Updated tooltip handling so QuestieLearner defers to AscensionDB-owned tooltip/objective data instead of hiding or replacing server-plugin data for active quests.
|
||||
- **[QuestieQuest - Unavailable Quest Guard]** Guarded the available-quest draw thread so unresolved quest IDs are skipped safely instead of crashing the thread, and deduped the skip log so the same missing quest does not spam every redraw.
|
||||
- **[QuestieLearner - Data Source Mode Cohesion]** Reworked the Auto / Learner / Static / Neither data-source modes so switching between them applies live and consistently. A single missing static sub-table (npc/object/quest/item) no longer locks the whole addon into learner mode — only the genuinely-missing store falls back. Static and Neither no longer silently fall back to learner records, switching modes now clears the per-zone quest cache, and the mode switch drives a full pin/tracker redraw through `QuestieQuest:SmoothReset()` (the previous redraw call imported a mis-named module and silently did nothing).
|
||||
- **[QuestieLearner - Pin Refresh Latency]** Collapsed a redundant second debounce stage on the live-learn pin-refresh path. Newly learned spawns now redraw within a single debounce window instead of waiting out both the NPC live-update delay and a separate pin-refresh delay, roughly halving perceived pin-update latency in the Balanced and Low presets. Also fixed a latent infinite timer re-arm that could occur once the pin flush was triggered directly.
|
||||
- **[Map - Dense Pin Clustering Aggressiveness Knob]** Re-implemented density-adaptive clustering for crowded kill objectives, now controlled by a new "Dense pin clustering aggressiveness" slider on the Advanced tab (0 = show every pin, higher = tighter consolidation where many pins share a zone). Coincident pins are always deduplicated regardless of the clustering settings, and the intentional per-zone (Sunstrider Isle) and object-icon range overrides are preserved.
|
||||
- **[QuestieLearner - One Pin Per Spawn, Not Per Kill]** Fixed learner kill evidence rendering a separate pin for every kill. Because kill coordinates are the player's position at kill time (and respawns carry fresh GUIDs), repeated kills at the same spot drifted just enough to dodge the exact-match dedup. The immediate learner-mode spawn builder now merges evidence within a small radius into one pin per physical spawn, and the weighted spawn-evidence merge groups kills by coordinate bucket so a single spot can actually accumulate enough evidence to clear the confidence threshold. The merge distance is exposed as a new "Spawn Pin Dedup Radius" slider on the Advanced tab (0 = show every distinct position, higher = fewer/tighter pins per spawn) which redraws live.
|
||||
|
||||
### Branch / Release Notes
|
||||
|
||||
|
||||
+78
-10
@@ -210,8 +210,45 @@ QuestieDB.ItemPointers = _dummyHandle.pointers
|
||||
QuestieDB.baseDatabaseMissing = false
|
||||
QuestieDB.baseDatabaseMissingKeys = {}
|
||||
|
||||
-- The four core static stores. The base DB is only considered fully "missing"
|
||||
-- (which forces learner mode) when EVERY one of these failed to load.
|
||||
QuestieDB._baseDatabaseStores = { "npcData", "objectData", "questData", "itemData" }
|
||||
|
||||
-- True only when a specific static store failed to load. Reads use this so a
|
||||
-- single missing/mismatched store falls back to learner data for that store
|
||||
-- alone, instead of locking the whole addon into learner mode.
|
||||
function QuestieDB:IsStoreMissing(storeKey)
|
||||
return QuestieDB.baseDatabaseMissingKeys and QuestieDB.baseDatabaseMissingKeys[storeKey] == true
|
||||
end
|
||||
|
||||
-- Clears every per-entity cache. Used when the data source mode changes so that
|
||||
-- quest/npc/item/object AND per-zone results are rebuilt against the new mode.
|
||||
function QuestieDB:ClearModeCaches()
|
||||
_QuestieDB.questCache = {}
|
||||
_QuestieDB.itemCache = {}
|
||||
_QuestieDB.npcCache = {}
|
||||
_QuestieDB.objectCache = {}
|
||||
_QuestieDB.zoneCache = {}
|
||||
end
|
||||
|
||||
function QuestieDB:IsBaseDatabaseMissing()
|
||||
return QuestieDB.baseDatabaseMissing == true
|
||||
if QuestieDB.baseDatabaseMissing ~= true then
|
||||
return false
|
||||
end
|
||||
-- Only report the base DB as missing when EVERY core store failed. A partial
|
||||
-- failure (e.g. itemData missing but npcData present) must not override the
|
||||
-- user's Data Source Mode selection — per-read fallback handles the gaps.
|
||||
local keys = QuestieDB.baseDatabaseMissingKeys
|
||||
if not keys then
|
||||
return false
|
||||
end
|
||||
local stores = QuestieDB._baseDatabaseStores
|
||||
for i = 1, 4 do
|
||||
if not keys[stores[i]] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function _GetLearnerSettings()
|
||||
@@ -233,11 +270,32 @@ local function _GetLearnerRecord(storeName, id)
|
||||
return store[id] or store[tostring(id)]
|
||||
end
|
||||
|
||||
-- Map-percent radius used to collapse per-GUID kill evidence into one pin per
|
||||
-- physical spawn. Kill coordinates are the player's position at kill time, so
|
||||
-- repeated kills (and respawns, which carry fresh GUIDs) land on slightly
|
||||
-- different coords. Without this merge, every kill would render its own pin.
|
||||
-- A distance test (rather than a grid bucket) avoids the boundary artifact where
|
||||
-- two near-identical coords straddle a cell edge and split into separate pins.
|
||||
-- The radius is user-tunable via the learner "Spawn Pin Dedup Radius" knob; 0
|
||||
-- disables merging (every distinct coord shown).
|
||||
local GUID_SPAWN_DEDUP_RADIUS = 4.0
|
||||
|
||||
local function _GetSpawnDedupRadius()
|
||||
local settings = _GetLearnerSettings()
|
||||
local r = settings and tonumber(settings.spawnDedupRadius)
|
||||
if r and r >= 0 then
|
||||
return r
|
||||
end
|
||||
return GUID_SPAWN_DEDUP_RADIUS
|
||||
end
|
||||
|
||||
local function _BuildSpawnTableFromGuidEvidence(evidence)
|
||||
if type(evidence) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local radius = _GetSpawnDedupRadius()
|
||||
local radiusSq = radius * radius
|
||||
local spawns = {}
|
||||
local hasEntries = false
|
||||
for _, entry in pairs(evidence) do
|
||||
@@ -250,7 +308,9 @@ local function _BuildSpawnTableFromGuidEvidence(evidence)
|
||||
local zoneSpawns = spawns[zoneId]
|
||||
local exists = false
|
||||
for _, coord in ipairs(zoneSpawns) do
|
||||
if coord[1] == x and coord[2] == y then
|
||||
local dx = coord[1] - x
|
||||
local dy = coord[2] - y
|
||||
if (dx * dx + dy * dy) <= radiusSq then
|
||||
exists = true
|
||||
break
|
||||
end
|
||||
@@ -604,12 +664,14 @@ function QuestieDB:GetObject(objectId)
|
||||
|
||||
local rawdata
|
||||
local override
|
||||
if mode == "learner" or QuestieDB:IsBaseDatabaseMissing() then
|
||||
if mode == "learner" or QuestieDB:IsStoreMissing("objectData") then
|
||||
rawdata = learnerRecord
|
||||
override = nil
|
||||
else
|
||||
rawdata = QuestieDB.QueryObject(objectId, QuestieDB._objectAdapterQueryOrder)
|
||||
if not rawdata and learnerRecord then
|
||||
if not rawdata and learnerRecord and mode == "auto" then
|
||||
-- Only "auto" overlays learner data on top of the static DB. "static"
|
||||
-- and "none" must never silently fall back to learner records.
|
||||
rawdata = learnerRecord
|
||||
end
|
||||
override = QuestieDB.objectDataOverrides and (QuestieDB.objectDataOverrides[objectId] or QuestieDB.objectDataOverrides[tostring(objectId)])
|
||||
@@ -660,12 +722,14 @@ function QuestieDB:GetItem(itemId)
|
||||
local learnerRecord = _GetLearnerRecord("items", itemId)
|
||||
local rawdata
|
||||
local override
|
||||
if mode == "learner" or QuestieDB:IsBaseDatabaseMissing() then
|
||||
if mode == "learner" or QuestieDB:IsStoreMissing("itemData") then
|
||||
rawdata = learnerRecord
|
||||
override = nil
|
||||
else
|
||||
rawdata = QuestieDB.QueryItem(itemId, QuestieDB._itemAdapterQueryOrder)
|
||||
if not rawdata and learnerRecord then
|
||||
if not rawdata and learnerRecord and mode == "auto" then
|
||||
-- Only "auto" overlays learner data on top of the static DB. "static"
|
||||
-- and "none" must never silently fall back to learner records.
|
||||
rawdata = learnerRecord
|
||||
end
|
||||
override = QuestieDB.itemDataOverrides and (QuestieDB.itemDataOverrides[itemId] or QuestieDB.itemDataOverrides[tostring(itemId)])
|
||||
@@ -1542,12 +1606,14 @@ function QuestieDB.GetQuest(questId, ...) -- /dump QuestieDB.GetQuest(867)
|
||||
local learnerRecord = _GetLearnerRecord("quests", questId)
|
||||
local rawdata
|
||||
local overrideData
|
||||
if mode == "learner" or QuestieDB:IsBaseDatabaseMissing() then
|
||||
if mode == "learner" or QuestieDB:IsStoreMissing("questData") then
|
||||
rawdata = learnerRecord
|
||||
overrideData = nil
|
||||
else
|
||||
rawdata = QuestieDB.QueryQuest(questId, QuestieDB._questAdapterQueryOrder)
|
||||
if not rawdata and learnerRecord then
|
||||
if not rawdata and learnerRecord and mode == "auto" then
|
||||
-- Only "auto" overlays learner data on top of the static DB. "static"
|
||||
-- and "none" must never silently fall back to learner records.
|
||||
rawdata = learnerRecord
|
||||
end
|
||||
overrideData = QuestieDB.questDataOverrides and (QuestieDB.questDataOverrides[questId] or QuestieDB.questDataOverrides[tostring(questId)])
|
||||
@@ -2155,12 +2221,14 @@ function QuestieDB:GetNPC(npcId)
|
||||
local learnerRecord = _GetLearnerRecord("npcs", npcId)
|
||||
local rawdata
|
||||
local override
|
||||
if mode == "learner" or QuestieDB:IsBaseDatabaseMissing() then
|
||||
if mode == "learner" or QuestieDB:IsStoreMissing("npcData") then
|
||||
rawdata = learnerRecord
|
||||
override = nil
|
||||
else
|
||||
rawdata = QuestieDB.QueryNPC(npcId, QuestieDB._npcAdapterQueryOrder)
|
||||
if not rawdata and learnerRecord then
|
||||
if not rawdata and learnerRecord and mode == "auto" then
|
||||
-- Only "auto" overlays learner data on top of the static DB. "static"
|
||||
-- and "none" must never silently fall back to learner records.
|
||||
rawdata = learnerRecord
|
||||
end
|
||||
override = QuestieDB.npcDataOverrides and (QuestieDB.npcDataOverrides[npcId] or QuestieDB.npcDataOverrides[tostring(npcId)])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -188,6 +188,78 @@ describe("QuestieDB learner source fallback", function()
|
||||
assert.equals(27.25, npc.spawns[44][1][2])
|
||||
end)
|
||||
|
||||
it("collapses many nearby kills (distinct GUIDs) into one spawn pin", function()
|
||||
-- Five kills of respawns at the same spot: distinct GUID keys, slightly
|
||||
-- drifting player coords. This must render as ONE pin, not five.
|
||||
Questie.dbLearner.global.npcs[9005] = {
|
||||
[1] = "Respawning Boar",
|
||||
[8] = {
|
||||
[201] = { zoneId = 44, x = 50.0, y = 50.0 },
|
||||
[202] = { zoneId = 44, x = 50.4, y = 50.3 },
|
||||
[203] = { zoneId = 44, x = 49.7, y = 50.6 },
|
||||
[204] = { zoneId = 44, x = 50.9, y = 49.8 },
|
||||
[205] = { zoneId = 44, x = 50.2, y = 50.1 },
|
||||
},
|
||||
}
|
||||
|
||||
local npc = QuestieDB:GetNPC(9005)
|
||||
assert.is_table(npc)
|
||||
assert.is_table(npc.spawns[44])
|
||||
assert.equals(1, #npc.spawns[44])
|
||||
end)
|
||||
|
||||
it("keeps genuinely separate spawn locations as distinct pins", function()
|
||||
Questie.dbLearner.global.npcs[9006] = {
|
||||
[1] = "Field Boars",
|
||||
[8] = {
|
||||
[301] = { zoneId = 44, x = 20.0, y = 20.0 },
|
||||
[302] = { zoneId = 44, x = 20.3, y = 20.2 }, -- same spot as 301
|
||||
[303] = { zoneId = 44, x = 70.0, y = 65.0 }, -- far corner
|
||||
},
|
||||
}
|
||||
|
||||
local npc = QuestieDB:GetNPC(9006)
|
||||
assert.is_table(npc)
|
||||
assert.is_table(npc.spawns[44])
|
||||
assert.equals(2, #npc.spawns[44])
|
||||
end)
|
||||
|
||||
it("honors the spawn dedup radius knob (0 disables proximity merge)", function()
|
||||
Questie.dbLearner.global.settings.spawnDedupRadius = 0
|
||||
Questie.dbLearner.global.npcs[9007] = {
|
||||
[1] = "Drifting Kills",
|
||||
[8] = {
|
||||
[401] = { zoneId = 44, x = 50.0, y = 50.0 },
|
||||
[402] = { zoneId = 44, x = 50.4, y = 50.3 },
|
||||
[403] = { zoneId = 44, x = 49.7, y = 50.6 },
|
||||
[404] = { zoneId = 44, x = 50.9, y = 49.8 },
|
||||
[405] = { zoneId = 44, x = 50.2, y = 50.1 },
|
||||
},
|
||||
}
|
||||
|
||||
local npc = QuestieDB:GetNPC(9007)
|
||||
assert.is_table(npc)
|
||||
assert.is_table(npc.spawns[44])
|
||||
-- With merging off, each distinct kill coordinate stays its own pin.
|
||||
assert.equals(5, #npc.spawns[44])
|
||||
end)
|
||||
|
||||
it("widens merging when the dedup radius is increased", function()
|
||||
Questie.dbLearner.global.settings.spawnDedupRadius = 12
|
||||
Questie.dbLearner.global.npcs[9008] = {
|
||||
[1] = "Loose Cluster",
|
||||
[8] = {
|
||||
[501] = { zoneId = 44, x = 40.0, y = 40.0 },
|
||||
[502] = { zoneId = 44, x = 48.0, y = 46.0 }, -- ~10 away: merges at radius 12
|
||||
},
|
||||
}
|
||||
|
||||
local npc = QuestieDB:GetNPC(9008)
|
||||
assert.is_table(npc)
|
||||
assert.is_table(npc.spawns[44])
|
||||
assert.equals(1, #npc.spawns[44])
|
||||
end)
|
||||
|
||||
it("returns learner object data when static queries are unavailable", function()
|
||||
local obj = QuestieDB:GetObject(9002)
|
||||
assert.is_table(obj)
|
||||
@@ -196,3 +268,97 @@ describe("QuestieDB learner source fallback", function()
|
||||
assert.equals(44, obj.zoneID)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieDB partial base DB missing", function()
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
dofile("Database/QuestieDB.lua")
|
||||
end)
|
||||
|
||||
it("does not report the base DB missing when only one store failed", function()
|
||||
QuestieDB.baseDatabaseMissing = true
|
||||
QuestieDB.baseDatabaseMissingKeys = { itemData = true }
|
||||
assert.is_false(QuestieDB:IsBaseDatabaseMissing())
|
||||
assert.is_true(QuestieDB:IsStoreMissing("itemData"))
|
||||
assert.is_false(QuestieDB:IsStoreMissing("npcData"))
|
||||
end)
|
||||
|
||||
it("reports the base DB missing only when every core store failed", function()
|
||||
QuestieDB.baseDatabaseMissing = true
|
||||
QuestieDB.baseDatabaseMissingKeys = {
|
||||
npcData = true, objectData = true, questData = true, itemData = true,
|
||||
}
|
||||
assert.is_true(QuestieDB:IsBaseDatabaseMissing())
|
||||
end)
|
||||
|
||||
it("honors the static selection for a present store even when another is missing", function()
|
||||
QuestieDB.baseDatabaseMissing = true
|
||||
QuestieDB.baseDatabaseMissingKeys = { itemData = true }
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "static"
|
||||
Questie.dbLearner.global.npcs = {
|
||||
[9100] = { [1] = "Should Not Win", [7] = { [44] = { { 1, 2 } } }, [9] = 44 },
|
||||
}
|
||||
QuestieDB.private.npcCache = {}
|
||||
local queried = false
|
||||
QuestieDB.QueryNPC = function() queried = true; return nil end
|
||||
|
||||
pcall(function() QuestieDB:GetNPC(9100) end)
|
||||
assert.is_true(queried)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieDB mode cohesion", function()
|
||||
before_each(function()
|
||||
dofile("Tests/wow_api_mock.lua")
|
||||
dofile("Database/QuestieDB.lua")
|
||||
dofile("Database/npcDB.lua")
|
||||
Questie.dbLearner.global.settings.enabled = true
|
||||
Questie.dbLearner.global.npcs = {
|
||||
[9200] = { [1] = "Learner Only NPC", [7] = { [44] = { { 5, 6 } } }, [9] = 44 },
|
||||
}
|
||||
QuestieDB.QueryNPC = function() return nil end
|
||||
QuestieDB.baseDatabaseMissing = false
|
||||
QuestieDB.baseDatabaseMissingKeys = {}
|
||||
QuestieDB.private.npcCache = {}
|
||||
end)
|
||||
|
||||
it("does NOT leak learner data into static mode when the store is present", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "static"
|
||||
QuestieDB.private.npcCache = {}
|
||||
assert.is_nil(QuestieDB:GetNPC(9200))
|
||||
end)
|
||||
|
||||
it("does NOT leak learner data into none mode when the store is present", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "none"
|
||||
QuestieDB.private.npcCache = {}
|
||||
assert.is_nil(QuestieDB:GetNPC(9200))
|
||||
end)
|
||||
|
||||
it("DOES overlay learner data in auto mode when the static DB lacks it", function()
|
||||
Questie.dbLearner.global.settings.dataSourceMode = "auto"
|
||||
QuestieDB.private.npcCache = {}
|
||||
local npc = QuestieDB:GetNPC(9200)
|
||||
assert.is_table(npc)
|
||||
assert.equals("Learner Only NPC", npc.name)
|
||||
end)
|
||||
|
||||
it("clears every cache including the zone cache on mode switch", function()
|
||||
QuestieDB.private.questCache[1] = {}
|
||||
QuestieDB.private.zoneCache[1] = {}
|
||||
QuestieDB:ClearModeCaches()
|
||||
assert.is_nil(QuestieDB.private.questCache[1])
|
||||
assert.is_nil(QuestieDB.private.zoneCache[1])
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("QuestieLearner mode switch redraw wiring", function()
|
||||
it("drives a full real-time refresh via SmoothReset, not the mis-named event handler", function()
|
||||
local function read(path)
|
||||
local f = assert(io.open(path, "r")); local c = f:read("*a"); f:close(); return c
|
||||
end
|
||||
local dbOptions = read("Modules/Options/DatabaseTab/QuestieOptionsDatabase.lua")
|
||||
assert.is_true(string.find(dbOptions, "QuestieQuest:SmoothReset()", 1, true) ~= nil)
|
||||
-- The old import name resolved to nil and silently skipped the redraw.
|
||||
assert.is_nil(string.find(dbOptions, "ImportModule(\"QuestieEventHandler\")", 1, true))
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -105,6 +105,42 @@ describe("QuestieLearner kill-path batching", function()
|
||||
assert.is_true(table.getn(queuedTimers) >= 2)
|
||||
end)
|
||||
|
||||
it("force-flushes active quest pins within the NPC live-update flush (no second debounce)", function()
|
||||
local updateCount = 0
|
||||
local originalUpdateQuest = QuestieQuest.UpdateQuest
|
||||
QuestieQuest.UpdateQuest = function(_, questId)
|
||||
updateCount = updateCount + 1
|
||||
end
|
||||
|
||||
QuestiePlayer.currentQuestlog = { [5500] = true }
|
||||
local originalGetQuest = QuestieDB.GetQuest
|
||||
QuestieDB.GetQuest = function(questId)
|
||||
if questId == 5500 then
|
||||
return { Objectives = { [1] = { Id = 7500, spawnList = { [7500] = {} } } } }
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
QuestieLearner:LearnNPC(7500, "Quest Boar", nil, nil, nil, nil, 41.0, 52.0, 44)
|
||||
|
||||
-- Drain exactly ONE timer round (the NPC live-update flush). The pin refresh
|
||||
-- must happen inside that same flush, not in a later pinRefreshDelay cycle.
|
||||
local firstRound = queuedTimers
|
||||
queuedTimers = {}
|
||||
for i = 1, table.getn(firstRound) do
|
||||
simulatedTime = simulatedTime + 1
|
||||
firstRound[i]()
|
||||
end
|
||||
|
||||
assert.is_true(updateCount >= 1)
|
||||
|
||||
-- And draining the rest must terminate (no infinite self-re-arming timer).
|
||||
drainQueuedTimers()
|
||||
|
||||
QuestieDB.GetQuest = originalGetQuest
|
||||
QuestieQuest.UpdateQuest = originalUpdateQuest
|
||||
end)
|
||||
|
||||
it("coalesces repeated quest-pin refreshes into one flush", function()
|
||||
local updateCount = 0
|
||||
local originalUpdateQuest = QuestieQuest.UpdateQuest
|
||||
|
||||
@@ -188,6 +188,10 @@
|
||||
<li><strong>[Arrow — Low-End Performance Controls]</strong> Added live Arrow update throttles to reduce repeated nearest-target and coordinate work while preserving existing arrow behavior.</li>
|
||||
<li><strong>[Measured Hot Paths — Phase 3]</strong> Landed measured optimizations on the phase 3 branch for literal localization caching, available quest redraw batching, <code>QuestieDB.IsDoable</code> batch reads, hot profile aliases, <code>GetTime()</code> hoists, NPC fallback lookup caching, and validate-cache allocation cleanup.</li>
|
||||
<li><strong>[QuestieQuest — Unavailable Quest Guard]</strong> Guarded the available-quest draw thread so unresolved quest IDs are skipped safely instead of crashing the thread, and deduped the skip log so the same missing quest does not spam every redraw.</li>
|
||||
<li><strong>[QuestieLearner — Data Source Mode Cohesion]</strong> Reworked the Auto / Learner / Static / Neither data-source modes so switching between them applies live and consistently. A single missing static sub-table (npc/object/quest/item) no longer locks the whole addon into learner mode — only the genuinely-missing store falls back. Static and Neither no longer silently fall back to learner records, switching modes now clears the per-zone quest cache, and the mode switch drives a full pin/tracker redraw through <code>QuestieQuest:SmoothReset()</code> (the previous redraw call imported a mis-named module and silently did nothing).</li>
|
||||
<li><strong>[QuestieLearner — Pin Refresh Latency]</strong> Collapsed a redundant second debounce stage on the live-learn pin-refresh path. Newly learned spawns now redraw within a single debounce window instead of waiting out both the NPC live-update delay and a separate pin-refresh delay, roughly halving perceived pin-update latency in the Balanced and Low presets. Also fixed a latent infinite timer re-arm that could occur once the pin flush was triggered directly.</li>
|
||||
<li><strong>[Map — Dense Pin Clustering Aggressiveness Knob]</strong> Re-implemented density-adaptive clustering for crowded kill objectives, now controlled by a new <em>Dense pin clustering aggressiveness</em> slider on the Advanced tab (0 = show every pin, higher = tighter consolidation where many pins share a zone). Coincident pins are always deduplicated regardless of the clustering settings, and the intentional per-zone (Sunstrider Isle) and object-icon range overrides are preserved.</li>
|
||||
<li><strong>[QuestieLearner — One Pin Per Spawn, Not Per Kill]</strong> Fixed learner kill evidence rendering a separate pin for every kill. Because kill coordinates are the player's position at kill time (and respawns carry fresh GUIDs), repeated kills at the same spot drifted just enough to dodge the exact-match dedup. The immediate learner-mode spawn builder now merges evidence within a small radius into one pin per physical spawn, and the weighted spawn-evidence merge groups kills by coordinate bucket so a single spot can actually accumulate enough evidence to clear the confidence threshold. The merge distance is exposed as a new <em>Spawn Pin Dedup Radius</em> slider on the Advanced tab (0 = show every distinct position, higher = fewer/tighter pins per spawn) which redraws live.</li>
|
||||
<li><strong>[Error Suppression — Debug Modes]</strong> Moved missing quest and other non-fatal database/error spam out of normal chat output and into Questie debug-critical/developer output. Fatal startup failures remain loud.</li>
|
||||
<li><strong>[Tooltip Data Precedence]</strong> Updated tooltip handling so QuestieLearner defers to AscensionDB-owned tooltip/objective data instead of hiding or replacing server-plugin data for active quests.</li>
|
||||
</ul>
|
||||
|
||||
@@ -6027,3 +6027,129 @@ Learner kill evidence now turns into visible spawn coordinates quickly enough
|
||||
to spawn pins in learner mode, and the available quest scanner now fails
|
||||
closed when a quest record is missing instead of flooding chat or crashing the
|
||||
draw thread.
|
||||
|
||||
---
|
||||
|
||||
# 2026-06-05 — Data Source Mode Cohesion, Pin Refresh Latency, and Pin Clustering Knob
|
||||
|
||||
## Summary
|
||||
|
||||
Three related areas were addressed after the immediate-spawn work above:
|
||||
|
||||
1. Switching the Data Source Mode (Auto / Learner / Static / Neither) did not
|
||||
reliably take effect — including a case where it could never switch back to
|
||||
the static database even across `/reload`.
|
||||
2. Newly learned spawns were slow to redraw pins because two trailing-debounce
|
||||
stages were stacked on the live-learn path.
|
||||
3. The density-adaptive pin clustering that was previously commented out needed
|
||||
re-implementing as a user-tunable knob, plus a guaranteed coincident-pin
|
||||
deduplication baseline.
|
||||
|
||||
## Root causes
|
||||
|
||||
- **Mode lock-in.** `QuestieDB.baseDatabaseMissing` was a single global flag set
|
||||
to `true` when ANY one of the four core stores (`npcData`, `objectData`,
|
||||
`questData`, `itemData`) failed to load. Both the per-read DB functions and
|
||||
`GetDataSourceMode()` force learner mode whenever `IsBaseDatabaseMissing()` is
|
||||
true, so a single missing/format-mismatched sub-table pinned the entire addon
|
||||
to learner data and survived `/reload` (the flag is recomputed identically at
|
||||
load).
|
||||
- **Learner leak into Static/Neither.** The per-read fallback
|
||||
(`if not rawdata and learnerRecord then rawdata = learnerRecord`) ran in the
|
||||
`else` branch for every non-learner mode, so Static and Neither silently used
|
||||
learner records when the static DB lacked an entry.
|
||||
- **Dead redraw on mode switch.** `ApplyLearnerMode` imported
|
||||
`"QuestieEventHandler"` (the module is registered as `"QuestEventHandler"`) and
|
||||
called `UpdateAllQuests`, which lives on the private table — so the import
|
||||
returned nil and available-quest pins were never redrawn on a mode switch.
|
||||
- **Stale zone cache.** `ApplyDataSourceMode` cleared the quest/npc/item/object
|
||||
caches but not `zoneCache`, so per-zone quest results lingered after a switch.
|
||||
- **Stacked pin-refresh debounce.** A learned kill flowed through
|
||||
`liveNpcUpdateDelay` (NPC live-update flush) and THEN a separate
|
||||
`pinRefreshDelay` gate before `UpdateQuest`. Because the only caller of the
|
||||
pin-invalidation path is the already-debounced NPC flush, the second stage was
|
||||
pure added latency (~1.5s Balanced, ~4s Low), with two independent
|
||||
`pinRefreshMaxWait` caps under sustained kills.
|
||||
|
||||
## What changed
|
||||
|
||||
- `Database/QuestieDB.lua`
|
||||
- `IsBaseDatabaseMissing()` now reports missing only when ALL four core stores
|
||||
failed; added `IsStoreMissing(storeKey)` for per-store checks.
|
||||
- Each read (`GetNPC` / `GetQuest` / `GetItem` / `GetObject`) gates force-learner
|
||||
on its own store and only overlays a learner-record fallback in Auto mode.
|
||||
- Added `ClearModeCaches()` which clears quest/item/npc/object AND zone caches.
|
||||
- `Modules/QuestieLearner.lua`
|
||||
- `ApplyDataSourceMode` now calls `ClearModeCaches()`.
|
||||
- Split the active-quest pin flush into a debounce-gate wrapper plus a
|
||||
`_DoFlushActiveQuestPins` body. The NPC live-update flush now suppresses the
|
||||
redundant second debounce and force-flushes pins once, immediately.
|
||||
- Hardened the flush gate so it only defers while there is genuine pending
|
||||
activity (fixes an infinite timer re-arm exposed by the direct flush).
|
||||
- `Modules/Options/DatabaseTab/QuestieOptionsDatabase.lua`
|
||||
- Mode switches now refresh through `QuestieQuest:SmoothReset()` (clears notes
|
||||
and tooltips, recalculates and redraws available quests, re-updates active
|
||||
quests, refreshes the tracker).
|
||||
- `Modules/Quest/QuestieQuest.lua`
|
||||
- Re-implemented density-adaptive clustering, scaled by a new
|
||||
`clusterDensityAggressiveness` profile knob. The intentional per-zone
|
||||
(Sunstrider Isle, uiMapID 1241, range 0) and object-icon (`range * 0.2`)
|
||||
overrides are preserved and still take precedence.
|
||||
- `Modules/Map/QuestieMapUtils.lua`
|
||||
- `CalcHotzones` now always merges coincident pins
|
||||
(within `COINCIDENT_EPSILON`), independent of the clustering range, so two
|
||||
icons never stack on the same spot even when clustering is disabled.
|
||||
- `Modules/Options/QuestieOptionsDefaults.lua`,
|
||||
`Modules/Options/AdvancedTab/QuestieOptionsAdvanced.lua`
|
||||
- Added the `clusterDensityAggressiveness` default (35) and a real-time
|
||||
Advanced-tab slider that redraws via `QuestieOptions:ClusterRedraw`.
|
||||
- Tests
|
||||
- `Tests/QuestieLearnerDataSourceMode_spec.lua`: partial-missing store handling,
|
||||
Static/Neither no-leak, Auto overlay, full cache clear, SmoothReset wiring.
|
||||
- `Tests/QuestieLearner_performance_spec.lua`: pins force-flush within a single
|
||||
NPC live-update round and draining terminates (infinite-loop guard).
|
||||
- `Tests/QuestiePinClustering_spec.lua` (new): coincident dedup, distinct-pin
|
||||
preservation at range 0, range-based clustering, cross-map isolation, and
|
||||
knob wiring.
|
||||
|
||||
## Verification
|
||||
|
||||
- `luac5.1 -p` on every changed Lua file.
|
||||
- `busted` full suite: 100 successes / 4 failures (the 4 failures are the
|
||||
pre-existing, unrelated `QuestieArrowAssets_spec` asset checks).
|
||||
|
||||
## Result
|
||||
|
||||
All four data-source modes now switch live and cohesively (and partial static
|
||||
DB failures no longer trap the addon in learner mode). Newly learned spawns
|
||||
redraw within a single debounce window. Dense kill objectives can be
|
||||
consolidated to taste via the new aggressiveness knob, while coincident pins are
|
||||
always deduplicated and the curated per-zone/object overrides remain intact.
|
||||
|
||||
### Follow-up — learner pin-per-kill fix
|
||||
|
||||
Learner kill evidence was rendering one pin per kill. Kill coordinates come from
|
||||
the player's position at kill time and respawns carry fresh GUIDs, so repeated
|
||||
kills at the same spawn drifted just enough to defeat the exact-match dedup in
|
||||
`_BuildSpawnTableFromGuidEvidence` (`Database/QuestieDB.lua`) and the
|
||||
full-precision grouping key in `_MergeSpawnEvidence` (`Modules/QuestieLearner.lua`).
|
||||
|
||||
- `_BuildSpawnTableFromGuidEvidence` now merges evidence coords within a small
|
||||
radius (distance test, not a grid bucket — avoids the cell-boundary split where
|
||||
two near-identical coords land in different buckets) into one pin per spawn.
|
||||
- `_MergeSpawnEvidence` now groups kills by a per-zone coordinate bucket
|
||||
(`GetCoordGridForZone`) instead of exact coords, so one location accumulates
|
||||
enough evidence to clear the >60% confidence threshold instead of every kill
|
||||
registering as its own single-count "location".
|
||||
- The `[7]` spawn path already deduped via `InsertIfNewBucket`; only the GUID
|
||||
evidence (`[8]`) consumers needed the fix.
|
||||
- The merge radius is user-tunable via the new `spawnDedupRadius` learner setting
|
||||
(default 4.0, map-percent) and a "Spawn Pin Dedup Radius" slider on the
|
||||
Advanced tab. `_BuildSpawnTableFromGuidEvidence` reads it through
|
||||
`_GetSpawnDedupRadius` (0 = exact-match only / every distinct position shown);
|
||||
changing it clears the NPC spawn cache and triggers a live redraw.
|
||||
- Regressions added in `Tests/QuestieLearnerDataSourceMode_spec.lua`: many nearby
|
||||
kills collapse to one pin, genuinely separate spawns stay distinct, radius 0
|
||||
disables proximity merge, and a larger radius widens merging.
|
||||
`Tests/QuestiePinClustering_spec.lua` asserts the knob wiring (default + UI +
|
||||
cache-clear redraw).
|
||||
|
||||
Reference in New Issue
Block a user