324 lines
13 KiB
Lua
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
|