Files
Questie-X/Database/Ebonhold/EbonholdLoader.lua
T

324 lines
13 KiB
Lua

---@class EbonholdLoader
---@type table
local EbonholdLoader = QuestieLoader:CreateModule("EbonholdLoader")
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
---@type table
local EbonholdDB = QuestieLoader:ImportModule("EbonholdDB")
_G.EbonholdDB = _G.EbonholdDB or EbonholdDB
local _overridesInjected = false
local _zonesApplied = false
local function _LoadIfString(data, label)
if not data then return nil end
if type(data) == "string" then
local fn, err = loadstring(data)
if not fn then
if Questie and Questie.Debug then
Questie:Debug(Questie.DEBUG_CRITICAL,
"[EbonholdLoader] loadstring failed for " .. tostring(label) .. ": " .. tostring(err))
end
return nil
end
local ok, tbl = pcall(fn)
if not ok then
if Questie and Questie.Debug then
Questie:Debug(Questie.DEBUG_CRITICAL,
"[EbonholdLoader] executing chunk failed for " .. tostring(label) .. ": " .. tostring(tbl))
end
return nil
end
return tbl
end
return data
end
local function _MergeInto(dst, src)
if type(dst) ~= "table" or type(src) ~= "table" then return end
for id, entry in pairs(src) do
dst[id] = entry
end
end
-- Apply Ebonhold custom uiMapId->areaId mappings into ZoneDB (used by map/Journey)
function EbonholdLoader:ApplyZoneTables()
if _zonesApplied then return end
if not EbonholdZoneTables or not EbonholdZoneTables.uiMapIdToAreaId 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 {}
for uiMapId, areaId in pairs(EbonholdZoneTables.uiMapIdToAreaId) do
if uiMapId and areaId then
-- Always set these mappings (remove the nil check to ensure they're set)
ZoneDB.private.uiMapIdToAreaId[uiMapId] = areaId
-- Ebonhold uses custom map ids for spawns/waypoints (e.g. 1238 for Northshire Valley).
-- QuestieMap draws by converting AreaId->UiMapId, so register these ids as self-mapping.
ZoneDB.private.areaIdToUiMapId[uiMapId] = uiMapId
-- If this uiMapId represents a *dungeon* map, prefer it for that dungeon's AreaId.
-- This keeps normal zones intact (e.g. Elwynn stays 1429), while letting Ebonhold
-- provide real instance maps (e.g. mapID 691 for The Stockade).
if type(ZoneDB.private.dungeons) == "table" and ZoneDB.private.dungeons[areaId] then
ZoneDB.private.areaIdToUiMapId[areaId] = uiMapId
end
-- Register subzone to parent zone mapping so GetParentZoneId works with Ebonhold zones
if uiMapId ~= areaId then
ZoneDB.private.subZoneToParentZone[uiMapId] = areaId
end
end
end
-- Optional: allow Ebonhold to define custom dungeon zones/entrances without touching core tables.
if type(EbonholdZoneTables.dungeons) == "table" then
for areaId, data in pairs(EbonholdZoneTables.dungeons) do
if areaId and data then
ZoneDB.private.dungeons[areaId] = data
end
end
end
if type(EbonholdZoneTables.dungeonLocations) == "table" then
for areaId, data in pairs(EbonholdZoneTables.dungeonLocations) do
if areaId and data then
ZoneDB.private.dungeonLocations[areaId] = data
end
end
end
if type(EbonholdZoneTables.dungeonParentZones) == "table" then
for subZoneId, parentZoneId in pairs(EbonholdZoneTables.dungeonParentZones) do
if subZoneId and parentZoneId then
ZoneDB.private.dungeonParentZones[subZoneId] = parentZoneId
end
end
end
-- Also map parentMapIDs (e.g. 10138) to a usable uiMapId to avoid fallback errors.
if EbonholdUiMapData and EbonholdUiMapData.uiMapData then
for uiMapId, data in pairs(EbonholdUiMapData.uiMapData) 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
end
end
-- Register zone sort names for custom zones
if EbonholdZoneTables.zoneSort then
ZoneDB.private.zoneSort = ZoneDB.private.zoneSort or {}
for zoneId, zoneName in pairs(EbonholdZoneTables.zoneSort) do
ZoneDB.private.zoneSort[zoneId] = zoneName
end
end
-- Clear cached zone lookups after adding custom zones
if QuestieDB and QuestieDB.private and QuestieDB.private.zoneCache then
QuestieDB.private.zoneCache = {}
end
-- Clear auto-blacklist since zone validation might have changed
if QuestieDB and QuestieDB.autoBlacklist then
QuestieDB.autoBlacklist = {}
end
_zonesApplied = true
end
-- Inject Ebonhold tables into QuestieDB *Overrides* so QueryQuestSingle/QueryNPCSingle can see them
function EbonholdLoader:InjectOverrides()
if _overridesInjected then return end
_overridesInjected = true
-- Apply zone tables FIRST, before any ZoneDB functions use the local variables
EbonholdLoader:ApplyZoneTables()
QuestieDB.npcDataOverrides = QuestieDB.npcDataOverrides or {}
QuestieDB.objectDataOverrides = QuestieDB.objectDataOverrides or {}
QuestieDB.itemDataOverrides = QuestieDB.itemDataOverrides or {}
QuestieDB.questDataOverrides = QuestieDB.questDataOverrides or {}
local npcData = _LoadIfString(EbonholdDB and EbonholdDB.npcData, "EbonholdDB.npcData")
local objectData = _LoadIfString(EbonholdDB and EbonholdDB.objectData, "EbonholdDB.objectData")
local itemData = _LoadIfString(EbonholdDB and EbonholdDB.itemData, "EbonholdDB.itemData")
local questData = _LoadIfString(EbonholdDB and EbonholdDB.questData, "EbonholdDB.questData")
_MergeInto(QuestieDB.npcDataOverrides, npcData)
_MergeInto(QuestieDB.objectDataOverrides, objectData)
_MergeInto(QuestieDB.itemDataOverrides, itemData)
_MergeInto(QuestieDB.questDataOverrides, questData)
-- Keep a lightweight list of custom quest ids for search/UI (DO NOT touch QuestPointers; those are numeric stream pointers)
if type(questData) == "table" then
QuestieDB.ebonholdQuestIds = QuestieDB.ebonholdQuestIds or {}
for questId, _ in pairs(questData) do
if type(questId) == "number" then
QuestieDB.ebonholdQuestIds[questId] = true
end
end
end
-- Keep a lightweight list of custom NPC ids for search/UI
if type(npcData) == "table" then
QuestieDB.ebonholdNpcIds = QuestieDB.ebonholdNpcIds or {}
for npcId, _ in pairs(npcData) do
if type(npcId) == "number" then
QuestieDB.ebonholdNpcIds[npcId] = true
end
end
end
-- Keep a lightweight list of custom object ids for search/UI
if type(objectData) == "table" then
QuestieDB.ebonholdObjectIds = QuestieDB.ebonholdObjectIds or {}
for objectId, _ in pairs(objectData) do
if type(objectId) == "number" then
QuestieDB.ebonholdObjectIds[objectId] = true
end
end
end
-- Keep a lightweight list of custom item ids for search/UI
if type(itemData) == "table" then
QuestieDB.ebonholdItemIds = QuestieDB.ebonholdItemIds or {}
for itemId, _ in pairs(itemData) do
if type(itemId) == "number" then
QuestieDB.ebonholdItemIds[itemId] = true
end
end
end
end
-- Hook ZoneDB.Initialize to inject custom zones BEFORE standard zones are processed
do
local ZoneDB = QuestieLoader:ImportModule("ZoneDB")
if ZoneDB then
local originalZoneInitialize = ZoneDB.Initialize
if type(originalZoneInitialize) == "function" then
function ZoneDB:Initialize(...)
-- Apply custom zones FIRST, before standard zone processing
EbonholdLoader:ApplyZoneTables()
-- Then initialize standard zones
return originalZoneInitialize(self, ...)
end
end
end
end
-- Hook QuestieDB.Initialize to guarantee timing for overrides
do
local originalInitialize = QuestieDB and QuestieDB.Initialize
if type(originalInitialize) == "function" then
function QuestieDB:Initialize(...)
-- 1) Inject overrides before the DB handles are created
EbonholdLoader:InjectOverrides()
local ret = originalInitialize(self, ...)
-- 2) Refresh Journey caches (zoneMap/zones) so "Quests by Zone" sees the new data
local ZoneDB = QuestieLoader:ImportModule("ZoneDB")
local QuestieJourney = QuestieLoader:ImportModule("QuestieJourney")
if ZoneDB and QuestieJourney then
QuestieJourney.zoneMap = ZoneDB:GetZonesWithQuests(true)
QuestieJourney.zones = ZoneDB:GetRelevantZones()
-- Clear the UI cache so the tree gets rebuilt
if QuestieJourney.private then
QuestieJourney.private.treeCache = nil
QuestieJourney.private.lastZoneSelection = {}
end
-- If the Journey window is open, rebuild the current tab
if QuestieJourneyFrame and QuestieJourneyFrame:IsShown() and QuestieJourney.tabGroup then
local p = QuestieJourney.private
if p and p.containerCache and p.lastOpenWindow then
p:HandleTabChange(p.containerCache, p.lastOpenWindow)
end
end
end
return ret
end
end
end
-- >>> NEW: Make QuestieDB.GetQuest work for Ebonhold override quests (Journey needs this)
do
local originalGetQuest = QuestieDB.GetQuest
if type(originalGetQuest) == "function" then
function QuestieDB.GetQuest(questId)
local q = originalGetQuest(questId)
if q then return q end
-- Fallback: build a minimal quest object from QueryQuestSingle (works with overrides)
local name = QuestieDB.QueryQuestSingle(questId, "name")
if not name then return nil end
local level = QuestieDB.QueryQuestSingle(questId, "level") or
QuestieDB.QueryQuestSingle(questId, "questLevel")
local reqLevel = QuestieDB.QueryQuestSingle(questId, "requiredLevel") or
QuestieDB.QueryQuestSingle(questId, "minLevel")
local desc = QuestieDB.QueryQuestSingle(questId, "objectiveText") or
QuestieDB.QueryQuestSingle(questId, "description") or QuestieDB.QueryQuestSingle(questId, "details")
-- Build the Starts field from startedBy (CRITICAL for available quests to show icons!)
local startedBy = QuestieDB.QueryQuestSingle(questId, "startedBy")
local starts = {
NPC = startedBy and startedBy[1] or {},
GameObject = startedBy and startedBy[2] or {},
Item = startedBy and startedBy[3] or {},
}
-- Also get other fields needed for quest display
local finishedBy = QuestieDB.QueryQuestSingle(questId, "finishedBy")
local specialFlags = QuestieDB.QueryQuestSingle(questId, "specialFlags")
local isRepeatable = specialFlags and (bit.band(specialFlags, 1) ~= 0) or false
-- Questie modules expect these common fields
return {
Id = questId,
id = questId,
name = name,
level = level or reqLevel or 0,
requiredLevel = reqLevel or 0,
Description = desc, -- Journey tooltip reads quest.Description
Starts = starts, -- CRITICAL: AvailableQuests needs this!
finishedBy = finishedBy,
IsRepeatable = isRepeatable,
}
end
end
end
-- Ensure zones also get applied when Questie finishes loading (covers edge cases)
do
local f = CreateFrame("Frame")
f:RegisterEvent("ADDON_LOADED")
f:SetScript("OnEvent", function(_, _, name)
if name ~= "Questie-335" then return end
if C_Timer and C_Timer.After then
C_Timer.After(0, function() EbonholdLoader:ApplyZoneTables() end)
else
EbonholdLoader:ApplyZoneTables()
end
end)
end
return EbonholdLoader