---@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