feat: refine learner data source and pin rendering

This commit is contained in:
Xurkon
2026-06-06 00:04:50 -05:00
parent 1532b70af3
commit 6597d20b7d
12 changed files with 576 additions and 58 deletions
@@ -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).