fix(map): harden dungeon quest detection

This commit is contained in:
Xurkon
2026-06-11 06:59:08 -05:00
parent abef53cede
commit 92d1ad09b7
3 changed files with 272 additions and 2 deletions
+1 -1
View File
@@ -62,7 +62,7 @@
- **[Tooltip - Data Source In Secondary Tooltip Only]** The `Source:` attribution line now appears only inside the secondary learner tooltip (when "Use secondary learner tooltip" is enabled) and is never added to the main NPC/object/item tooltip; with the secondary tooltip disabled it does not appear at all. The map-pin source line is gated the same way. The "Show data source" option now depends on the secondary-tooltip option being enabled.
- **[Tooltip - Learner Pin Mislabelled As AscensionDB]** A spawn freshly learned via the learner was shown as `Source: AscensionDB` instead of `Learner`. `QuestieDB.GetPinDataSource` resolved AscensionDB-curated ownership before checking the learner, but in learner-only mode the displayed spawns are the learner's (curated coords are discarded by `GetNPC`/`GetObject`). It is now mode-aware: in learner mode, an entity with a learner record reports `Learner` even when AscensionDB also curates it.
- **[Learner - Learner-Only Mode No Longer Draws AscensionDB/Static Pins]** In learner-only mode the map/minimap drew curated AscensionDB (and static) spawns for every quest NPC/object the player had not personally recorded, instead of only the learner's own spawns. `QuestieDB:GetNPC`/`GetObject` fall back to the `npcDataOverrides`/`objectDataOverrides` entry for metadata when there is no learner record, and that entry's spawns (AscensionDB-curated for un-recorded entities) were being used as pins. Both functions now track whether a real learner record exists and, in learner mode, strip the spawns when it does not — so learner-only mode shows exclusively what the learner has recorded (the metadata fallback for names/tooltips is kept). Added a regression test. Recorded entities still show their learner spawns; auto/static/none modes are unchanged.
- **[Map - Quest Type Filters Now Apply To Minimap]** (#11) Quest-type filters (Available Dungeon/Raid/PvP/Repeatable/Event Quests, plus other `ShouldBeHidden` rules like untracked/unexplored and `enableMiniMapIcons`) were honored on the world map but not the minimap. The world-map icon stayed hidden once `ShouldBeHidden` filtered it, but the minimap icon's per-frame `FadeLogic` re-showed any hidden icon as soon as the player came within range, so e.g. a filtered dungeon quest still appeared on the minimap. `FadeLogic` now re-checks `ShouldBeHidden` before re-showing, so the same filters apply to both maps (fixed in both the world-icon and manual-icon minimap fade paths).
- **[Map - Quest Type Filters Now Apply To Minimap]** (#11) Quest-type filters (Available Dungeon/Raid/PvP/Repeatable/Event Quests, plus other `ShouldBeHidden` rules like untracked/unexplored and `enableMiniMapIcons`) were honored on the world map but not the minimap. The world-map icon stayed hidden once `ShouldBeHidden` filtered it, but the minimap icon's per-frame `FadeLogic` re-showed any hidden icon as soon as the player came within range, so e.g. a filtered dungeon quest still appeared on the minimap. `FadeLogic` now re-checks `ShouldBeHidden` before re-showing, so the same filters apply to both maps (fixed in both the world-icon and manual-icon minimap fade paths). Dungeon detection is also hardened now: when the quest tag is missing, Questie falls back to quest zone/sort data and dungeon-only starter spawns so missing quest-key flags do not leak dungeon quests back onto the minimap.
- **[Libs/AceComm - Long Comm Prefix Login Error]** (#12) Fixed a recurring login Lua error `AceComm:RegisterComm(...): prefix length is limited to 16 characters` on servers like ChromieCraft. The cause is a third-party addon (e.g. AtlasLoot) registering an AceComm prefix longer than the client's 16-character limit; because Questie's bundled AceComm is the LibStub winner and its `xpcall` polyfill wraps AceAddon's `OnEnable`, the upstream hard `error()` surfaced through Questie's frames every login. Our bundled `AceComm:RegisterComm` now degrades gracefully for over-long prefixes (which can never work on the client anyway): it warns once and skips the registration instead of throwing, so the offending addon's `OnEnable` is no longer aborted and the error popup is gone.
- **[Tracker - VoiceOver questPlayButtons Nil Crash]** (#15) Fixed `attempt to index field 'questPlayButtons' (a nil value)` in `TrackerUtils:UpdateVoiceOverPlayButtons` and `TrackerLinePool.SetAllPlayButtonAlpha`. `TrackerUtils:IsVoiceOverLoaded` only checked that the VoiceOver addons were loaded, but some VoiceOver builds (seen on Elune) expose a `QuestOverlayUI` without a `questPlayButtons` table. It now also verifies `VoiceOver.QuestOverlayUI.questPlayButtons` exists; since every play-button call site gates on this function, the integration is now safely skipped instead of crashing on those builds.
- **[Libs/AceGUI - Tree Tooltip Nil Crash]** (#15) Hardened `AceGUIContainer-TreeGroup` against `attempt to index ... 'tooltip' (a nil value)` when a conflicting addon registers a broken `AceGUI-3.0` core (observed in the wild as version `1.#INF`) that wins LibStub but never creates the shared `AceGUI.tooltip` frame. The tree button enter/leave handlers now lazily (re)create the tooltip frame; the widget version was bumped 47→48 so the fixed widget wins registration over an unpatched same-version copy.
+70 -1
View File
@@ -867,6 +867,49 @@ function QuestieDB.IsBoardQuest(questId)
return result
end
local _dungeonQuestCache = {}
local function _ZoneLooksDungeon(zoneId)
if type(zoneId) ~= "number" or zoneId <= 0 then
return false
end
if ZoneDB and ZoneDB.IsDungeonZone and ZoneDB:IsDungeonZone(zoneId) then
return true
end
if not ZoneDB then
return false
end
local alternativeZoneId = ZoneDB.GetAlternativeZoneId and ZoneDB:GetAlternativeZoneId(zoneId)
if alternativeZoneId and ZoneDB.IsDungeonZone and ZoneDB:IsDungeonZone(alternativeZoneId) then
return true
end
local parentZoneId = ZoneDB.GetParentZoneId and ZoneDB:GetParentZoneId(zoneId)
return parentZoneId and ZoneDB.IsDungeonZone and ZoneDB:IsDungeonZone(parentZoneId) or false
end
local function _AnySpawnZoneLooksDungeon(ids, querySingle)
if type(ids) ~= "table" or type(querySingle) ~= "function" then
return false
end
for _, id in ipairs(ids) do
local spawns = querySingle(id, "spawns")
if type(spawns) == "table" then
for zoneId in pairs(spawns) do
if _ZoneLooksDungeon(zoneId) then
return true
end
end
end
end
return false
end
---@param questId number
---@return boolean
function QuestieDB.IsDailyQuest(questId)
@@ -887,7 +930,33 @@ end
---@return boolean
function QuestieDB.IsDungeonQuest(questId)
local questType, _ = QuestieDB.GetQuestTagInfo(questId)
return questType == 81
if questType == 81 then
return true
end
if questType ~= nil and questType ~= 0 then
return false
end
local cached = _dungeonQuestCache[questId]
if cached ~= nil then
return cached
end
local result = false
local zoneOrSort = QuestieDB.QueryQuestSingle(questId, "zoneOrSort")
if _ZoneLooksDungeon(zoneOrSort) then
result = true
else
local startedBy = QuestieDB.QueryQuestSingle(questId, "startedBy")
if type(startedBy) == "table" then
result = _AnySpawnZoneLooksDungeon(startedBy[1], QuestieDB.QueryNPCSingle)
or _AnySpawnZoneLooksDungeon(startedBy[2], QuestieDB.QueryObjectSingle)
end
end
_dungeonQuestCache[questId] = result
return result
end
---@param questId number
+201
View File
@@ -0,0 +1,201 @@
describe("QuestieDB learner spawn suppression hardening", function()
before_each(function()
dofile("Tests/wow_api_mock.lua")
dofile("Database/QuestieDB.lua")
end)
it("ignores malformed numeric NPC spawns while still suppressing valid learned rows", function()
Questie.dbLearner.global.settings.enabled = true
Questie.dbLearner.global.settings.prioritizeMyData = true
Questie.dbLearner.global.npcs = {
[1001] = {
mc = 2,
[7] = 123,
},
[1002] = {
mc = 2,
[7] = {
[3431] = { { 12.5, 34.5 } },
},
},
}
local suppressed = QuestieDB.GetSuppressedNPCs(3431)
assert.is_nil(suppressed[1001])
assert.is_true(suppressed[1002])
end)
it("falls back to legacy object spawn tables when the current field is malformed", function()
Questie.dbLearner.global.settings.enabled = true
Questie.dbLearner.global.settings.prioritizeMyData = true
Questie.dbLearner.global.objects = {
[2001] = {
mc = 2,
[4] = 456,
[7] = {
[3431] = { { 55.5, 66.6 } },
},
},
[2002] = {
mc = 2,
[4] = 789,
},
}
local suppressed = QuestieDB.GetSuppressedObjects(3431)
assert.is_true(suppressed[2001])
assert.is_nil(suppressed[2002])
end)
it("normalizes malformed NPC spawn entries during cleanup", function()
local entry = {
[1] = "Test NPC",
[4] = {
[3431] = { { 10.1, 20.2 } },
},
[7] = 999,
}
local changed = QuestieDB.private.NormalizeLearnerSpawnEntry(entry, 7, 4)
assert.is_true(changed)
assert.is_table(entry[7])
assert.is_nil(entry[4])
assert.are.same({ [3431] = { { 10.1, 20.2 } } }, entry[7])
end)
it("normalizes malformed object spawn entries during cleanup", function()
local entry = {
[1] = "Test Object",
[4] = 999,
[7] = {
[3431] = { { 77.7, 88.8 } },
},
}
local changed = QuestieDB.private.NormalizeLearnerSpawnEntry(entry, 4, 7)
assert.is_true(changed)
assert.is_table(entry[4])
assert.is_nil(entry[7])
assert.are.same({ [3431] = { { 77.7, 88.8 } } }, entry[4])
end)
it("treats dungeon quests with missing quest tags as dungeon quests when zone data proves it", function()
local oldGetQuestTagInfo = _G.GetQuestTagInfo
local oldQueryQuestSingle = QuestieDB.QueryQuestSingle
local oldIsDungeonZone = ZoneDB.IsDungeonZone
local oldGetAlternativeZoneId = ZoneDB.GetAlternativeZoneId
local oldGetParentZoneId = ZoneDB.GetParentZoneId
_G.GetQuestTagInfo = function() return nil end
QuestieDB.QueryQuestSingle = function(_, key)
if key == "zoneOrSort" then
return 4810
end
return nil
end
ZoneDB.IsDungeonZone = function(_, areaId)
return areaId == 4810
end
ZoneDB.GetAlternativeZoneId = function() return nil end
ZoneDB.GetParentZoneId = function() return nil end
local isDungeon = QuestieDB.IsDungeonQuest(12345)
_G.GetQuestTagInfo = oldGetQuestTagInfo
QuestieDB.QueryQuestSingle = oldQueryQuestSingle
ZoneDB.IsDungeonZone = oldIsDungeonZone
ZoneDB.GetAlternativeZoneId = oldGetAlternativeZoneId
ZoneDB.GetParentZoneId = oldGetParentZoneId
assert.is_true(isDungeon)
end)
it("treats dungeon quests with missing quest tags as dungeon quests when starter spawns are dungeon-only", function()
local oldGetQuestTagInfo = _G.GetQuestTagInfo
local oldQueryQuestSingle = QuestieDB.QueryQuestSingle
local oldQueryNPCSingle = QuestieDB.QueryNPCSingle
local oldQueryObjectSingle = QuestieDB.QueryObjectSingle
local oldIsDungeonZone = ZoneDB.IsDungeonZone
local oldGetAlternativeZoneId = ZoneDB.GetAlternativeZoneId
local oldGetParentZoneId = ZoneDB.GetParentZoneId
_G.GetQuestTagInfo = function() return nil end
QuestieDB.QueryQuestSingle = function(_, key)
if key == "startedBy" then
return {
{ 101 },
{ 202 },
}
end
return nil
end
QuestieDB.QueryNPCSingle = function(id, key)
if id == 101 and key == "spawns" then
return {
[4810] = { { 12.5, 34.5 } },
}
end
return nil
end
QuestieDB.QueryObjectSingle = function() return nil end
ZoneDB.IsDungeonZone = function(_, areaId)
return areaId == 4810
end
ZoneDB.GetAlternativeZoneId = function() return nil end
ZoneDB.GetParentZoneId = function() return nil end
local isDungeon = QuestieDB.IsDungeonQuest(23456)
_G.GetQuestTagInfo = oldGetQuestTagInfo
QuestieDB.QueryQuestSingle = oldQueryQuestSingle
QuestieDB.QueryNPCSingle = oldQueryNPCSingle
QuestieDB.QueryObjectSingle = oldQueryObjectSingle
ZoneDB.IsDungeonZone = oldIsDungeonZone
ZoneDB.GetAlternativeZoneId = oldGetAlternativeZoneId
ZoneDB.GetParentZoneId = oldGetParentZoneId
assert.is_true(isDungeon)
end)
it("does not treat ordinary quests as dungeon quests when the tag is missing", function()
local oldGetQuestTagInfo = _G.GetQuestTagInfo
local oldQueryQuestSingle = QuestieDB.QueryQuestSingle
local oldIsDungeonZone = ZoneDB.IsDungeonZone
local oldGetAlternativeZoneId = ZoneDB.GetAlternativeZoneId
local oldGetParentZoneId = ZoneDB.GetParentZoneId
_G.GetQuestTagInfo = function() return nil end
QuestieDB.QueryQuestSingle = function(_, key)
if key == "zoneOrSort" then
return 12
elseif key == "startedBy" then
return {
{ 301 },
{ 401 },
}
end
return nil
end
ZoneDB.IsDungeonZone = function(_, areaId)
return false
end
ZoneDB.GetAlternativeZoneId = function() return nil end
ZoneDB.GetParentZoneId = function() return nil end
local isDungeon = QuestieDB.IsDungeonQuest(34567)
_G.GetQuestTagInfo = oldGetQuestTagInfo
QuestieDB.QueryQuestSingle = oldQueryQuestSingle
ZoneDB.IsDungeonZone = oldIsDungeonZone
ZoneDB.GetAlternativeZoneId = oldGetAlternativeZoneId
ZoneDB.GetParentZoneId = oldGetParentZoneId
assert.is_false(isDungeon)
end)
end)