Files
Questie-X/Modules/Libs/QuestiePluginAPI.lua
T
Xurkon 164584ca87 fix: Sunstrider pin regression, ghost pins, and learner zone normalization
- Fix: isSunstrider block in _MergeSpawnEvidence now checks IsAscensionProtected
  before writing learner data to npcDataOverrides. Without this guard, each Mana
  Wyrm kill overwrote AscensionDB's z1241=5 data with learner z3431 coords.

- Fix: Clustering disabled for zone 1241 (Sunstrider Isle) in _DrawObjectiveIcons
  so all 5 AscensionDB spawn pins display individually instead of collapsing to 2.

- Fix: Ghost pin loop in AvailableQuests.lua -- 'while frames[i]' was iterating a
  string-keyed table with a numeric index (never iterated). Changed to pairs().

- Fix: NormalizeSpawnZoneKey now uses ZoneDB.areaIdToUiMapId for all zones so
  learner evidence is stored under map IDs (e.g. 1241) not area IDs (e.g. 3431),
  matching AscensionDB's key space. Applied in LearnNPC and _StoreGuidSpawnEvidence.

- Fix: isSunstrider detection in _MergeSpawnEvidence updated from hardcoded
  zoneId==3431 check to IsSunstriderNativeZone() since zone IDs are now normalized
  to map IDs at storage time.
2026-05-25 19:02:56 -05:00

337 lines
13 KiB
Lua

---@class QuestiePluginAPI
local QuestiePluginAPI = QuestieLoader:CreateModule("QuestiePluginAPI")
QuestiePluginAPI.registeredPlugins = {}
QuestiePluginAPI.loadedDBFlavor = nil -- set by the first DB plugin that calls FinishLoading
QuestiePluginAPI.pendingPluginsCount = 0 -- count of plugins that have registered but not yet FinishedLoading
--- Returns true if at least one DB plugin has fully loaded.
---@return boolean
function QuestiePluginAPI:IsAnyPluginLoaded()
local name, _ = next(self.registeredPlugins)
while name do
return true
end
return false
end
--- Returns true if there are plugins that have registered but not yet finished loading.
---@return boolean
function QuestiePluginAPI:HasPendingPlugins()
return self.pendingPluginsCount > 0
end
--- Returns the flavor key of the loaded DB plugin ("WotLK", "Classic", "TBC", "Turtle", "Ascension", etc.)
---@return string|nil
function QuestiePluginAPI:GetLoadedFlavor()
return self.loadedDBFlavor
end
---@class QuestiePlugin
local QuestiePlugin = {}
QuestiePlugin.__index = QuestiePlugin
local function _GetAscensionProtection(dbType)
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
QuestieDB.ascensionOverrideKeys = QuestieDB.ascensionOverrideKeys or {}
QuestieDB.ascensionOverrideKeys[dbType] = QuestieDB.ascensionOverrideKeys[dbType] or {}
return QuestieDB.ascensionOverrideKeys[dbType]
end
local function _IsAscensionProtected(dbType, id, key)
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
local protected = QuestieDB.ascensionOverrideKeys
and QuestieDB.ascensionOverrideKeys[dbType]
and QuestieDB.ascensionOverrideKeys[dbType][id]
return protected and protected[key] == true
end
local function _ProtectAscensionField(dbType, id, key)
local protectedByType = _GetAscensionProtection(dbType)
protectedByType[id] = protectedByType[id] or {}
protectedByType[id][key] = true
end
local function _MergeEntry(target, id, entry, sourceName, dbType)
if type(target) ~= "table" or type(entry) ~= "table" then return end
local existing = target[id]
if type(existing) ~= "table" then
target[id] = entry
if sourceName == "Ascension" then
local key = next(entry)
while key do
_ProtectAscensionField(dbType, id, key)
key = next(entry, key)
end
end
return
end
local key, value = next(entry)
while key do
if sourceName == "Ascension" or not _IsAscensionProtected(dbType, id, key) then
existing[key] = value
if sourceName == "Ascension" then
_ProtectAscensionField(dbType, id, key)
end
end
key, value = next(entry, key)
end
end
--- Registers a new Questie plugin
---@param pluginName string The unique name of the plugin
---@return table|nil plugin The initialized plugin object, or nil if already registered
function QuestiePluginAPI:RegisterPlugin(pluginName)
if not pluginName or type(pluginName) ~= "string" then
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestiePluginAPI] RegisterPlugin called with invalid name: " .. tostring(pluginName))
return nil
end
if self.registeredPlugins[pluginName] then
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestiePluginAPI] Plugin '" .. pluginName .. "' is already registered — duplicate RegisterPlugin call ignored.")
return nil
end
local plugin = setmetatable({
name = pluginName,
stats = {
QUEST = 0,
NPC = 0,
OBJECT = 0,
ITEM = 0
}
}, QuestiePlugin)
self.registeredPlugins[pluginName] = plugin
self.pendingPluginsCount = self.pendingPluginsCount + 1
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestiePluginAPI] Successfully registered plugin: " .. pluginName .. ". Total pending: " .. self.pendingPluginsCount)
return plugin
end
--- Retrieves a registered plugin
---@param pluginName string
---@return table|nil
function QuestiePluginAPI:GetPlugin(pluginName)
return self.registeredPlugins[pluginName]
end
---------------------------------------------------------------------------------------------------
-- Plugin Object Methods
---------------------------------------------------------------------------------------------------
--- Safely injects data into the core QuestieDB overrides tables
---@param dbType string "QUEST", "NPC", "OBJECT", or "ITEM"
---@param data table The database to inject
function QuestiePlugin:InjectDatabase(dbType, data)
if type(data) ~= "table" then return end
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
-- Ensure overrides tables exist
QuestieDB.npcDataOverrides = QuestieDB.npcDataOverrides or {}
QuestieDB.objectDataOverrides = QuestieDB.objectDataOverrides or {}
QuestieDB.itemDataOverrides = QuestieDB.itemDataOverrides or {}
QuestieDB.questDataOverrides = QuestieDB.questDataOverrides or {}
local count = 0
local targetOverride
if dbType == "QUEST" then
targetOverride = QuestieDB.questDataOverrides
elseif dbType == "NPC" then
targetOverride = QuestieDB.npcDataOverrides
elseif dbType == "OBJECT" then
targetOverride = QuestieDB.objectDataOverrides
elseif dbType == "ITEM" then
targetOverride = QuestieDB.itemDataOverrides
else
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestiePluginAPI] Plugin '" .. self.name .. "' passed unknown DB type to InjectDatabase: '" .. tostring(dbType) .. "'. Expected QUEST, NPC, OBJECT, or ITEM.")
return
end
local qid, entry = next(data)
while qid do
_MergeEntry(targetOverride, qid, entry, self.name, dbType)
if type(qid) == "number" then
count = count + 1
end
qid, entry = next(data, qid)
end
self.stats[dbType] = (self.stats[dbType] or 0) + count
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestiePluginAPI] Plugin '" .. self.name .. "' injected " .. tostring(count) .. " " .. dbType .. " records.")
end
--- Safely injects zone, dungeon, and map routing data into ZoneDB
function QuestiePlugin:InjectZoneTables(customZoneTables)
if type(customZoneTables) ~= "table" then return end
local ZoneDB = QuestieLoader:ImportModule("ZoneDB")
if not ZoneDB then return end
ZoneDB.private = ZoneDB.private or {}
ZoneDB.private.uiMapIdToAreaId = ZoneDB.private.uiMapIdToAreaId or {}
ZoneDB.private.areaIdToUiMapId = ZoneDB.private.areaIdToUiMapId or {}
ZoneDB.private.subZoneToParentZone = ZoneDB.private.subZoneToParentZone or {}
ZoneDB.private.dungeons = ZoneDB.private.dungeons or {}
ZoneDB.private.dungeonLocations = ZoneDB.private.dungeonLocations or {}
ZoneDB.private.dungeonParentZones = ZoneDB.private.dungeonParentZones or {}
if customZoneTables.uiMapIdToAreaId then
local uiMapId, areaId = next(customZoneTables.uiMapIdToAreaId)
while uiMapId do
if uiMapId and areaId then
ZoneDB.private.uiMapIdToAreaId[uiMapId] = areaId
ZoneDB.private.areaIdToUiMapId[areaId] = uiMapId
if type(ZoneDB.private.dungeons) == "table" and ZoneDB.private.dungeons[areaId] then
ZoneDB.private.areaIdToUiMapId[areaId] = uiMapId
end
if uiMapId ~= areaId then
ZoneDB.private.subZoneToParentZone[uiMapId] = areaId
ZoneDB.private.areaIdToUiMapId[uiMapId] = uiMapId
end
end
uiMapId, areaId = next(customZoneTables.uiMapIdToAreaId, uiMapId)
end
end
if type(customZoneTables.dungeons) == "table" then
local areaId, data = next(customZoneTables.dungeons)
while areaId do
if areaId and data then
ZoneDB.private.dungeons[areaId] = data
end
areaId, data = next(customZoneTables.dungeons, areaId)
end
end
if type(customZoneTables.dungeonLocations) == "table" then
local areaId, data = next(customZoneTables.dungeonLocations)
while areaId do
if areaId and data then
ZoneDB.private.dungeonLocations[areaId] = data
end
areaId, data = next(customZoneTables.dungeonLocations, areaId)
end
end
if type(customZoneTables.dungeonParentZones) == "table" then
local subZoneId, parentZoneId = next(customZoneTables.dungeonParentZones)
while subZoneId do
if subZoneId and parentZoneId then
ZoneDB.private.dungeonParentZones[subZoneId] = parentZoneId
end
subZoneId, parentZoneId = next(customZoneTables.dungeonParentZones, subZoneId)
end
end
if customZoneTables.zoneSort then
ZoneDB.private.zoneSort = ZoneDB.private.zoneSort or {}
local zoneId, zoneName = next(customZoneTables.zoneSort)
while zoneId do
ZoneDB.private.zoneSort[zoneId] = zoneName
zoneId, zoneName = next(customZoneTables.zoneSort, zoneId)
end
end
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestiePluginAPI] Plugin '" .. self.name .. "' injected Custom Zone Tables.")
end
--- Safely injects fallback UiMapData for boundary maps
function QuestiePlugin:InjectUiMapData(customUiMapData)
if type(customUiMapData) ~= "table" or not customUiMapData.uiMapData then return end
local ZoneDB = QuestieLoader:ImportModule("ZoneDB")
if not ZoneDB then return end
ZoneDB.private = ZoneDB.private or {}
ZoneDB.private.areaIdToUiMapId = ZoneDB.private.areaIdToUiMapId or {}
QuestieCompat.UiMapData = QuestieCompat.UiMapData or {}
local count = 0
local uiMapId, data = next(customUiMapData.uiMapData)
while uiMapId do
if uiMapId and ZoneDB.private.areaIdToUiMapId[uiMapId] == nil then
ZoneDB.private.areaIdToUiMapId[uiMapId] = uiMapId
end
if data and type(data.parentMapID) == "number" and ZoneDB.private.areaIdToUiMapId[data.parentMapID] == nil then
ZoneDB.private.areaIdToUiMapId[data.parentMapID] = uiMapId
end
if uiMapId and QuestieCompat.UiMapData[uiMapId] == nil then
QuestieCompat.UiMapData[uiMapId] = data
count = count + 1
end
uiMapId, data = next(customUiMapData.uiMapData, uiMapId)
end
ZoneDB:ApplyCustomZones()
print("[QuestiePluginAPI] Plugin '" .. tostring(self.name) .. "' injected " .. count .. " custom UI Map entries. ZoneDB areaIdToUiMapId[1244]=" .. tostring(ZoneDB.private.areaIdToUiMapId[1244]))
end
--- Signals that the plugin has finished loading. This automatically cleans up necessary caches.
---@param flavorKey string|nil Optional flavor label e.g. "WotLK", "Classic", "Turtle"
function QuestiePlugin:FinishLoading(flavorKey)
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
if not QuestieDB then
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestiePluginAPI] Plugin '" .. self.name .. "' FinishLoading failed: QuestieDB module not found.")
return
end
if QuestieDB.private and QuestieDB.private.zoneCache then
QuestieDB.private.zoneCache = {}
end
if QuestieDB.autoBlacklist then
QuestieDB.autoBlacklist = {}
end
if flavorKey then
QuestiePluginAPI.loadedDBFlavor = flavorKey
elseif not QuestiePluginAPI.loadedDBFlavor then
QuestiePluginAPI.loadedDBFlavor = self.name
end
if not self.isFinished then
self.isFinished = true
QuestiePluginAPI.pendingPluginsCount = math.max(0, QuestiePluginAPI.pendingPluginsCount - 1)
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestiePluginAPI] Plugin '" .. self.name .. "' set as FINISHED. Remaining pending: " .. QuestiePluginAPI.pendingPluginsCount)
end
Questie:Debug(Questie.DEBUG_INFO, "[QuestiePluginAPI] Plugin '" .. self.name .. "' finished loading successfully.")
end
--- Injects XP-per-level data used by QuestieXP. The table must be keyed by level number.
---@param xpTable table { [level] = xp_required, ... }
function QuestiePlugin:InjectXpData(xpTable)
if type(xpTable) ~= "table" then return end
local QuestXP = QuestieLoader:ImportModule("QuestieXP")
if not QuestXP then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestiePluginAPI] Plugin '" .. self.name .. "': InjectXpData — QuestieXP module not loaded, skipping.")
return
end
QuestXP.db = xpTable
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestiePluginAPI] Plugin '" .. self.name .. "' injected XP data (" .. tostring(#xpTable) .. " levels).")
end
--- Applies expansion-specific correction tables that are stored in QuestieDB
--- correction globals (set by correction files loaded by the plugin TOC).
--- Call this AFTER InjectDatabase calls and BEFORE FinishLoading.
function QuestiePlugin:InjectCorrections()
local QuestieCorrections = QuestieLoader:ImportModule("QuestieCorrections")
if not QuestieCorrections then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestiePluginAPI] Plugin '" .. self.name .. "': InjectCorrections — QuestieCorrections module not loaded, skipping.")
return
end
if QuestieCorrections.ApplyAll then
QuestieCorrections:ApplyAll()
end
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestiePluginAPI] Plugin '" .. self.name .. "' applied corrections.")
end