feat(learner): exclude critters; fix Sunstrider object mis-keyed to areaId 3430

- Critters are never quest-relevant. Added a static CRITTER_NPC_SET (common
  classic critters) plus runtime detection via UnitCreatureType on
  mouseover/target (covers Ascension custom critters). LearnNPC and the kill
  handler skip critters; InjectLearnedData purges already-recorded ones; and
  critters are purged on sight when a unit token is available.
- Object/NPC spawns mis-stored under Eversong parent areaId 3430 (e.g. object
  180516 'Shrine of Dath'Remar' on Sunstrider) now migrate to uiMapId 1241,
  along with the [9]/[5] home-zone field. Legit Eversong data is keyed 1941,
  so any 3430 key is mis-stored Sunstrider data.

Verified in Tests/QuestieLearnerCritterPurge_spec.lua.
This commit is contained in:
Xurkon
2026-06-11 16:29:41 -05:00
parent 32c538de15
commit 7bd14b80fc
2 changed files with 85 additions and 6 deletions
+84 -6
View File
@@ -189,6 +189,7 @@ local UnitLevel = UnitLevel
local UnitFactionGroup = UnitFactionGroup
local UnitReaction = UnitReaction
local UnitCreatureFamily = UnitCreatureFamily
local UnitCreatureType = UnitCreatureType
local GetRealZoneText = GetRealZoneText
local GetTitleText = GetTitleText
local GetObjectiveText = GetObjectiveText
@@ -1435,12 +1436,63 @@ local PLAYER_SPAWNED_NPC_SET = {
[1107398] = true, -- Stoneclaw Totem V
}
-- Critters are never quest-relevant, so they should never enter the learner DB.
-- This static set covers the common classic critters; Ascension's custom critters are
-- caught at runtime via UnitCreatureType (see _NoteUnitCreatureType / _Learner.critterIds).
local CRITTER_NPC_SET = {
[721] = true, -- Rabbit
[883] = true, -- Deer
[1933] = true, -- Sheep
[2442] = true, -- Cow
[6368] = true, -- Cat
[2620] = true, -- Prairie Dog
[4953] = true, -- Cat (Wisp)
[9700] = true, -- Squirrel
[5113] = true, -- Mouse
[5114] = true, -- Rat
[5115] = true, -- Snake
[5116] = true, -- Toad
[2914] = true, -- Frog
[385] = true, -- Small Frog
[890] = true, -- Crab
[299] = true, -- Chicken
[620] = true, -- Chicken
[2719] = true, -- Battered Rabbit
}
-- Runtime-discovered critters (npcId -> true), populated by _NoteUnitCreatureType when a
-- unit token is available, so Ascension's custom critters are excluded even if not in the
-- static set above.
_Learner.critterIds = _Learner.critterIds or {}
local function IsCritterNpc(npcId)
return npcId and (CRITTER_NPC_SET[npcId] or _Learner.critterIds[npcId]) or false
end
-- Records the creature type for a unit we currently have a token for (mouseover/target).
-- When the unit is a Critter, remember its npcId and purge any learner data for it, since
-- critters are never quest-relevant and only pollute the learner DB.
local function _NoteUnitCreatureType(unit, npcId)
if not unit or not npcId or npcId <= 0 then return end
if not (UnitCreatureType and UnitExists and UnitExists(unit)) then return end
if UnitCreatureType(unit) == "Critter" then
_Learner.critterIds[npcId] = true
local ld = Questie.dbLearner and Questie.dbLearner.global
if ld and ld.npcs and ld.npcs[npcId] then
ld.npcs[npcId] = nil
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Removed critter from learner data:", npcId)
end
end
end
function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionString, spawnX, spawnY, spawnZoneId)
if not self:IsEnabled() then return end
if not Questie.dbLearner.global.settings.learnNpcs then return end
if not npcId or npcId <= 0 then return end
-- Never learn player-spawned totems
if PLAYER_SPAWNED_NPC_SET[npcId] then return end
-- Never learn critters (not quest-relevant)
if IsCritterNpc(npcId) then return end
-- Use provided spawn coords (e.g. from kill event) or fall back to current player position.
-- Normalize area IDs → map IDs immediately so all storage uses the same key space
@@ -2733,9 +2785,11 @@ function QuestieLearner:InjectLearnedData()
if data[7] then
local zonesToMigrate = {}
for zoneKey, coords in pairs(data[7]) do
-- Legacy Sunstrider coords mis-stored under the Eversong parent areaId 3430
-- belong on Sunstrider's uiMapId 1241, not Eversong's 1941.
if zoneKey == 3430 and IsSunstriderNativeZone(data[9]) then
-- Any spawn under the Eversong PARENT areaId 3430 is mis-stored Sunstrider
-- data: legitimate Eversong coords are keyed by uiMapId 1941, never 3430 (3430
-- only appears via the old uiMapId->areaId bug, which mangled Sunstrider's
-- 1241). Move it to Sunstrider's uiMapId 1241.
if zoneKey == 3430 then
zonesToMigrate[zoneKey] = 1241
else
local normalized = NormalizeSpawnZoneKey(zoneKey)
@@ -2762,7 +2816,9 @@ function QuestieLearner:InjectLearnedData()
if data[4] then
local zonesToMigrate = {}
for zoneKey, coords in pairs(data[4]) do
if zoneKey == 3430 and IsSunstriderNativeZone(data[5]) then
-- Same rule as NPCs: spawns under Eversong parent areaId 3430 are mis-stored
-- Sunstrider data (e.g. object 180516 "Shrine of Dath'Remar") -> uiMapId 1241.
if zoneKey == 3430 then
zonesToMigrate[zoneKey] = 1241
else
local normalized = NormalizeSpawnZoneKey(zoneKey)
@@ -2796,7 +2852,7 @@ function QuestieLearner:InjectLearnedData()
local fieldsFixed = 0
for npcId, data in pairs(learned.npcs) do
if type(data[9]) == "number" then
local normalized = NormalizeSpawnZoneKey(data[9])
local normalized = (data[9] == 3430) and 1241 or NormalizeSpawnZoneKey(data[9])
if normalized and normalized ~= data[9] then
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] NPC", npcId, "zone field [9]", data[9], "->", normalized)
data[9] = normalized
@@ -2806,7 +2862,7 @@ function QuestieLearner:InjectLearnedData()
end
for objId, data in pairs(learned.objects) do
if type(data[5]) == "number" then
local normalized = NormalizeSpawnZoneKey(data[5])
local normalized = (data[5] == 3430) and 1241 or NormalizeSpawnZoneKey(data[5])
if normalized and normalized ~= data[5] then
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Object", objId, "zone field [5]", data[5], "->", normalized)
data[5] = normalized
@@ -2833,6 +2889,12 @@ function QuestieLearner:InjectLearnedData()
learned.npcs[npcId] = nil
purgedNpcs = purgedNpcs + 1
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Purged player-spawned NPC", npcId, data[1] or "?")
elseif IsCritterNpc(npcId) then
-- Critters are never quest-relevant; drop any that were recorded before
-- critter filtering existed.
learned.npcs[npcId] = nil
purgedNpcs = purgedNpcs + 1
Questie:Debug(Questie.DEBUG_INFO, "[QuestieLearner] Purged critter NPC", npcId, data[1] or "?")
elseif data[7] then
-- Check for empty spawn table (no coords at all = stale learner artifact).
-- Only purge if the NPC has no other useful state — keep entries that
@@ -3413,6 +3475,11 @@ function QuestieLearner:OnMouseoverUnit()
end
if unitType ~= "Creature" and unitType ~= "Vehicle" then return end
-- Flag (and purge) critters the moment we have a unit token for them, so Ascension's
-- custom critters are excluded going forward and any already-recorded critter is removed.
_NoteUnitCreatureType("mouseover", entityId)
if IsCritterNpc(entityId) then return end
-- Only learn this NPC if it carries the questgiver flag OR if it is already
-- known in the database as a starter/finisher (so we can update its coords).
local npcFlags = UnitNPCFlags and UnitNPCFlags("mouseover") or 0
@@ -3480,6 +3547,10 @@ function QuestieLearner:OnTargetChanged()
end
if unitType ~= "Creature" and unitType ~= "Vehicle" then return end
-- Flag (and purge) critters as soon as we target one.
_NoteUnitCreatureType("target", entityId)
if IsCritterNpc(entityId) then return end
local level = UnitLevel("target")
_Learner.guidNpcCache = _Learner.guidNpcCache or {}
@@ -4252,6 +4323,13 @@ function QuestieLearner:OnCombatLogEvent(timestamp, eventType, srcGUID, srcName,
if not npcId or npcId <= 0 then return end
-- If we have the dead unit as our current target, note its creature type so critters
-- get flagged/purged; then skip recording any critter kill entirely.
if UnitGUID and UnitGUID("target") == dstGUID then
_NoteUnitCreatureType("target", npcId)
end
if IsCritterNpc(npcId) then return end
-- Determine whether this kill is "credited" to us (our own/party kill we engaged)
-- BEFORE using it to decide whether to record our position. This MUST come first:
-- previously `credited` was read while still nil (defined further below), so the