011cdb58a9
Mana Wyrm 15274 (quest 8325) pins rendered in the NE corner because the
learner's Sunstrider spawns leaked into the curated AscensionDB coords.
Root causes:
- In learner mode GetNPC uses rawdata = learner record, and _MergeOverride
deep-merges spawn tables, so the learner's bad zone (1445 @ 54,12)
survived since AscensionDB had no entry to displace it.
- The injection guards used IsAscensionProtected(), which returns false in
learner mode, so the learner overrode AscensionDB-owned spawns. Commit
0f20ea8 had also added a (not learnerLiveMode) bypass to the
_MergeSpawnEvidence Sunstrider guard, re-enabling the e80a008 regression.
Fixes:
- QuestieDB.GetNPC: for AscensionDB-owned NPC spawns (direct
ascensionOverrideKeys check), use ONLY the curated override spawns and
discard the merged learner zones.
- New mode-independent AscensionOwnsNpcSpawns() helper; used by the
InjectLearnedData spawn restore and the _MergeSpawnEvidence guard
(removed the (not learnerLiveMode) bypass).
Regression test asserts GetNPC returns only the curated 1241 coords and
drops the learner 1445 zone.
2652 lines
104 KiB
Lua
2652 lines
104 KiB
Lua
---@class QuestieDB
|
|
local QuestieDB = QuestieLoader:CreateModule("QuestieDB")
|
|
|
|
---@type QuestieDBPrivate
|
|
QuestieDB.private = QuestieDB.private or {}
|
|
|
|
---@class QuestieDBPrivate
|
|
local _QuestieDB = QuestieDB.private
|
|
|
|
------------------------------------------------------------------------
|
|
-- _MergeOverride: Merge override data into a result table.
|
|
-- Override tables from QuestieLearner and AscensionDB use numeric keys
|
|
-- ([1]=name, [7]=spawns, ...) while the result uses string keys
|
|
-- ("name", "spawns", ...). This helper checks both formats:
|
|
-- 1. override[stringKey] (string-keyed, e.g. from wotlkNPCFixes)
|
|
-- 2. override[intKey] (numeric-keyed, e.g. from QuestieLearner / AscensionDB)
|
|
-- 3. rawdata[intKey] (fallback to compiled DB)
|
|
--
|
|
-- Empty tables from QuestieLearner (e.g. spawns={}) are treated as "not present"
|
|
-- so the base DB's real spawn data is preserved until actual coordinates are learned.
|
|
------------------------------------------------------------------------
|
|
local function IsEmptyTable(val)
|
|
return type(val) == "table" and next(val) == nil
|
|
end
|
|
|
|
local function CopySpawnTable(spawns)
|
|
if type(spawns) ~= "table" then
|
|
return spawns
|
|
end
|
|
|
|
local copied = {}
|
|
for zoneId, coords in pairs(spawns) do
|
|
local zoneCoords = {}
|
|
for _, coord in ipairs(coords or {}) do
|
|
if type(coord) == "table" then
|
|
zoneCoords[#zoneCoords + 1] = { coord[1], coord[2] }
|
|
end
|
|
end
|
|
copied[zoneId] = zoneCoords
|
|
end
|
|
return copied
|
|
end
|
|
|
|
local function MergeSpawnTables(baseSpawns, overrideSpawns)
|
|
if type(baseSpawns) ~= "table" then
|
|
return CopySpawnTable(overrideSpawns)
|
|
end
|
|
if type(overrideSpawns) ~= "table" then
|
|
return CopySpawnTable(baseSpawns)
|
|
end
|
|
|
|
local merged = CopySpawnTable(baseSpawns)
|
|
for zoneId, coords in pairs(overrideSpawns) do
|
|
merged[zoneId] = merged[zoneId] or {}
|
|
for _, coord in ipairs(coords or {}) do
|
|
local exists = false
|
|
for _, existing in ipairs(merged[zoneId]) do
|
|
if existing[1] == coord[1] and existing[2] == coord[2] then
|
|
exists = true
|
|
break
|
|
end
|
|
end
|
|
if not exists then
|
|
merged[zoneId][#merged[zoneId] + 1] = { coord[1], coord[2] }
|
|
end
|
|
end
|
|
end
|
|
return merged
|
|
end
|
|
|
|
local function GetSpawnTable(entry, primaryKey, legacyKey)
|
|
if type(entry) ~= "table" then
|
|
return nil
|
|
end
|
|
|
|
local spawns = entry[primaryKey]
|
|
if type(spawns) == "table" then
|
|
return spawns
|
|
end
|
|
|
|
spawns = entry[legacyKey]
|
|
if type(spawns) == "table" then
|
|
return spawns
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function NormalizeLearnerSpawnEntry(entry, primaryKey, legacyKey)
|
|
if type(entry) ~= "table" then
|
|
return false
|
|
end
|
|
|
|
local changed = false
|
|
local currentSpawns = entry[primaryKey]
|
|
local legacySpawns = entry[legacyKey]
|
|
|
|
if currentSpawns ~= nil and type(currentSpawns) ~= "table" then
|
|
entry[primaryKey] = nil
|
|
currentSpawns = nil
|
|
changed = true
|
|
end
|
|
|
|
if legacySpawns ~= nil and type(legacySpawns) ~= "table" then
|
|
entry[legacyKey] = nil
|
|
legacySpawns = nil
|
|
changed = true
|
|
end
|
|
|
|
if type(currentSpawns) ~= "table" and type(legacySpawns) == "table" then
|
|
entry[primaryKey] = CopySpawnTable(legacySpawns)
|
|
entry[legacyKey] = nil
|
|
return true
|
|
end
|
|
|
|
if type(currentSpawns) == "table" and type(legacySpawns) == "table" then
|
|
entry[primaryKey] = MergeSpawnTables(currentSpawns, legacySpawns)
|
|
entry[legacyKey] = nil
|
|
return true
|
|
end
|
|
|
|
if type(currentSpawns) == "table" and legacySpawns ~= nil then
|
|
entry[legacyKey] = nil
|
|
changed = true
|
|
end
|
|
|
|
return changed
|
|
end
|
|
|
|
QuestieDB.private.GetLearnerSpawnTable = GetSpawnTable
|
|
QuestieDB.private.NormalizeLearnerSpawnEntry = NormalizeLearnerSpawnEntry
|
|
QuestieDB.private.missingQuestLog = QuestieDB.private.missingQuestLog or {}
|
|
QuestieDB.private.missingNpcLog = QuestieDB.private.missingNpcLog or {}
|
|
QuestieDB.private.missingObjectLog = QuestieDB.private.missingObjectLog or {}
|
|
QuestieDB.private.missingItemLog = QuestieDB.private.missingItemLog or {}
|
|
|
|
local function _MergeOverride(result, override, rawdata, keyMap)
|
|
for stringKey, intKey in pairs(keyMap) do
|
|
local overrideValue = override[stringKey]
|
|
if overrideValue == nil then
|
|
overrideValue = override[intKey]
|
|
end
|
|
|
|
if stringKey == "spawns" then
|
|
if overrideValue ~= nil and not IsEmptyTable(overrideValue) then
|
|
result[stringKey] = MergeSpawnTables(rawdata and rawdata[intKey], overrideValue)
|
|
elseif rawdata then
|
|
result[stringKey] = CopySpawnTable(rawdata[intKey])
|
|
end
|
|
elseif override[stringKey] ~= nil then
|
|
result[stringKey] = override[stringKey]
|
|
elseif override[intKey] ~= nil and not IsEmptyTable(override[intKey]) then
|
|
result[stringKey] = override[intKey]
|
|
elseif rawdata then
|
|
result[stringKey] = rawdata[intKey]
|
|
end
|
|
end
|
|
end
|
|
|
|
-------------------------
|
|
--Import modules.
|
|
-------------------------
|
|
---@type QuestieLib
|
|
local QuestieLib = QuestieLoader:ImportModule("QuestieLib")
|
|
---@type QuestiePlayer
|
|
local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer")
|
|
---@type QuestieCorrections
|
|
local QuestieCorrections = QuestieLoader:ImportModule("QuestieCorrections")
|
|
---@type QuestieProfessions
|
|
local QuestieProfessions = QuestieLoader:ImportModule("QuestieProfessions")
|
|
---@type DailyQuests
|
|
local DailyQuests = QuestieLoader:ImportModule("DailyQuests")
|
|
---@type QuestieReputation
|
|
local QuestieReputation = QuestieLoader:ImportModule("QuestieReputation")
|
|
---@type QuestieEvent
|
|
local QuestieEvent = QuestieLoader:ImportModule("QuestieEvent")
|
|
---@type DBCompiler
|
|
local QuestieDBCompiler = QuestieLoader:ImportModule("DBCompiler")
|
|
---@type ZoneDB
|
|
local ZoneDB = QuestieLoader:ImportModule("ZoneDB")
|
|
---@type l10n
|
|
local l10n = QuestieLoader:ImportModule("l10n")
|
|
---@type QuestLogCache
|
|
local QuestLogCache = QuestieLoader:ImportModule("QuestLogCache")
|
|
|
|
-- Dummy handles to prevent Stage 2 crashes during deferred initialization
|
|
local _dummyHandle = {
|
|
QuerySingle = function() return nil end,
|
|
Query = function() return nil end,
|
|
pointers = {}
|
|
}
|
|
QuestieDB.QueryNPC = _dummyHandle.Query
|
|
QuestieDB.QueryQuest = _dummyHandle.Query
|
|
QuestieDB.QueryObject = _dummyHandle.Query
|
|
QuestieDB.QueryItem = _dummyHandle.Query
|
|
QuestieDB.QueryQuestSingle = _dummyHandle.QuerySingle
|
|
QuestieDB.QueryNPCSingle = _dummyHandle.QuerySingle
|
|
QuestieDB.QueryObjectSingle = _dummyHandle.QuerySingle
|
|
QuestieDB.QueryItemSingle = _dummyHandle.QuerySingle
|
|
QuestieDB._QueryQuestSingle = _dummyHandle.QuerySingle
|
|
QuestieDB._QueryNPCSingle = _dummyHandle.QuerySingle
|
|
QuestieDB._QueryObjectSingle = _dummyHandle.QuerySingle
|
|
QuestieDB._QueryItemSingle = _dummyHandle.QuerySingle
|
|
|
|
-- Initialize pointers to empty tables to prevent Stage 2 crashes during deferred initialization
|
|
QuestieDB.QuestPointers = _dummyHandle.pointers
|
|
QuestieDB.NPCPointers = _dummyHandle.pointers
|
|
QuestieDB.ObjectPointers = _dummyHandle.pointers
|
|
QuestieDB.ItemPointers = _dummyHandle.pointers
|
|
|
|
-- Set by QuestieInit when the compiled/static base DB fails to load.
|
|
-- When this is true, learner should force itself on so the addon still works
|
|
-- even if the static database is unavailable or partially missing.
|
|
QuestieDB.baseDatabaseMissing = false
|
|
QuestieDB.baseDatabaseMissingKeys = {}
|
|
|
|
-- The four core static stores. The base DB is only considered fully "missing"
|
|
-- (which forces learner mode) when EVERY one of these failed to load.
|
|
QuestieDB._baseDatabaseStores = { "npcData", "objectData", "questData", "itemData" }
|
|
|
|
-- True only when a specific static store failed to load. Reads use this so a
|
|
-- single missing/mismatched store falls back to learner data for that store
|
|
-- alone, instead of locking the whole addon into learner mode.
|
|
function QuestieDB:IsStoreMissing(storeKey)
|
|
return QuestieDB.baseDatabaseMissingKeys and QuestieDB.baseDatabaseMissingKeys[storeKey] == true
|
|
end
|
|
|
|
-- Clears every per-entity cache. Used when the data source mode changes so that
|
|
-- quest/npc/item/object AND per-zone results are rebuilt against the new mode.
|
|
function QuestieDB:ClearModeCaches()
|
|
_QuestieDB.questCache = {}
|
|
_QuestieDB.itemCache = {}
|
|
_QuestieDB.npcCache = {}
|
|
_QuestieDB.objectCache = {}
|
|
_QuestieDB.zoneCache = {}
|
|
end
|
|
|
|
function QuestieDB:IsBaseDatabaseMissing()
|
|
if QuestieDB.baseDatabaseMissing ~= true then
|
|
return false
|
|
end
|
|
-- Only report the base DB as missing when EVERY core store failed. A partial
|
|
-- failure (e.g. itemData missing but npcData present) must not override the
|
|
-- user's Data Source Mode selection — per-read fallback handles the gaps.
|
|
local keys = QuestieDB.baseDatabaseMissingKeys
|
|
if not keys then
|
|
return false
|
|
end
|
|
local stores = QuestieDB._baseDatabaseStores
|
|
for i = 1, 4 do
|
|
if not keys[stores[i]] then
|
|
return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function _GetLearnerSettings()
|
|
local ld = Questie and Questie.dbLearner and Questie.dbLearner.global
|
|
return ld and ld.settings or nil
|
|
end
|
|
|
|
local function _GetLearnerRecord(storeName, id)
|
|
local ld = Questie and Questie.dbLearner and Questie.dbLearner.global
|
|
if not ld then
|
|
return nil
|
|
end
|
|
|
|
local store = ld[storeName]
|
|
if not store then
|
|
return nil
|
|
end
|
|
|
|
return store[id] or store[tostring(id)]
|
|
end
|
|
|
|
-- Map-percent radius used to collapse per-GUID kill evidence into one pin per
|
|
-- physical spawn. Kill coordinates are the player's position at kill time, so
|
|
-- repeated kills (and respawns, which carry fresh GUIDs) land on slightly
|
|
-- different coords. Without this merge, every kill would render its own pin.
|
|
-- A distance test (rather than a grid bucket) avoids the boundary artifact where
|
|
-- two near-identical coords straddle a cell edge and split into separate pins.
|
|
-- The radius is user-tunable via the learner "Spawn Pin Dedup Radius" knob; 0
|
|
-- disables merging (every distinct coord shown).
|
|
local GUID_SPAWN_DEDUP_RADIUS = 4.0
|
|
|
|
local function _GetSpawnDedupRadius()
|
|
local settings = _GetLearnerSettings()
|
|
local r = settings and tonumber(settings.spawnDedupRadius)
|
|
if r and r >= 0 then
|
|
return r
|
|
end
|
|
return GUID_SPAWN_DEDUP_RADIUS
|
|
end
|
|
|
|
local function _BuildSpawnTableFromGuidEvidence(evidence)
|
|
if type(evidence) ~= "table" then
|
|
return nil
|
|
end
|
|
|
|
local radius = _GetSpawnDedupRadius()
|
|
local radiusSq = radius * radius
|
|
local spawns = {}
|
|
local hasEntries = false
|
|
for _, entry in pairs(evidence) do
|
|
if type(entry) == "table" and entry.zoneId and entry.x and entry.y then
|
|
local zoneId = tonumber(entry.zoneId)
|
|
local x = tonumber(entry.x)
|
|
local y = tonumber(entry.y)
|
|
if zoneId and x and y then
|
|
spawns[zoneId] = spawns[zoneId] or {}
|
|
local zoneSpawns = spawns[zoneId]
|
|
local exists = false
|
|
for _, coord in ipairs(zoneSpawns) do
|
|
local dx = coord[1] - x
|
|
local dy = coord[2] - y
|
|
if (dx * dx + dy * dy) <= radiusSq then
|
|
exists = true
|
|
break
|
|
end
|
|
end
|
|
if not exists then
|
|
zoneSpawns[#zoneSpawns + 1] = { x, y }
|
|
hasEntries = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if not hasEntries then
|
|
return nil
|
|
end
|
|
|
|
return spawns
|
|
end
|
|
|
|
---@type QuestieQuest
|
|
local QuestieQuest = QuestieLoader:ImportModule("QuestieQuest")
|
|
---@type QuestieQuestPrivate
|
|
local _QuestieQuest = QuestieQuest.private
|
|
|
|
--- A list of quests that will never be available, used to quickly skip quests.
|
|
---@alias AutoBlacklistString "rep"|"skill"|"race"|"class"
|
|
---@type table<number, AutoBlacklistString>
|
|
QuestieDB.autoBlacklist = {}
|
|
|
|
local tinsert = table.insert
|
|
local bitband = bit.band
|
|
|
|
-- questFlags https://github.com/cmangos/issues/wiki/Quest_template#questflags
|
|
local QUEST_FLAGS_DAILY = 4096
|
|
local QUEST_FLAGS_WEEKLY = 32768
|
|
-- Pre calculated 2 * QUEST_FLAGS, for testing a bit flag
|
|
local QUEST_FLAGS_DAILY_X2 = 2 * QUEST_FLAGS_DAILY
|
|
local QUEST_FLAGS_WEEKLY_X2 = 2 * QUEST_FLAGS_WEEKLY
|
|
--- COMPATIBILITY ---
|
|
local WOW_PROJECT_ID = QuestieCompat.WOW_PROJECT_ID
|
|
local WOW_PROJECT_CLASSIC = QuestieCompat.WOW_PROJECT_CLASSIC
|
|
local C_QuestLog = QuestieCompat.C_QuestLog
|
|
local GetQuestTagInfo = QuestieCompat.GetQuestTagInfo
|
|
local IsPlayerSpell = QuestieCompat.IsPlayerSpell
|
|
local IsSpellKnownOrOverridesKnown = QuestieCompat.IsSpellKnownOrOverridesKnown
|
|
local IsQuestFlaggedCompleted = QuestieCompat.IsQuestFlaggedCompleted or C_QuestLog.IsQuestFlaggedCompleted
|
|
|
|
--- Tag corrections for quests for which the API returns the wrong values.
|
|
--- Strucute: [questId] = {tagId, "questType"}
|
|
---@type table<number, {[1]: number, [2]: string}>
|
|
local questTagCorrections = {
|
|
[373] = {81, "Dungeon"},
|
|
[4146] = {81, "Dungeon"},
|
|
[5342] = {0, ""},
|
|
[5344] = {0, ""},
|
|
[6846] = {41, "PvP"},
|
|
[6901] = {41, "PvP"},
|
|
[7001] = {41, "PvP"},
|
|
[7027] = {41, "PvP"},
|
|
[7161] = {41, "PvP"},
|
|
[7162] = {41, "PvP"},
|
|
[7841] = {0, ""},
|
|
[7842] = {0, ""},
|
|
[7843] = {0, ""},
|
|
[8122] = {41, "PvP"},
|
|
[8386] = {41, "PvP"},
|
|
[8404] = {41, "PvP"},
|
|
[8405] = {41, "PvP"},
|
|
[8406] = {41, "PvP"},
|
|
[8407] = {41, "PvP"},
|
|
[8408] = {41, "PvP"},
|
|
[12170] = {41, "PvP"},
|
|
[12244] = {41, "PvP"},
|
|
[12268] = {41, "PvP"},
|
|
[12270] = {41, "PvP"},
|
|
[12280] = {41, "PvP"},
|
|
[12284] = {41, "PvP"},
|
|
[12288] = {41, "PvP"},
|
|
[12289] = {41, "PvP"},
|
|
[12296] = {41, "PvP"},
|
|
[12314] = {41, "PvP"},
|
|
[12315] = {41, "PvP"},
|
|
[12316] = {41, "PvP"},
|
|
[12317] = {41, "PvP"},
|
|
[12323] = {41, "PvP"},
|
|
[12324] = {41, "PvP"},
|
|
[12432] = {41, "PvP"},
|
|
[12433] = {41, "PvP"},
|
|
[12434] = {41, "PvP"},
|
|
[12437] = {41, "PvP"},
|
|
[12443] = {41, "PvP"},
|
|
[12446] = {41, "PvP"},
|
|
[13199] = {41, "PvP"},
|
|
[78680] = {1, "Elite"},
|
|
[78681] = {1, "Elite"},
|
|
[78684] = {1, "Elite"},
|
|
[80098] = {1, "Elite"},
|
|
[80147] = {1, "Elite"},
|
|
[80148] = {1, "Elite"},
|
|
[80149] = {1, "Elite"},
|
|
[80150] = {1, "Elite"},
|
|
[80151] = {1, "Elite"},
|
|
[80152] = {1, "Elite"},
|
|
[90014] = {1, "Elite"},
|
|
[90054] = {1, "Elite"},
|
|
[90070] = {1, "Elite"},
|
|
[90081] = {1, "Elite"},
|
|
[90091] = {1, "Elite"},
|
|
[90094] = {1, "Elite"},
|
|
[90113] = {1, "Elite"},
|
|
[90114] = {1, "Elite"},
|
|
[90116] = {1, "Elite"},
|
|
[90119] = {1, "Elite"},
|
|
[90122] = {1, "Elite"},
|
|
[90151] = {1, "Elite"},
|
|
[90152] = {1, "Elite"},
|
|
[90159] = {1, "Elite"},
|
|
[90163] = {1, "Elite"},
|
|
[90166] = {1, "Elite"},
|
|
[90169] = {1, "Elite"},
|
|
[90175] = {1, "Elite"},
|
|
[90190] = {1, "Elite"},
|
|
[90202] = {1, "Elite"},
|
|
[90204] = {1, "Elite"},
|
|
[90221] = {1, "Elite"},
|
|
[90225] = {1, "Elite"},
|
|
[90230] = {1, "Elite"},
|
|
[90269] = {1, "Elite"},
|
|
[90271] = {1, "Elite"},
|
|
}
|
|
-- 3.3.5 compatibility: fake Item API if missing (Retail style)
|
|
if not Item then
|
|
Item = {}
|
|
end
|
|
|
|
if not Item.CreateFromItemID then
|
|
local function SafeGetItemName(itemID)
|
|
local numericItemID = tonumber(itemID)
|
|
if not numericItemID or numericItemID == 0 then
|
|
return "item:" .. tostring(itemID)
|
|
end
|
|
|
|
local ok, name = pcall(GetItemInfo, numericItemID)
|
|
if not ok then
|
|
return "item:" .. tostring(numericItemID)
|
|
end
|
|
return name or ("item:" .. tostring(numericItemID))
|
|
end
|
|
|
|
function Item:CreateFromItemID(itemID)
|
|
local obj = {}
|
|
|
|
function obj:ContinueOnItemLoad(callback)
|
|
-- In 3.3.5 there is no async loading, so call the callback immediately
|
|
if callback then
|
|
callback()
|
|
end
|
|
end
|
|
|
|
function obj:GetItemName()
|
|
return SafeGetItemName(itemID)
|
|
end
|
|
|
|
return obj
|
|
end
|
|
end
|
|
|
|
-- race bitmask data, for easy access
|
|
local VANILLA = WOW_PROJECT_ID == WOW_PROJECT_CLASSIC
|
|
|
|
QuestieDB.raceKeys = {
|
|
ALL_ALLIANCE = VANILLA and 77 or 1101,
|
|
ALL_HORDE = VANILLA and 178 or 690,
|
|
NONE = 0,
|
|
|
|
HUMAN = 1,
|
|
ORC = 2,
|
|
DWARF = 4,
|
|
NIGHT_ELF = 8,
|
|
UNDEAD = 16,
|
|
TAUREN = 32,
|
|
GNOME = 64,
|
|
TROLL = 128,
|
|
--GOBLIN = 256,
|
|
BLOOD_ELF = 512,
|
|
DRAENEI = 1024
|
|
}
|
|
|
|
-- Combining these with "and" makes the order matter
|
|
-- 1 and 2 ~= 2 and 1
|
|
QuestieDB.classKeys = {
|
|
NONE = 0,
|
|
|
|
WARRIOR = 1,
|
|
PALADIN = 2,
|
|
HUNTER = 4,
|
|
ROGUE = 8,
|
|
PRIEST = 16,
|
|
DEATH_KNIGHT = 32,
|
|
SHAMAN = 64,
|
|
MAGE = 128,
|
|
WARLOCK = 256,
|
|
DRUID = 1024
|
|
}
|
|
|
|
QuestieDB.specialFlags = {
|
|
REPEATABLE = 1,
|
|
}
|
|
|
|
_QuestieDB.questCache = {}; -- stores quest objects so they dont need to be regenerated
|
|
_QuestieDB.itemCache = {};
|
|
_QuestieDB.npcCache = {};
|
|
_QuestieDB.objectCache = {};
|
|
_QuestieDB.zoneCache = {};
|
|
|
|
---A Memoized table for function Quest:CheckRace
|
|
---
|
|
---Usage: checkRace[requiredRaces]
|
|
---@type table<number, boolean>
|
|
local checkRace
|
|
---A Memoized table for function Quest:CheckClass
|
|
---
|
|
---Usage: checkRace[requiredClasses]
|
|
---@type table<number, boolean>
|
|
local checkClass
|
|
|
|
---QuestieCorrections.hiddenQuests
|
|
local QuestieCorrectionshiddenQuests
|
|
---Questie.db.char.hidden
|
|
local Questiedbcharhidden
|
|
|
|
QuestieDB.itemDataOverrides = {}
|
|
QuestieDB.npcDataOverrides = {}
|
|
QuestieDB.objectDataOverrides = {}
|
|
QuestieDB.questDataOverrides = {}
|
|
|
|
QuestieDB.activeChildQuests = {}
|
|
|
|
|
|
function QuestieDB:Initialize()
|
|
|
|
StaticPopupDialogs["QUESTIE_DATABASE_ERROR"] = { -- /run StaticPopup_Show ("QUESTIE_DATABASE_ERROR")
|
|
text = l10n("There was a problem initializing Questie's database. This can usually be fixed by recompiling the database."),
|
|
button1 = l10n("Recompile Database"),
|
|
button2 = l10n("Don't show again"),
|
|
OnAccept = function()
|
|
if Questie.IsSoD then
|
|
Questie.db.global.sod.dbIsCompiled = false
|
|
else
|
|
Questie.db.global.dbIsCompiled = false
|
|
end
|
|
ReloadUI()
|
|
end,
|
|
OnDecline = function()
|
|
Questie.db.profile.disableDatabaseWarnings = true
|
|
end,
|
|
OnShow = function(self)
|
|
self:SetFrameStrata("TOOLTIP")
|
|
end,
|
|
timeout = 0,
|
|
whileDead = true,
|
|
hideOnEscape = false,
|
|
preferredIndex = 3
|
|
}
|
|
|
|
-- For now we store both, the SoD database and the Era/HC database
|
|
local npcBin, npcPtrs, questBin, questPtrs, objBin, objPtrs, itemBin, itemPtrs
|
|
local binaryBucket = Questie.dbCache and Questie.dbCache.global or Questie.db.global
|
|
|
|
if Questie.IsSoD then
|
|
binaryBucket = binaryBucket.sod or {}
|
|
npcBin = binaryBucket.npcBin
|
|
npcPtrs = binaryBucket.npcPtrs
|
|
questBin = binaryBucket.questBin
|
|
questPtrs = binaryBucket.questPtrs
|
|
objBin = binaryBucket.objBin
|
|
objPtrs = binaryBucket.objPtrs
|
|
itemBin = binaryBucket.itemBin
|
|
itemPtrs = binaryBucket.itemPtrs
|
|
else
|
|
npcBin = binaryBucket.npcBin
|
|
npcPtrs = binaryBucket.npcPtrs
|
|
questBin = binaryBucket.questBin
|
|
questPtrs = binaryBucket.questPtrs
|
|
objBin = binaryBucket.objBin
|
|
objPtrs = binaryBucket.objPtrs
|
|
itemBin = binaryBucket.itemBin
|
|
itemPtrs = binaryBucket.itemPtrs
|
|
end
|
|
|
|
QuestieDB.QueryNPC = QuestieDBCompiler:GetDBHandle(npcBin, npcPtrs, QuestieDBCompiler:BuildSkipMap(QuestieDB.npcCompilerTypes, QuestieDB.npcCompilerOrder), QuestieDB.npcKeys, QuestieDB.npcDataOverrides)
|
|
QuestieDB.QueryQuest = QuestieDBCompiler:GetDBHandle(questBin, questPtrs, QuestieDBCompiler:BuildSkipMap(QuestieDB.questCompilerTypes, QuestieDB.questCompilerOrder), QuestieDB.questKeys, QuestieDB.questDataOverrides)
|
|
QuestieDB.QueryObject = QuestieDBCompiler:GetDBHandle(objBin, objPtrs, QuestieDBCompiler:BuildSkipMap(QuestieDB.objectCompilerTypes, QuestieDB.objectCompilerOrder), QuestieDB.objectKeys, QuestieDB.objectDataOverrides)
|
|
QuestieDB.QueryItem = QuestieDBCompiler:GetDBHandle(itemBin, itemPtrs, QuestieDBCompiler:BuildSkipMap(QuestieDB.itemCompilerTypes, QuestieDB.itemCompilerOrder), QuestieDB.itemKeys, QuestieDB.itemDataOverrides)
|
|
|
|
QuestieDB._QueryQuestSingle = QuestieDB.QueryQuest.QuerySingle
|
|
QuestieDB._QueryNPCSingle = QuestieDB.QueryNPC.QuerySingle
|
|
QuestieDB._QueryObjectSingle = QuestieDB.QueryObject.QuerySingle
|
|
QuestieDB._QueryItemSingle = QuestieDB.QueryItem.QuerySingle
|
|
|
|
QuestieDB.NPCPointers = QuestieDB.QueryNPC.pointers
|
|
QuestieDB.QuestPointers = QuestieDB.QueryQuest.pointers
|
|
QuestieDB.ObjectPointers = QuestieDB.QueryObject.pointers
|
|
QuestieDB.ItemPointers = QuestieDB.QueryItem.pointers
|
|
|
|
QuestieDB._QueryNPC = QuestieDB.QueryNPC.Query
|
|
QuestieDB._QueryQuest = QuestieDB.QueryQuest.Query
|
|
QuestieDB._QueryObject = QuestieDB.QueryObject.Query
|
|
QuestieDB._QueryItem = QuestieDB.QueryItem.Query
|
|
|
|
QuestieDB.QueryNPC = QuestieDB._QueryNPC
|
|
QuestieDB.QueryQuest = QuestieDB._QueryQuest
|
|
QuestieDB.QueryObject = QuestieDB._QueryObject
|
|
QuestieDB.QueryItem = QuestieDB._QueryItem
|
|
|
|
QuestieDB.QueryQuestSingle = QuestieDB._QueryQuestSingle
|
|
QuestieDB.QueryNPCSingle = QuestieDB._QueryNPCSingle
|
|
QuestieDB.QueryObjectSingle = QuestieDB._QueryObjectSingle
|
|
QuestieDB.QueryItemSingle = QuestieDB._QueryItemSingle
|
|
|
|
-- data has been corrected, ensure cache is empty (something might have accessed the api before questie initialized)
|
|
_QuestieDB.questCache = {};
|
|
_QuestieDB.itemCache = {};
|
|
_QuestieDB.npcCache = {};
|
|
_QuestieDB.objectCache = {};
|
|
_QuestieDB.zoneCache = {};
|
|
|
|
--? This improves performance a lot, the regular functions still work but this is much faster because i caches
|
|
checkRace = QuestieLib:TableMemoizeFunction(QuestiePlayer.HasRequiredRace)
|
|
checkClass = QuestieLib:TableMemoizeFunction(QuestiePlayer.HasRequiredClass)
|
|
--? Set the localized versions of these.
|
|
QuestieCorrectionshiddenQuests = QuestieCorrections.hiddenQuests
|
|
Questiedbcharhidden = Questie.db.char.hidden
|
|
end
|
|
|
|
function QuestieDB:GetObject(objectId)
|
|
if self ~= QuestieDB then
|
|
objectId = self
|
|
end
|
|
objectId = tonumber(objectId)
|
|
if not objectId then
|
|
return nil
|
|
end
|
|
if _QuestieDB.objectCache[objectId] then
|
|
return _QuestieDB.objectCache[objectId];
|
|
end
|
|
|
|
local settings = _GetLearnerSettings()
|
|
local mode = settings and settings.dataSourceMode or "auto"
|
|
local learnerRecord = _GetLearnerRecord("objects", objectId)
|
|
|
|
local rawdata
|
|
local override
|
|
override = QuestieDB.objectDataOverrides and (QuestieDB.objectDataOverrides[objectId] or QuestieDB.objectDataOverrides[tostring(objectId)])
|
|
if mode == "learner" or QuestieDB:IsStoreMissing("objectData") then
|
|
rawdata = learnerRecord
|
|
if not rawdata then
|
|
rawdata = override
|
|
end
|
|
else
|
|
rawdata = QuestieDB.QueryObject(objectId, QuestieDB._objectAdapterQueryOrder)
|
|
if not rawdata and learnerRecord and mode == "auto" then
|
|
-- Only "auto" overlays learner data on top of the static DB. "static"
|
|
-- and "none" must never silently fall back to learner records.
|
|
rawdata = learnerRecord
|
|
end
|
|
end
|
|
|
|
if not rawdata and not override then
|
|
if not QuestieDB.private.missingObjectLog[objectId] then
|
|
QuestieDB.private.missingObjectLog[objectId] = true
|
|
Questie:Debug(Questie.DEBUG_INFO, "[QuestieDB:GetObject] data not found for objectID:", objectId)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local obj = {
|
|
id = objectId,
|
|
type = "object"
|
|
}
|
|
|
|
if override then
|
|
-- Prefer override data (corrections); _MergeOverride checks both
|
|
-- string keys (override.name) and numeric keys (override[1]) so
|
|
-- AscensionDB and QuestieLearner numeric-key overrides are picked up.
|
|
_MergeOverride(obj, override, rawdata, QuestieDB.objectKeys)
|
|
else
|
|
-- Use standard DB data
|
|
local stringKey, intKey = next(QuestieDB.objectKeys)
|
|
while stringKey do
|
|
obj[stringKey] = rawdata[intKey]
|
|
stringKey, intKey = next(QuestieDB.objectKeys, stringKey)
|
|
end
|
|
end
|
|
|
|
_QuestieDB.objectCache[objectId] = obj;
|
|
return obj;
|
|
end
|
|
|
|
function QuestieDB:GetItem(itemId)
|
|
if self ~= QuestieDB then
|
|
itemId = self
|
|
end
|
|
itemId = tonumber(itemId)
|
|
if (not itemId) or (itemId == 0) then
|
|
return nil
|
|
end
|
|
if _QuestieDB.itemCache[itemId] then
|
|
return _QuestieDB.itemCache[itemId];
|
|
end
|
|
|
|
local settings = _GetLearnerSettings()
|
|
local mode = settings and settings.dataSourceMode or "auto"
|
|
local learnerRecord = _GetLearnerRecord("items", itemId)
|
|
local rawdata
|
|
local override
|
|
override = QuestieDB.itemDataOverrides and (QuestieDB.itemDataOverrides[itemId] or QuestieDB.itemDataOverrides[tostring(itemId)])
|
|
if mode == "learner" or QuestieDB:IsStoreMissing("itemData") then
|
|
rawdata = learnerRecord
|
|
if not rawdata then
|
|
rawdata = override
|
|
end
|
|
else
|
|
rawdata = QuestieDB.QueryItem(itemId, QuestieDB._itemAdapterQueryOrder)
|
|
if not rawdata and learnerRecord and mode == "auto" then
|
|
-- Only "auto" overlays learner data on top of the static DB. "static"
|
|
-- and "none" must never silently fall back to learner records.
|
|
rawdata = learnerRecord
|
|
end
|
|
end
|
|
|
|
if not rawdata and not override then
|
|
if not QuestieDB.private.missingItemLog[itemId] then
|
|
QuestieDB.private.missingItemLog[itemId] = true
|
|
Questie:Debug(Questie.DEBUG_INFO, "[QuestieDB:GetItem] data not found for itemID:", itemId)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local item = {
|
|
Id = itemId,
|
|
Sources = {},
|
|
Hidden = QuestieCorrections.questItemBlacklist[itemId]
|
|
}
|
|
|
|
if override then
|
|
-- Prefer override data (corrections); _MergeOverride handles both
|
|
-- string-keyed and numeric-keyed override formats.
|
|
_MergeOverride(item, override, rawdata, QuestieDB.itemKeys)
|
|
else
|
|
-- Use standard DB data
|
|
local stringKey, intKey = next(QuestieDB.itemKeys)
|
|
while stringKey do
|
|
item[stringKey] = rawdata[intKey]
|
|
stringKey, intKey = next(QuestieDB.itemKeys, stringKey)
|
|
end
|
|
end
|
|
|
|
local sources = item.Sources
|
|
|
|
if item.npcDrops then
|
|
local _npcId, npcId = next(item.npcDrops)
|
|
while _npcId do
|
|
table.insert(sources, {
|
|
Id = npcId,
|
|
Type = "monster",
|
|
})
|
|
_npcId, npcId = next(item.npcDrops, _npcId)
|
|
end
|
|
end
|
|
|
|
if item.vendors then
|
|
local _npcId, npcId = next(item.vendors)
|
|
while _npcId do
|
|
table.insert(sources, {
|
|
Id = npcId,
|
|
Type = "monster",
|
|
})
|
|
_npcId, npcId = next(item.vendors, _npcId)
|
|
end
|
|
end
|
|
|
|
if item.objectDrops then
|
|
local _k, v = next(item.objectDrops)
|
|
while _k do
|
|
table.insert(sources, {
|
|
Id = v,
|
|
Type = "object",
|
|
})
|
|
_k, v = next(item.objectDrops, _k)
|
|
end
|
|
end
|
|
|
|
return item
|
|
end
|
|
|
|
---@param questId number
|
|
---@return boolean
|
|
function QuestieDB.IsRepeatable(questId)
|
|
local flags = QuestieDB.QueryQuestSingle(questId, "specialFlags")
|
|
return flags and bitband(flags, 1) ~= 0
|
|
end
|
|
|
|
---@param questId number
|
|
---@return boolean
|
|
function QuestieDB.IsDailyQuest(questId)
|
|
local flags = QuestieDB.QueryQuestSingle(questId, "questFlags")
|
|
-- test a bit flag: (value % (2*flag) >= flag)
|
|
return flags and (flags % QUEST_FLAGS_DAILY_X2) >= QUEST_FLAGS_DAILY
|
|
end
|
|
|
|
---@param questId number
|
|
---@return boolean
|
|
function QuestieDB.IsWeeklyQuest(questId)
|
|
local flags = QuestieDB.QueryQuestSingle(questId, "questFlags")
|
|
-- test a bit flag: (value % (2*flag) >= flag)
|
|
return flags and (flags % QUEST_FLAGS_WEEKLY_X2) >= QUEST_FLAGS_WEEKLY
|
|
end
|
|
|
|
---@param questId number
|
|
---@return boolean
|
|
function QuestieDB.IsDungeonQuest(questId)
|
|
local questType, _ = QuestieDB.GetQuestTagInfo(questId)
|
|
return questType == 81
|
|
end
|
|
|
|
---@param questId number
|
|
---@return boolean
|
|
function QuestieDB.IsRaidQuest(questId)
|
|
local questType, _ = QuestieDB.GetQuestTagInfo(questId)
|
|
return questType == 62
|
|
end
|
|
|
|
---@param questId number
|
|
---@return boolean
|
|
function QuestieDB.IsPvPQuest(questId)
|
|
local questType, _ = QuestieDB.GetQuestTagInfo(questId)
|
|
return questType == 41
|
|
end
|
|
|
|
---@param questId number
|
|
---@return boolean
|
|
function QuestieDB.IsGroupQuest(questId)
|
|
local questType, _ = QuestieDB.GetQuestTagInfo(questId)
|
|
return questType == 1
|
|
end
|
|
|
|
--[[ Commented out because not used anywhere
|
|
---@param questId number
|
|
---@return boolean
|
|
function QuestieDB:IsAQWarEffortQuest(questId)
|
|
return QuestieQuestBlacklist.AQWarEffortQuests[questId]
|
|
end
|
|
]]--
|
|
|
|
---@param class string
|
|
---@return number
|
|
function QuestieDB:GetZoneOrSortForClass(class)
|
|
return QuestieDB.sortKeys[class]
|
|
end
|
|
|
|
--- Wrapper function for the GetQuestTagInfo API to correct
|
|
--- quests that are falsely marked by Blizzard
|
|
---@param questId number
|
|
---@return number|nil questType, string|nil questTag
|
|
function QuestieDB.GetQuestTagInfo(questId)
|
|
if questTagCorrections[questId] then
|
|
return questTagCorrections[questId][1], questTagCorrections[questId][2]
|
|
end
|
|
local questType, questTag = GetQuestTagInfo(questId)
|
|
|
|
return questType, questTag
|
|
end
|
|
|
|
---@param questId number
|
|
---@return boolean
|
|
function QuestieDB.IsActiveEventQuest(questId)
|
|
--! If you edit the logic here, also edit in QuestieDB:IsLevelRequirementsFulfilled
|
|
return QuestieEvent.activeQuests[questId] == true
|
|
end
|
|
|
|
---@param exclusiveTo table<number, number>
|
|
---@return boolean
|
|
function QuestieDB:IsExclusiveQuestInQuestLogOrComplete(exclusiveTo)
|
|
if (not exclusiveTo) then
|
|
return false
|
|
end
|
|
|
|
local _exId, exId = next(exclusiveTo)
|
|
while _exId do
|
|
if Questie.db.char.complete[exId] then
|
|
return true
|
|
end
|
|
_exId, exId = next(exclusiveTo, _exId)
|
|
end
|
|
return false
|
|
end
|
|
|
|
---@param questId QuestId
|
|
---@param minLevel Level @The level a quest must have at least to be shown
|
|
---@param maxLevel Level @The level a quest can have at most to be shown
|
|
---@param playerLevel Level? @Pass player level to avoid calling UnitLevel or to use custom level
|
|
---@return boolean
|
|
function QuestieDB.IsLevelRequirementsFulfilled(questId, minLevel, maxLevel, playerLevel)
|
|
playerLevel = playerLevel or QuestiePlayer.GetPlayerLevel()
|
|
|
|
local level, requiredLevel, requiredMaxLevel = QuestieLib.GetTbcLevel(questId, playerLevel)
|
|
requiredMaxLevel = requiredMaxLevel or 0
|
|
--* QuestiePlayer.currentQuestlog[parentQuestId] logic is from QuestieDB.IsParentQuestActive, if you edit here, also edit there
|
|
local parentQuestId = QuestieDB.QueryQuestSingle(questId, "parentQuest")
|
|
if parentQuestId and QuestiePlayer.currentQuestlog[parentQuestId] then
|
|
-- If the quest is in the player's log already, there's no need to do any logic here, it must already be available
|
|
return true
|
|
end
|
|
|
|
--* QuestieEvent.activeQuests[questId] logic is from QuestieDB.IsParentQuestActive, if you edit here, also edit there
|
|
if (Questie.db.profile.lowLevelStyle ~= Questie.LOWLEVEL_RANGE) and
|
|
minLevel > requiredLevel and
|
|
QuestieEvent.activeQuests[questId] then
|
|
return true
|
|
end
|
|
|
|
if (Questie.IsSoD == true) and (QuestieDB.IsSoDRuneQuest(questId) == true) and (requiredLevel <= playerLevel) then
|
|
-- Season of Discovery Rune quests are still shown when trivial
|
|
return true
|
|
end
|
|
|
|
if maxLevel >= level then
|
|
if (Questie.db.profile.lowLevelStyle ~= Questie.LOWLEVEL_ALL) and minLevel > level then
|
|
-- The quest level is too low and trivial quests are not shown
|
|
return false
|
|
end
|
|
else
|
|
if (Questie.db.profile.lowLevelStyle == Questie.LOWLEVEL_RANGE) or maxLevel < requiredLevel then
|
|
-- Either an absolute level range is set and maxLevel < level OR the maxLevel is manually set to a lower value
|
|
return false
|
|
end
|
|
end
|
|
|
|
if maxLevel < requiredLevel then
|
|
-- Either the players level is not high enough or the maxLevel is manually set to a lower value
|
|
return false
|
|
end
|
|
|
|
if requiredMaxLevel ~= 0 and playerLevel > requiredMaxLevel then
|
|
-- The players level exceeds the requiredMaxLevel of a quest
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
---@param parentID number
|
|
---@return boolean
|
|
function QuestieDB.IsParentQuestActive(parentID)
|
|
--! If you edit the logic here, also edit in QuestieDB:IsLevelRequirementsFulfilled
|
|
if (not parentID) or (parentID == 0) then
|
|
return false
|
|
end
|
|
if QuestiePlayer.currentQuestlog[parentID] then
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
--- Returns a table of [npcId] = true for NPCs that have verified learned data (Confidence >= 2)
|
|
--- in a specific zone. Used to hide static database spawns in favor of verified ones.
|
|
---@param zoneId number
|
|
---@return table<number, boolean>
|
|
function QuestieDB.GetSuppressedNPCs(zoneId)
|
|
local suppressed = {}
|
|
local ld = Questie.dbLearner.global
|
|
local mode = ld and ld.settings and ld.settings.dataSourceMode or "auto"
|
|
if mode == "auto" or mode == "learner" then
|
|
if ld and ld.settings and ld.settings.enabled and ld.npcs then
|
|
local threshold = ld.settings.minConfidencePins or 2
|
|
local npcId, entry = next(ld.npcs)
|
|
while npcId do
|
|
local spawns = GetSpawnTable(entry, 7, 4)
|
|
if entry.mc and entry.mc >= threshold and spawns and spawns[zoneId] then
|
|
suppressed[npcId] = true
|
|
end
|
|
npcId, entry = next(ld.npcs, npcId)
|
|
end
|
|
end
|
|
end
|
|
return suppressed
|
|
end
|
|
|
|
--- Returns a table of [objectId] = true for Objects that have verified learned data (Confidence >= 2)
|
|
--- in a specific zone. Used to hide static database spawns in favor of verified ones.
|
|
---@param zoneId number
|
|
---@return table<number, boolean>
|
|
function QuestieDB.GetSuppressedObjects(zoneId)
|
|
local suppressed = {}
|
|
local ld = Questie.dbLearner.global
|
|
local mode = ld and ld.settings and ld.settings.dataSourceMode or "auto"
|
|
if mode == "auto" or mode == "learner" then
|
|
if ld and ld.settings and ld.settings.enabled and ld.objects then
|
|
local threshold = ld.settings.minConfidencePins or 2
|
|
local objId, entry = next(ld.objects)
|
|
while objId do
|
|
local spawns = GetSpawnTable(entry, 4, 7)
|
|
if entry.mc and entry.mc >= threshold and spawns and spawns[zoneId] then
|
|
suppressed[objId] = true
|
|
end
|
|
objId, entry = next(ld.objects, objId)
|
|
end
|
|
end
|
|
end
|
|
return suppressed
|
|
end
|
|
|
|
---@param preQuestGroup table<number, number>
|
|
---@return boolean
|
|
function QuestieDB:IsPreQuestGroupFulfilled(preQuestGroup)
|
|
if not preQuestGroup then
|
|
return true
|
|
end
|
|
for preQuestIndex=1, table.getn(preQuestGroup) do
|
|
-- If a quest is not complete and no exlusive quest is complete, the requirement is not fulfilled
|
|
if not Questie.db.char.complete[preQuestGroup[preQuestIndex]] then
|
|
local preQuest = QuestieDB.QueryQuestSingle(preQuestGroup[preQuestIndex], "exclusiveTo")
|
|
if (not preQuest) then
|
|
return false
|
|
end
|
|
|
|
local anyExlusiveFinished = false
|
|
for i=1, table.getn(preQuest) do
|
|
if Questie.db.char.complete[preQuest[i]] then
|
|
anyExlusiveFinished = true
|
|
end
|
|
end
|
|
if not anyExlusiveFinished then
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
-- All preQuests are complete
|
|
return true
|
|
end
|
|
|
|
---@param preQuestSingle number[]
|
|
---@return boolean
|
|
function QuestieDB:IsPreQuestSingleFulfilled(preQuestSingle)
|
|
if (not preQuestSingle) then
|
|
return true
|
|
end
|
|
for preQuestIndex=1, table.getn(preQuestSingle) do
|
|
-- If a quest is complete the requirement is fulfilled
|
|
if Questie.db.char.complete[preQuestSingle[preQuestIndex]] then
|
|
return true
|
|
end
|
|
end
|
|
-- No preQuest is complete
|
|
return false
|
|
end
|
|
|
|
---@param questId number
|
|
---@param debugPrint boolean? -- if true, IsDoable will print conclusions to debug channel
|
|
---@return boolean
|
|
function QuestieDB.IsDoable(questId, debugPrint)
|
|
|
|
--! Before changing any logic in QuestieDB.IsDoable, make sure
|
|
--! to mirror the same logic to QuestieDB.IsDoableVerbose!
|
|
|
|
-- IsDoable determines if the player is currently eligible for
|
|
-- a quest, and returns that result as true/false in order to
|
|
-- programmatically show/hide quests and determine further logic.
|
|
|
|
-- IsDoableVerbose does the same logic, but returns human-readable
|
|
-- explanations as a string for display in the UI.
|
|
|
|
-- These functions are maintained separately for performance,
|
|
-- because IsDoable is often called in a loop through every
|
|
-- quest in the DB in order to update icons, while
|
|
-- IsDoableVerbose is only called manually by the user.
|
|
|
|
-- These are localized in the init function
|
|
if QuestieCorrectionshiddenQuests[questId] then
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Quest " .. questId .. " is hidden automatically!") end
|
|
return false
|
|
end
|
|
|
|
if Questiedbcharhidden[questId] then
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Quest " .. questId .. " is hidden manually!") end
|
|
return false
|
|
end
|
|
|
|
if C_QuestLog.IsOnQuest(questId) == true then
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Quest " .. questId .. " is eligible because Player is on the quest!") end
|
|
return true
|
|
end
|
|
|
|
if QuestieDB.activeChildQuests[questId] then -- The parent quest is active, so this quest is doable
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Quest " .. questId .. " is eligible because it's a child quest and the parent is active!") end
|
|
return true
|
|
-- this scenario actually returns true, so it skips the rest of the later checks, because
|
|
-- if we're on the parent quest then we implicitly know all other requirements are met
|
|
end
|
|
|
|
local requiredRaces = QuestieDB.QueryQuestSingle(questId, "requiredRaces")
|
|
if (requiredRaces and not checkRace[requiredRaces]) then
|
|
QuestieDB.autoBlacklist[questId] = "race"
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Race requirement not fulfilled for quest " .. questId) end
|
|
return false
|
|
end
|
|
|
|
-- Check the preQuestSingle field where just one of the required quests has to be complete for a quest to show up
|
|
local preQuestSingle = QuestieDB.QueryQuestSingle(questId, "preQuestSingle")
|
|
if preQuestSingle then
|
|
local isPreQuestSingleFulfilled = QuestieDB:IsPreQuestSingleFulfilled(preQuestSingle)
|
|
if not isPreQuestSingleFulfilled then
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Pre-quest requirement not fulfilled for quest " .. questId) end
|
|
return false
|
|
end
|
|
end
|
|
|
|
local requiredClasses = QuestieDB.QueryQuestSingle(questId, "requiredClasses")
|
|
if (requiredClasses and not checkClass[requiredClasses]) then
|
|
QuestieDB.autoBlacklist[questId] = "class"
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Class requirement not fulfilled for quest " .. questId) end
|
|
return false
|
|
end
|
|
|
|
local requiredMinRep = QuestieDB.QueryQuestSingle(questId, "requiredMinRep")
|
|
local requiredMaxRep = QuestieDB.QueryQuestSingle(questId, "requiredMaxRep")
|
|
if (requiredMinRep or requiredMaxRep) then
|
|
local aboveMinRep, hasMinFaction, belowMaxRep, hasMaxFaction = QuestieReputation:HasFactionAndReputationLevel(requiredMinRep, requiredMaxRep)
|
|
if (not ((aboveMinRep and hasMinFaction) and (belowMaxRep and hasMaxFaction))) then
|
|
--- If we haven't got the faction for min or max we blacklist it
|
|
if not hasMinFaction or not hasMaxFaction then -- or not belowMaxRep -- This is something we could have done, but would break if you rep downwards
|
|
QuestieDB.autoBlacklist[questId] = "rep"
|
|
end
|
|
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Player does not meet reputation requirements for quest " .. questId) end
|
|
return false
|
|
end
|
|
end
|
|
|
|
local requiredSkill = QuestieDB.QueryQuestSingle(questId, "requiredSkill")
|
|
if (requiredSkill) then
|
|
local hasProfession, hasSkillLevel = QuestieProfessions:HasProfessionAndSkillLevel(requiredSkill)
|
|
if (not (hasProfession and hasSkillLevel)) then
|
|
--? We haven't got the profession so we blacklist it.
|
|
if(not hasProfession) then
|
|
QuestieDB.autoBlacklist[questId] = "skill"
|
|
end
|
|
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Player does not meet profession requirements for quest " .. questId) end
|
|
return false
|
|
end
|
|
end
|
|
|
|
--? PreQuestGroup and PreQuestSingle are mutualy exclusive to eachother and preQuestSingle is more prevalent
|
|
--? Only try group if single does not exist.
|
|
if not preQuestSingle then
|
|
-- Check the preQuestGroup field where every required quest has to be complete for a quest to show up
|
|
local preQuestGroup = QuestieDB.QueryQuestSingle(questId, "preQuestGroup")
|
|
if preQuestGroup then
|
|
local isPreQuestGroupFulfilled = QuestieDB:IsPreQuestGroupFulfilled(preQuestGroup)
|
|
if not isPreQuestGroupFulfilled then
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Group pre-quest requirement not fulfilled for quest " .. questId) end
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
|
|
local parentQuest = QuestieDB.QueryQuestSingle(questId, "parentQuest")
|
|
if parentQuest and parentQuest ~= 0 then
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Quest " .. questId .. " has an inactive parent quest") end
|
|
return false
|
|
end
|
|
|
|
local nextQuestInChain = QuestieDB.QueryQuestSingle(questId, "nextQuestInChain")
|
|
if nextQuestInChain and nextQuestInChain ~= 0 then
|
|
if Questie.db.char.complete[nextQuestInChain] or QuestiePlayer.currentQuestlog[nextQuestInChain] then
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Follow up quests already completed or in the quest log for quest " .. questId) end
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- Check if a quest which is exclusive to the current has already been completed or accepted
|
|
-- If yes the current quest can't be accepted
|
|
local ExclusiveQuestGroup = QuestieDB.QueryQuestSingle(questId, "exclusiveTo")
|
|
if ExclusiveQuestGroup then -- fix (DO NOT REVERT, tested thoroughly)
|
|
local _k, v = next(ExclusiveQuestGroup)
|
|
while _k do
|
|
if Questie.db.char.complete[v] or QuestiePlayer.currentQuestlog[v] then
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Player has completed a quest exclusive with quest " .. questId) end
|
|
return false
|
|
end
|
|
_k, v = next(ExclusiveQuestGroup, _k)
|
|
end
|
|
end
|
|
|
|
if (not DailyQuests:IsActiveDailyQuest(questId)) then
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Quest " .. questId .. " is a daily quest which isn't active today!") end
|
|
return false
|
|
end
|
|
|
|
local requiredSpecialization = QuestieDB.QueryQuestSingle(questId, "requiredSpecialization")
|
|
if (requiredSpecialization) and (requiredSpecialization > 0) then
|
|
local hasSpecialization = QuestieProfessions:HasSpecialization(requiredSpecialization)
|
|
if (not hasSpecialization) then
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Player does not meet profession specialization requirements for quest " .. questId) end
|
|
return false
|
|
end
|
|
end
|
|
|
|
local requiredSpell = QuestieDB.QueryQuestSingle(questId, "requiredSpell")
|
|
if (requiredSpell) and (requiredSpell ~= 0) then
|
|
local hasSpell = IsSpellKnownOrOverridesKnown(math.abs(requiredSpell))
|
|
local hasProfSpell = IsPlayerSpell(math.abs(requiredSpell))
|
|
if (requiredSpell > 0) and (not hasSpell) and (not hasProfSpell) then --if requiredSpell is positive, we make the quest ineligible if the player does NOT have the spell
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Player does not meet learned spell requirements for quest " .. questId) end
|
|
return false
|
|
elseif (requiredSpell < 0) and (hasSpell or hasProfSpell) then --if requiredSpell is negative, we make the quest ineligible if the player DOES have the spell
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Player does not meet unlearned spell requirements for quest " .. questId) end
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- Check and see if the Quest requires an achievement before showing as available
|
|
if _QuestieDB:CheckAchievementRequirements(questId) == false then
|
|
if debugPrint then Questie:Debug(Questie.DEBUG_SPAM, "[QuestieDB.IsDoable] Player does not meet achievement requirements for quest " .. questId) end
|
|
return false
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
---@param questId number
|
|
---@param debugPrint boolean? -- if true, IsDoable will print conclusions to debug channel
|
|
---@param returnText boolean? -- if true, IsDoable will return plaintext explanation instead of true/false
|
|
---@param returnBrief boolean? -- if true and returnText is true, IsDoable will return a very brief explanation instead of a verbose one
|
|
---@return boolean
|
|
function QuestieDB.IsDoableVerbose(questId, debugPrint, returnText, returnBrief)
|
|
|
|
--! Before changing any logic in QuestieDB.IsDoable, make sure
|
|
--! to mirror the same logic to QuestieDB.IsDoableVerbose!
|
|
|
|
-- IsDoable determines if the player is currently eligible for
|
|
-- a quest, and returns that result as true/false in order to
|
|
-- programmatically show/hide quests and determine further logic.
|
|
|
|
-- IsDoableVerbose does the same logic, but returns human-readable
|
|
-- explanations as a string for display in the UI.
|
|
|
|
-- These functions are maintained separately for performance,
|
|
-- because IsDoable is often called in a loop through every
|
|
-- quest in the DB in order to update icons, while
|
|
-- IsDoableVerbose is only called manually by the user.
|
|
|
|
if C_QuestLog.IsOnQuest(questId) == true then
|
|
local msg = "Quest " .. questId .. " is active!"
|
|
if returnText and returnBrief then
|
|
return "Eligible: Player is on quest"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
|
|
-- These are localized in the init function
|
|
if QuestieCorrectionshiddenQuests[questId] then
|
|
local msg = "Quest " .. questId .. " is hidden automatically!"
|
|
if returnText and returnBrief then
|
|
return "Unknown: Automatically blacklisted"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
|
|
if Questiedbcharhidden[questId] then
|
|
local msg = "Quest " .. questId .. " is hidden manually!"
|
|
if returnText and returnBrief then
|
|
return "Unknown: Manually blacklisted"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
|
|
if Questiedbcharhidden[questId] then
|
|
local msg = "Quest " .. questId .. " is hidden manually!"
|
|
if returnText and returnBrief then
|
|
return "Unknown: Manually blacklisted"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
|
|
if QuestieDB.activeChildQuests[questId] then -- The parent quest is active, so this quest is doable
|
|
local msg = "Quest " .. questId .. " is eligible because it's a child quest and the parent is active!"
|
|
if returnText and returnBrief then
|
|
return "Eligible: Parent active"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
|
|
local requiredRaces = QuestieDB.QueryQuestSingle(questId, "requiredRaces")
|
|
if (requiredRaces and not checkRace[requiredRaces]) then
|
|
local msg = "Race requirement not fulfilled for quest " .. questId
|
|
if returnText and returnBrief then
|
|
return "Ineligible: Race requirement"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
|
|
-- Check the preQuestSingle field where just one of the required quests has to be complete for a quest to show up
|
|
local preQuestSingle = QuestieDB.QueryQuestSingle(questId, "preQuestSingle")
|
|
if preQuestSingle then
|
|
local isPreQuestSingleFulfilled = QuestieDB:IsPreQuestSingleFulfilled(preQuestSingle)
|
|
if not isPreQuestSingleFulfilled then
|
|
local msg = "Pre-quest requirement not fulfilled for quest " .. questId
|
|
if returnText and returnBrief then
|
|
return "Ineligible: Incomplete pre-quest"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
end
|
|
|
|
local requiredClasses = QuestieDB.QueryQuestSingle(questId, "requiredClasses")
|
|
if (requiredClasses and not checkClass[requiredClasses]) then
|
|
QuestieDB.autoBlacklist[questId] = "class"
|
|
local msg = "Class requirement not fulfilled for quest " .. questId
|
|
if returnText and returnBrief then
|
|
return "Ineligible: Class requirement"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
|
|
local requiredMinRep = QuestieDB.QueryQuestSingle(questId, "requiredMinRep")
|
|
local requiredMaxRep = QuestieDB.QueryQuestSingle(questId, "requiredMaxRep")
|
|
if (requiredMinRep or requiredMaxRep) then
|
|
local aboveMinRep, hasMinFaction, belowMaxRep, hasMaxFaction = QuestieReputation:HasFactionAndReputationLevel(requiredMinRep, requiredMaxRep)
|
|
if (not ((aboveMinRep and hasMinFaction) and (belowMaxRep and hasMaxFaction))) then
|
|
--- If we haven't got the faction for min or max we blacklist it
|
|
if not hasMinFaction or not hasMaxFaction then -- or not belowMaxRep -- This is something we could have done, but would break if you rep downwards
|
|
QuestieDB.autoBlacklist[questId] = "rep"
|
|
end
|
|
|
|
local msg = "Player does not meet reputation requirements for quest " .. questId
|
|
if returnText and returnBrief then
|
|
return "Ineligible: Reputation requirement"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
end
|
|
|
|
local requiredSkill = QuestieDB.QueryQuestSingle(questId, "requiredSkill")
|
|
if (requiredSkill) then
|
|
local hasProfession, hasSkillLevel = QuestieProfessions:HasProfessionAndSkillLevel(requiredSkill)
|
|
if (not (hasProfession and hasSkillLevel)) then
|
|
--? We haven't got the profession so we blacklist it.
|
|
if(not hasProfession) then
|
|
QuestieDB.autoBlacklist[questId] = "skill"
|
|
end
|
|
|
|
local msg = "Player does not meet profession requirements for quest " .. questId
|
|
if returnText and returnBrief then
|
|
return "Ineligible: Profession requirement"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
end
|
|
|
|
--? PreQuestGroup and PreQuestSingle are mutualy exclusive to eachother and preQuestSingle is more prevalent
|
|
--? Only try group if single does not exist.
|
|
if not preQuestSingle then
|
|
-- Check the preQuestGroup field where every required quest has to be complete for a quest to show up
|
|
local preQuestGroup = QuestieDB.QueryQuestSingle(questId, "preQuestGroup")
|
|
if preQuestGroup then
|
|
local isPreQuestGroupFulfilled = QuestieDB:IsPreQuestGroupFulfilled(preQuestGroup)
|
|
if not isPreQuestGroupFulfilled then
|
|
local msg = "Group pre-quest requirement not fulfilled for quest " .. questId
|
|
if returnText and returnBrief then
|
|
return "Ineligible: Incomplete pre-quest group"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local parentQuest = QuestieDB.QueryQuestSingle(questId, "parentQuest")
|
|
if parentQuest and parentQuest ~= 0 then
|
|
local msg = "Quest " .. questId .. " has an inactive parent quest"
|
|
if returnText and returnBrief then
|
|
return "Ineligible: Inactive parent"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
|
|
local nextQuestInChain = QuestieDB.QueryQuestSingle(questId, "nextQuestInChain")
|
|
if nextQuestInChain and nextQuestInChain ~= 0 then
|
|
if Questie.db.char.complete[nextQuestInChain] or QuestiePlayer.currentQuestlog[nextQuestInChain] then
|
|
local msg = "Follow up quests already completed or in the quest log for quest " .. questId
|
|
if returnText and returnBrief then
|
|
return "Ineligible: Later quest completed"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Check if a quest which is exclusive to the current has already been completed or accepted
|
|
-- If yes the current quest can't be accepted
|
|
local ExclusiveQuestGroup = QuestieDB.QueryQuestSingle(questId, "exclusiveTo")
|
|
if ExclusiveQuestGroup then -- fix (DO NOT REVERT, tested thoroughly)
|
|
local _v, v = next(ExclusiveQuestGroup)
|
|
while _v do
|
|
if Questie.db.char.complete[v] or QuestiePlayer.currentQuestlog[v] then
|
|
local msg = "Player has completed a quest exclusive with quest " .. questId
|
|
if returnText and returnBrief then
|
|
return "Ineligible: Exclusive quest completed"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
_v, v = next(ExclusiveQuestGroup, _v)
|
|
end
|
|
end
|
|
|
|
if (not DailyQuests:IsActiveDailyQuest(questId)) then
|
|
local msg = "Quest " .. questId .. " is a daily quest which isn't active today!"
|
|
if returnText and returnBrief then
|
|
return "Ineligible: Inactive daily"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
|
|
local requiredSpecialization = QuestieDB.QueryQuestSingle(questId, "requiredSpecialization")
|
|
if (requiredSpecialization) and (requiredSpecialization > 0) then
|
|
local hasSpecialization = QuestieProfessions:HasSpecialization(requiredSpecialization)
|
|
if (not hasSpecialization) then
|
|
local msg = "Player does not meet profession specialization requirements for quest " .. questId
|
|
if returnText and returnBrief then
|
|
return "Ineligible: Profession specialization requirement"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
end
|
|
|
|
local requiredSpell = QuestieDB.QueryQuestSingle(questId, "requiredSpell")
|
|
if (requiredSpell) and (requiredSpell ~= 0) then
|
|
local hasSpell = IsSpellKnownOrOverridesKnown(math.abs(requiredSpell))
|
|
local hasProfSpell = IsPlayerSpell(math.abs(requiredSpell))
|
|
if (requiredSpell > 0) and (not hasSpell) and (not hasProfSpell) then --if requiredSpell is positive, we make the quest ineligible if the player does NOT have the spell
|
|
local msg = "Player does not meet learned spell requirements for quest " .. questId
|
|
if returnText and returnBrief then
|
|
return "Ineligible: Spell not yet learned"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
elseif (requiredSpell < 0) and (hasSpell or hasProfSpell) then --if requiredSpell is negative, we make the quest ineligible if the player DOES have the spell
|
|
local msg = "Player does not meet unlearned spell requirements for quest " .. questId
|
|
if returnText and returnBrief then
|
|
return "Ineligible: Already learned spell"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Check and see if the Quest requires an achievement before showing as available
|
|
if _QuestieDB:CheckAchievementRequirements(questId) == false then
|
|
local msg = "Player does not meet achievement requirements for quest " .. questId
|
|
if returnText and returnBrief then
|
|
return "Ineligible: Achievement requirement"
|
|
elseif returnText and not returnBrief then
|
|
return msg
|
|
end
|
|
end
|
|
|
|
if returnText then
|
|
if IsQuestFlaggedCompleted(questId) then
|
|
if returnBrief then
|
|
return "Already complete"
|
|
else
|
|
return "Player has already completed quest " .. questId .. "!"
|
|
end
|
|
else
|
|
return "Player is eligible for quest " .. questId .. "!"
|
|
end
|
|
else
|
|
return ""
|
|
end
|
|
end
|
|
|
|
---@param questId number
|
|
---@return number @Complete = 1, Failed = -1, Incomplete = 0
|
|
function QuestieDB.IsComplete(questId)
|
|
local questLogEntry = QuestLogCache.questLog_DO_NOT_MODIFY[questId] -- DO NOT MODIFY THE RETURNED TABLE
|
|
local noQuestItem = not QuestieQuest:CheckQuestSourceItem(questId)
|
|
|
|
--[[ pseudo:
|
|
if no questLogEntry then return 0
|
|
if has questLogEntry.isComplete then return questLogEntry.isComplete
|
|
if no objectives and an item is needed but not obtained then return 0
|
|
if no objectives then return 1
|
|
if all objectives are finished then return 1
|
|
return 0
|
|
--]]
|
|
|
|
if not questLogEntry then return 0 end
|
|
if questLogEntry.isComplete == 1 or questLogEntry.isComplete == -1 then
|
|
return questLogEntry.isComplete
|
|
end
|
|
|
|
local objectives = questLogEntry.objectives
|
|
if not objectives or table.getn(objectives) == 0 then
|
|
-- Before assuming an empty objective list means the quest is done, check if we *expect* objectives from the DB.
|
|
-- On WotLK private servers, GetQuestObjectives sometimes transiently returns nil during cache rebuilds,
|
|
-- which leads to IsComplete prematurely returning 1 and unloading map icons in the middle of a quest.
|
|
local expectedObjectives = QuestieDB.GetQuest(questId) and QuestieDB.GetQuest(questId).ObjectiveData
|
|
if expectedObjectives and table.getn(expectedObjectives) > 0 then
|
|
return 0
|
|
end
|
|
|
|
return (noQuestItem and 0) or 1
|
|
end
|
|
|
|
-- Check if every objective is fulfilled. Use numFulfilled == numRequired rather than the
|
|
-- finished flag, because finished is only set by QuestLogCache when the server sends
|
|
-- isComplete=1 via GetQuestLogTitle. For quests using consumable items (e.g. Cold Iron Key
|
|
-- for quest 12843), the key is consumed and numFulfilled/numRequired update immediately,
|
|
-- but isComplete=1 may not arrive until the player physically visits the turn-in NPC.
|
|
local allDone = true
|
|
for i=1, table.getn(objectives) do
|
|
local obj = objectives[i]
|
|
if obj.numRequired and obj.numRequired > 0 and obj.numFulfilled ~= obj.numRequired then
|
|
allDone = false
|
|
break
|
|
end
|
|
end
|
|
|
|
return allDone and 1 or 0
|
|
end
|
|
|
|
---@param self Quest
|
|
---@return number @Complete = 1, Failed = -1, Incomplete = 0
|
|
local function _IsComplete(self)
|
|
return QuestieDB.IsComplete(self.Id)
|
|
end
|
|
|
|
---@param questLevel number the level of the quest
|
|
---@return boolean @Returns true if the quest should be grey, false otherwise
|
|
function QuestieDB.IsTrivial(questLevel)
|
|
if questLevel == -1 then
|
|
return false -- Scaling quests are never trivial
|
|
end
|
|
|
|
local levelDiff = questLevel - QuestiePlayer.GetPlayerLevel();
|
|
if (levelDiff >= 5) then
|
|
return false -- Red
|
|
elseif (levelDiff >= 3) then
|
|
return false -- Orange
|
|
elseif (levelDiff >= -2) then
|
|
return false -- Yellow
|
|
elseif (-levelDiff <= GetQuestGreenRange("player")) then
|
|
return false -- Green
|
|
else
|
|
return true -- Grey
|
|
end
|
|
end
|
|
|
|
---@return number
|
|
local _GetIconScale = function()
|
|
return Questie.db.profile.objectScale or 1
|
|
end
|
|
|
|
---@param questId QuestId
|
|
---@return Quest|nil @The quest object or nil if the quest is missing
|
|
function QuestieDB.GetQuest(questId, ...) -- /dump QuestieDB.GetQuest(867)
|
|
-- Handle colon calling QuestieDB:GetQuest(questId)
|
|
if type(questId) == "table" and questId == QuestieDB then
|
|
questId = select(1, ...)
|
|
end
|
|
-- Handle string input from console or other sources
|
|
if type(questId) == "string" then
|
|
questId = tonumber(questId)
|
|
end
|
|
|
|
if not questId then
|
|
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieDB.GetQuest] No questId.")
|
|
return nil
|
|
end
|
|
if _QuestieDB.questCache[questId] then
|
|
return _QuestieDB.questCache[questId];
|
|
end
|
|
|
|
local settings = _GetLearnerSettings()
|
|
local mode = settings and settings.dataSourceMode or "auto"
|
|
local learnerRecord = _GetLearnerRecord("quests", questId)
|
|
local rawdata
|
|
local overrideData
|
|
overrideData = QuestieDB.questDataOverrides and (QuestieDB.questDataOverrides[questId] or QuestieDB.questDataOverrides[tostring(questId)])
|
|
if mode == "learner" or QuestieDB:IsStoreMissing("questData") then
|
|
rawdata = learnerRecord
|
|
if not rawdata then
|
|
rawdata = overrideData
|
|
end
|
|
else
|
|
rawdata = QuestieDB.QueryQuest(questId, QuestieDB._questAdapterQueryOrder)
|
|
if not rawdata and learnerRecord and mode == "auto" then
|
|
-- Only "auto" overlays learner data on top of the static DB. "static"
|
|
-- and "none" must never silently fall back to learner records.
|
|
rawdata = learnerRecord
|
|
end
|
|
end
|
|
|
|
if (not rawdata) then
|
|
rawdata = overrideData
|
|
if (not rawdata) then
|
|
if not QuestieDB.private.missingQuestLog[questId] then
|
|
QuestieDB.private.missingQuestLog[questId] = true
|
|
Questie:Debug(Questie.DEBUG_INFO, "[QuestieDB.GetQuest] rawdata is nil for questID:", questId)
|
|
end
|
|
if questId == 0 then
|
|
Questie:Error("[QuestieDB.GetQuest] rawdata is nil for questID:", questId)
|
|
end
|
|
return nil
|
|
end
|
|
-- Build a minimal live-fallback quest from the quest log so the tracker still works
|
|
local logEntry = QuestLogCache.GetQuest(questId)
|
|
if not logEntry then return nil end
|
|
local fallback = {
|
|
Id = questId,
|
|
name = logEntry.title or tostring(questId),
|
|
level = logEntry.level or 0,
|
|
questLevel = logEntry.level or 0,
|
|
requiredLevel = 0,
|
|
zoneOrSort = 0,
|
|
questFlags = 0,
|
|
specialFlags = 0,
|
|
Starts = { CreatureStarts = {}, ObjectStarts = {}, ItemStarts = {} },
|
|
Finisher = { Type = "monster", Id = 0, Name = "" },
|
|
Objectives = {},
|
|
ObjectiveData = {},
|
|
ExtraObjectives= {},
|
|
_isLogFallback = true,
|
|
isComplete = (logEntry.isComplete == 1),
|
|
}
|
|
_QuestieDB.questCache[questId] = fallback
|
|
return fallback
|
|
end
|
|
|
|
---@class Quest
|
|
---@field public Id QuestId
|
|
---@field public name Name
|
|
---@field public startedBy StartedBy
|
|
---@field public finishedBy FinishedBy
|
|
---@field public requiredLevel Level
|
|
---@field public questLevel Level
|
|
---@field public requiredRaces number @bitmask
|
|
---@field public requiredClasses number @bitmask
|
|
---@field public objectivesText string[]
|
|
---@field public triggerEnd { [1]: string, [2]: table<AreaId, CoordPair[]>}
|
|
---@field public objectives RawObjectives
|
|
---@field public sourceItemId ItemId
|
|
---@field public preQuestGroup QuestId[]
|
|
---@field public preQuestSingle QuestId[]
|
|
---@field public childQuests QuestId[]
|
|
---@field public inGroupWith QuestId[]
|
|
---@field public exclusiveTo QuestId[]
|
|
---@field public zoneOrSort ZoneOrSort
|
|
---@field public requiredSkill SkillPair
|
|
---@field public requiredMinRep ReputationPair
|
|
---@field public requiredMaxRep ReputationPair
|
|
---@field public requiredSourceItems ItemId[]
|
|
---@field public nextQuestInChain number
|
|
---@field public questFlags number @bitmask: see https://github.com/cmangos/issues/wiki/Quest_template#questflags
|
|
---@field public specialFlags number @bitmask: 1 = Repeatable, 2 = Needs event, 4 = Monthly reset (req. 1). See https://github.com/cmangos/issues/wiki/Quest_template#specialflags
|
|
---@field public parentQuest QuestId
|
|
---@field public reputationReward ReputationPair[]
|
|
---@field public extraObjectives ExtraObjective[]
|
|
---@field public requiredMaxLevel Level
|
|
local QO = {
|
|
Id = questId
|
|
}
|
|
|
|
-- General filling of the QuestObjective with all database values
|
|
local questKeys = QuestieDB.questKeys
|
|
local stringKey, intKey = next(questKeys)
|
|
while stringKey do
|
|
QO[stringKey] = rawdata[intKey]
|
|
stringKey, intKey = next(questKeys, stringKey)
|
|
end
|
|
|
|
-- Support for index-aware objective mapping (QuestieLearner & custom overrides)
|
|
if rawdata.objIndex then QO.objIndex = rawdata.objIndex end
|
|
|
|
|
|
-- Apply overrides to QO directly (merging objectives if needed)
|
|
if overrideData and rawdata ~= overrideData then
|
|
local _sKey, _iKey = next(questKeys)
|
|
while _sKey do
|
|
local overrideVal = overrideData[_iKey] or overrideData[_sKey]
|
|
if overrideVal ~= nil then
|
|
if _sKey == "objectives" and QO.objectives then
|
|
-- Merge objectives
|
|
local _objIdx, _objList = next(overrideVal)
|
|
while _objIdx do
|
|
if not QO.objectives[_objIdx] then
|
|
QO.objectives[_objIdx] = _objList
|
|
else
|
|
local _id, _data = next(_objList)
|
|
while _id do
|
|
QO.objectives[_objIdx][_id] = _data
|
|
_id, _data = next(_objList, _id)
|
|
end
|
|
end
|
|
_objIdx, _objList = next(overrideVal, _objIdx)
|
|
end
|
|
else
|
|
QO[_sKey] = overrideVal
|
|
end
|
|
end
|
|
_sKey, _iKey = next(questKeys, _sKey)
|
|
end
|
|
if overrideData.objIndex then QO.objIndex = overrideData.objIndex end
|
|
end
|
|
|
|
-- In auto mode, the SavedVariables learner payload should still enrich static DB quests.
|
|
-- This is required for quests like 8325 where the static DB has only a stub record,
|
|
-- but QuestieLearner has the real objective payload in QuestieLearnerDB.global.quests.
|
|
if mode == "auto" and learnerRecord and rawdata ~= learnerRecord then
|
|
local _sKey, _iKey = next(questKeys)
|
|
while _sKey do
|
|
local learnerVal = learnerRecord[_iKey] or learnerRecord[_sKey]
|
|
if learnerVal ~= nil then
|
|
if _sKey == "objectives" and QO.objectives then
|
|
local _objIdx, _objList = next(learnerVal)
|
|
while _objIdx do
|
|
if not QO.objectives[_objIdx] then
|
|
QO.objectives[_objIdx] = _objList
|
|
else
|
|
local _id, _data = next(_objList)
|
|
while _id do
|
|
QO.objectives[_objIdx][_id] = _data
|
|
_id, _data = next(_objList, _id)
|
|
end
|
|
end
|
|
_objIdx, _objList = next(learnerVal, _objIdx)
|
|
end
|
|
elseif QO[_sKey] == nil then
|
|
QO[_sKey] = learnerVal
|
|
end
|
|
end
|
|
_sKey, _iKey = next(questKeys, _sKey)
|
|
end
|
|
if learnerRecord.objIndex and not QO.objIndex then QO.objIndex = learnerRecord.objIndex end
|
|
end
|
|
|
|
local questLevel, requiredLevel = QuestieLib.GetTbcLevel(questId)
|
|
QO.level = questLevel
|
|
QO.requiredLevel = requiredLevel
|
|
|
|
---@type StartedBy
|
|
local startedBy = QO.startedBy
|
|
QO.Starts = {
|
|
NPC = startedBy and startedBy[1],
|
|
GameObject = startedBy and startedBy[2],
|
|
Item = startedBy and startedBy[3],
|
|
}
|
|
QO.isHidden = rawdata.hidden or QuestieCorrections.hiddenQuests[questId]
|
|
QO.Description = QO.objectivesText
|
|
if QO.specialFlags then
|
|
QO.IsRepeatable = bitband(QO.specialFlags, 1) ~= 0
|
|
end
|
|
|
|
QO.IsComplete = _IsComplete
|
|
|
|
---@type FinishedBy
|
|
local finishedBy = QO.finishedBy
|
|
if finishedBy and finishedBy[1] then
|
|
local _id, id = next(finishedBy[1])
|
|
while _id do
|
|
if id then
|
|
QO.Finisher = {
|
|
Type = "monster",
|
|
Id = id,
|
|
---@type Name @We have to hard-type it here because of the function
|
|
Name = QuestieDB.QueryNPCSingle(id, "name")
|
|
}
|
|
end
|
|
_id, id = next(finishedBy[1], _id)
|
|
end
|
|
end
|
|
if finishedBy and finishedBy[2] then
|
|
local _id, id = next(finishedBy[2])
|
|
while _id do
|
|
if id then
|
|
-- Some custom servers (like Ascension) incorrectly place Item IDs in the GameObject finisher array.
|
|
-- We verify the object actually exists in the ObjectDB before assigning it as a Finisher,
|
|
-- to prevent "rawdata is nil for objectID" errors during Arrow/Map updates.
|
|
local objectName = QuestieDB.QueryObjectSingle(id, "name")
|
|
if objectName then
|
|
QO.Finisher = {
|
|
Type = "object",
|
|
Id = id,
|
|
Name = objectName
|
|
}
|
|
else
|
|
Questie:Debug(Questie.DEBUG_INFO, "[QuestieDB:GetQuest] Ignored invalid object finisher ID:", id, "for quest:", questId)
|
|
end
|
|
end
|
|
_id, id = next(finishedBy[2], _id)
|
|
end
|
|
end
|
|
|
|
--- to differentiate from the current quest log info.
|
|
--- Quest objectives generated from DB+Corrections.
|
|
--- Data itself is for example for monster type { Type = "monster", Id = 16518, Text = "Nestlewood Owlkin inoculated" }
|
|
---@type Objective[]
|
|
QO.ObjectiveData = {}
|
|
|
|
-- Support for index-aware objective mapping (QuestieLearner & custom overrides)
|
|
-- This handles "kill-credit" scenarios and prevents "UI swapping" by pinning
|
|
-- entities to the specific quest log objective index 'j'.
|
|
if QO.objIndex then
|
|
local _index, _data = next(QO.objIndex)
|
|
while _index do
|
|
if _data and _data.id then
|
|
QO.ObjectiveData[_index] = {
|
|
Type = _data.type or "monster",
|
|
Id = _data.id,
|
|
Text = _data.text or "",
|
|
-- Store original mapping for reference
|
|
_isIndexMapped = true,
|
|
}
|
|
end
|
|
_index, _data = next(QO.objIndex, _index)
|
|
end
|
|
end
|
|
|
|
---@type RawObjectives
|
|
local objectives = QO.objectives
|
|
if objectives then
|
|
if objectives[1] then
|
|
local _creatureObjective, creatureObjective = next(objectives[1])
|
|
while _creatureObjective do
|
|
if creatureObjective then
|
|
-- Only insert if not already mapped via objIndex
|
|
local alreadyMapped = false
|
|
if QO.objIndex then
|
|
for _, existing in pairs(QO.ObjectiveData) do
|
|
if existing.Id == creatureObjective[1] then
|
|
alreadyMapped = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if not alreadyMapped then
|
|
---@type NpcObjective
|
|
table.insert(QO.ObjectiveData, {
|
|
Type = "monster",
|
|
Id = creatureObjective[1],
|
|
Text = creatureObjective[2],
|
|
HideCondition = creatureObjective[3],
|
|
})
|
|
end
|
|
end
|
|
_creatureObjective, creatureObjective = next(objectives[1], _creatureObjective)
|
|
end
|
|
end
|
|
if objectives[2] then
|
|
local _objectObjective, objectObjective = next(objectives[2])
|
|
while _objectObjective do
|
|
if objectObjective then
|
|
---@type ObjectObjective
|
|
table.insert(QO.ObjectiveData, {
|
|
Type = "object",
|
|
Id = objectObjective[1],
|
|
Text = objectObjective[2],
|
|
HideCondition = objectObjective[3],
|
|
})
|
|
end
|
|
_objectObjective, objectObjective = next(objectives[2], _objectObjective)
|
|
end
|
|
end
|
|
if objectives[3] then
|
|
local _itemObjective, itemObjective = next(objectives[3])
|
|
while _itemObjective do
|
|
if itemObjective then
|
|
---@type ItemObjective
|
|
table.insert(QO.ObjectiveData, {
|
|
Type = "item",
|
|
Id = itemObjective[1],
|
|
Text = itemObjective[2],
|
|
HideCondition = itemObjective[3],
|
|
})
|
|
end
|
|
_itemObjective, itemObjective = next(objectives[3], _itemObjective)
|
|
end
|
|
end
|
|
if objectives[4] then
|
|
---@type ReputationObjective
|
|
table.insert(QO.ObjectiveData, {
|
|
Type = "reputation",
|
|
Id = objectives[4][1],
|
|
RequiredRepValue = objectives[4][2]
|
|
})
|
|
end
|
|
if objectives[5] and type(objectives[5]) == "table" and table.getn(objectives[5]) > 0 then
|
|
local _creditObjective, creditObjective = next(objectives[5])
|
|
while _creditObjective do
|
|
---@type KillObjective
|
|
local killCreditObjective = {
|
|
Type = "killcredit",
|
|
IdList = creditObjective[1],
|
|
RootId = creditObjective[2],
|
|
Id = creditObjective[2] or (creditObjective[1] and creditObjective[1][1]) or 0,
|
|
Text = creditObjective[3]
|
|
}
|
|
|
|
--? There are quest(s) which have the killCredit at first so we need to switch them
|
|
-- Place the kill credit objective first
|
|
if QuestieCorrections.killCreditObjectiveFirst[questId] then
|
|
tinsert(QO.ObjectiveData, 1, killCreditObjective);
|
|
else
|
|
tinsert(QO.ObjectiveData, killCreditObjective);
|
|
end
|
|
_creditObjective, creditObjective = next(objectives[5], _creditObjective)
|
|
end
|
|
end
|
|
if objectives[6] then
|
|
local index, spellObjective = next(objectives[6])
|
|
while index do
|
|
if spellObjective then
|
|
---@type SpellObjective
|
|
table.insert(QO.ObjectiveData, {
|
|
Type = "spell",
|
|
Id = spellObjective[1],
|
|
Text = spellObjective[2],
|
|
ItemSourceId = spellObjective[3],
|
|
})
|
|
QO.SpellItemId = spellObjective[3]
|
|
end
|
|
index, spellObjective = next(objectives[6], index)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Events need to be added at the end of ObjectiveData
|
|
local triggerEnd = QO.triggerEnd
|
|
if triggerEnd then
|
|
---@type TriggerEndObjective
|
|
table.insert(QO.ObjectiveData, {
|
|
Type = "event",
|
|
Text = triggerEnd[1],
|
|
Coordinates = triggerEnd[2]
|
|
})
|
|
end
|
|
|
|
local preQuestGroup = QO.preQuestGroup
|
|
local preQuestSingle = QO.preQuestSingle
|
|
if preQuestGroup and preQuestSingle and next(preQuestGroup) and next(preQuestSingle) then
|
|
Questie:Debug(Questie.DEBUG_CRITICAL, "ERRRRORRRRRRR not mutually exclusive for questID:", questId)
|
|
end
|
|
|
|
--- Quest objectives generated from quest log in QuestieQuest.lua -> QuestieQuest:PopulateQuestLogInfo(quest)
|
|
--- Includes also icons drawn to maps, and other stuff.
|
|
---@type table<ObjectiveIndex, QuestObjective>
|
|
QO.Objectives = {}
|
|
|
|
QO.SpecialObjectives = {}
|
|
|
|
---@type ItemId[]
|
|
local requiredSourceItems = QO.requiredSourceItems
|
|
if requiredSourceItems then
|
|
local _itemId, itemId = next(requiredSourceItems)
|
|
while _itemId do
|
|
if itemId then
|
|
-- Make sure requiredSourceItems aren't already an objective
|
|
local itemObjPresent = false
|
|
if objectives and objectives[3] then
|
|
local _itemObjective, itemObjective = next(objectives[3])
|
|
while _itemObjective do
|
|
if itemObjective then
|
|
if itemId == itemObjective[1] then
|
|
itemObjPresent = true
|
|
break
|
|
end
|
|
end
|
|
_itemObjective, itemObjective = next(objectives[3], _itemObjective)
|
|
end
|
|
end
|
|
|
|
-- Make an objective for requiredSourceItem
|
|
if not itemObjPresent then
|
|
QO.SpecialObjectives[itemId] = {
|
|
Type = "item",
|
|
Id = itemId,
|
|
---@type string @We have to hard-type it here because of the function
|
|
Description = QuestieDB.QueryItemSingle(itemId, "name")
|
|
}
|
|
end
|
|
end
|
|
_itemId, itemId = next(requiredSourceItems, _itemId)
|
|
end
|
|
end
|
|
|
|
---@type ExtraObjective[]
|
|
local extraObjectives = QO.extraObjectives
|
|
if extraObjectives then
|
|
local index, o = next(extraObjectives)
|
|
while index do
|
|
local specialObjective = {
|
|
Icon = o[2],
|
|
Description = o[3],
|
|
RealObjectiveIndex = o[4],
|
|
HideCondition = o[6],
|
|
}
|
|
if o[1] then -- custom spawn
|
|
specialObjective.spawnList = {{
|
|
Name = o[3],
|
|
Spawns = o[1],
|
|
Icon = o[2],
|
|
GetIconScale = _GetIconScale,
|
|
IconScale = _GetIconScale(),
|
|
}}
|
|
end
|
|
if o[5] then -- db ref
|
|
specialObjective.Type = o[5][1][1]
|
|
specialObjective.Id = o[5][1][2]
|
|
local spawnList = {}
|
|
|
|
local _ref, ref = next(o[5])
|
|
while _ref do
|
|
local callFn = _QuestieQuest.objectiveSpawnListCallTable[ref[1]]
|
|
local spawnResult = callFn and callFn(ref[2], specialObjective)
|
|
if spawnResult then
|
|
local k, v = next(spawnResult)
|
|
while k do
|
|
-- we want to be able to override the icon in the corrections (e.g. Questie.ICON_TYPE_OBJECT on objects instead of Questie.ICON_TYPE_LOOT)
|
|
v.Icon = o[2]
|
|
spawnList[k] = v
|
|
k, v = next(spawnResult, k)
|
|
end
|
|
end
|
|
_ref, ref = next(o[5], _ref)
|
|
end
|
|
|
|
specialObjective.spawnList = spawnList
|
|
end
|
|
QO.SpecialObjectives[index] = specialObjective
|
|
index, o = next(extraObjectives, index)
|
|
end
|
|
end
|
|
|
|
_QuestieDB.questCache[questId] = QO
|
|
return QO
|
|
end
|
|
|
|
QuestieDB._CreatureLevelCache = {}
|
|
---@param quest Quest
|
|
---@return table<string, table> @List of creature names with their min-max level and rank
|
|
function QuestieDB:GetCreatureLevels(quest)
|
|
if quest and QuestieDB._CreatureLevelCache[quest.Id] then
|
|
return QuestieDB._CreatureLevelCache[quest.Id]
|
|
end
|
|
local creatureLevels = {}
|
|
|
|
local function _CollectCreatureLevels(npcIds)
|
|
local _npcId, npcId = next(npcIds)
|
|
while _npcId do
|
|
local npc = QuestieDB:GetNPC(npcId)
|
|
if npc and not creatureLevels[npc.name] then
|
|
creatureLevels[npc.name] = {npc.minLevel, npc.maxLevel, npc.rank}
|
|
end
|
|
_npcId, npcId = next(npcIds, _npcId)
|
|
end
|
|
end
|
|
|
|
if quest.objectives then
|
|
if quest.objectives[1] then -- Killing creatures
|
|
local _creatureObjective, creatureObjective = next(quest.objectives[1])
|
|
while _creatureObjective do
|
|
local npcId = creatureObjective[1]
|
|
_CollectCreatureLevels({npcId})
|
|
_creatureObjective, creatureObjective = next(quest.objectives[1], _creatureObjective)
|
|
end
|
|
end
|
|
if quest.objectives[3] then -- Looting items from creatures
|
|
local _itemObjective, itemObjective = next(quest.objectives[3])
|
|
while _itemObjective do
|
|
local itemId = itemObjective[1]
|
|
local npcIds = QuestieDB.QueryItemSingle(itemId, "npcDrops")
|
|
if npcIds then
|
|
_CollectCreatureLevels(npcIds)
|
|
end
|
|
_itemObjective, itemObjective = next(quest.objectives[3], _itemObjective)
|
|
end
|
|
end
|
|
if quest.objectives[5] then -- Kill credit objectives
|
|
local _creditObjective, creditObjective = next(quest.objectives[5])
|
|
while _creditObjective do
|
|
local npcIds = creditObjective[1]
|
|
if npcIds then
|
|
_CollectCreatureLevels(npcIds)
|
|
end
|
|
_creditObjective, creditObjective = next(quest.objectives[5], _creditObjective)
|
|
end
|
|
end
|
|
end
|
|
if quest.Id then
|
|
QuestieDB._CreatureLevelCache[quest.Id] = creatureLevels
|
|
end
|
|
return creatureLevels
|
|
end
|
|
|
|
local playerFaction = UnitFactionGroup("player")
|
|
local factionReactions = {
|
|
A = (playerFaction == "Alliance") or nil,
|
|
H = (playerFaction == "Horde") or nil,
|
|
AH = true,
|
|
}
|
|
--
|
|
--
|
|
--function __TEST()
|
|
-- local questsWithoutTriggerEnd = {
|
|
-- 869,
|
|
-- 944,
|
|
-- 1448,
|
|
-- 6185,
|
|
-- 9260,
|
|
-- 9261,
|
|
-- 9262,
|
|
-- 9263,
|
|
-- 9264,
|
|
-- 9265,
|
|
-- 12032,
|
|
-- 12036,
|
|
-- 12816,
|
|
-- 12817,
|
|
-- 13564,
|
|
-- 13639,
|
|
-- 13881,
|
|
-- 13961,
|
|
-- 14066,
|
|
-- 14165,
|
|
-- 14389,
|
|
-- 24452,
|
|
-- 24618,
|
|
-- 25081,
|
|
-- 25325,
|
|
-- 25621,
|
|
-- 25715,
|
|
-- 25930,
|
|
-- 26258,
|
|
-- 26512,
|
|
-- 26930,
|
|
-- 26975,
|
|
-- 27007,
|
|
-- 27044,
|
|
-- 27152,
|
|
-- 27341,
|
|
-- 27349,
|
|
-- 27610,
|
|
-- 27704,
|
|
-- 28228,
|
|
-- 28635,
|
|
-- 28732,
|
|
-- 29392,
|
|
-- 29415,
|
|
-- 29536,
|
|
-- 29539,
|
|
-- }
|
|
--
|
|
-- local corrections = {}
|
|
--
|
|
-- -- Read QuestieDB to check if the quest has a triggerEnd
|
|
-- for _, questId in pairs(questsWithoutTriggerEnd) do
|
|
-- local quest = QuestieDB.GetQuest(questId)
|
|
-- if (not quest) then
|
|
-- print("Quest " .. questId .. " not found in QuestieDB")
|
|
-- corrections[questId] = true
|
|
-- else
|
|
-- if quest.triggerEnd then
|
|
-- print("Quest " .. questId .. " has a triggerEnd")
|
|
-- else
|
|
-- print("Quest " .. questId .. " does not have a triggerEnd")
|
|
-- corrections[questId] = true
|
|
-- end
|
|
-- end
|
|
-- end
|
|
--
|
|
-- Questie.db.global.__TEST = corrections
|
|
--end
|
|
local a = {
|
|
[869] = true,
|
|
[13564] = true,
|
|
[13639] = true,
|
|
[13881] = true,
|
|
[13961] = true,
|
|
[14165] = true,
|
|
|
|
[27610] = true,
|
|
[27044] = true,
|
|
[26930] = true,
|
|
[14066] = true,
|
|
[28635] = true,
|
|
[25930] = true,
|
|
[27007] = true,
|
|
[14389] = true,
|
|
[27704] = true,
|
|
[24618] = true,
|
|
[26258] = true,
|
|
[26512] = true,
|
|
[29539] = true,
|
|
[27341] = true,
|
|
[29536] = true,
|
|
[29415] = true,
|
|
[29392] = true,
|
|
[28732] = true,
|
|
[28228] = true,
|
|
[27152] = true,
|
|
[27349] = true,
|
|
[24452] = true,
|
|
[25325] = true,
|
|
[26975] = true,
|
|
[25621] = true,
|
|
[25715] = true,
|
|
[25081] = true,
|
|
}
|
|
|
|
---@param npcId number
|
|
---@return table
|
|
function QuestieDB:GetNPC(npcId)
|
|
if self ~= QuestieDB then
|
|
npcId = self
|
|
end
|
|
npcId = tonumber(npcId)
|
|
if not npcId or npcId == 0 then
|
|
return nil
|
|
end
|
|
if _QuestieDB.npcCache[npcId] then
|
|
return _QuestieDB.npcCache[npcId]
|
|
end
|
|
|
|
local settings = _GetLearnerSettings()
|
|
local mode = settings and settings.dataSourceMode or "auto"
|
|
local learnerRecord = _GetLearnerRecord("npcs", npcId)
|
|
local rawdata
|
|
local override
|
|
override = QuestieDB.npcDataOverrides and (QuestieDB.npcDataOverrides[npcId] or QuestieDB.npcDataOverrides[tostring(npcId)])
|
|
if mode == "learner" or QuestieDB:IsStoreMissing("npcData") then
|
|
rawdata = learnerRecord
|
|
if not rawdata then
|
|
rawdata = override
|
|
end
|
|
else
|
|
rawdata = QuestieDB.QueryNPC(npcId, QuestieDB._npcAdapterQueryOrder)
|
|
if not rawdata and learnerRecord and mode == "auto" then
|
|
-- Only "auto" overlays learner data on top of the static DB. "static"
|
|
-- and "none" must never silently fall back to learner records.
|
|
rawdata = learnerRecord
|
|
end
|
|
end
|
|
|
|
if not rawdata and not override then
|
|
if not QuestieDB.private.missingNpcLog[npcId] then
|
|
QuestieDB.private.missingNpcLog[npcId] = true
|
|
Questie:Debug(Questie.DEBUG_INFO, "[QuestieDB:GetNPC] data not found for npcID:", npcId)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local npcKeys = QuestieDB.npcKeys
|
|
local npc = {
|
|
id = npcId,
|
|
type = "monster",
|
|
}
|
|
|
|
if override then
|
|
-- Prefer override data (corrections); _MergeOverride handles both
|
|
-- string-keyed and numeric-keyed override formats. This is essential
|
|
-- for AscensionDB and QuestieLearner data which use numeric keys.
|
|
_MergeOverride(npc, override, rawdata, npcKeys)
|
|
else
|
|
-- Use standard DB data
|
|
local stringKey, intKey = next(npcKeys)
|
|
while stringKey do
|
|
npc[stringKey] = rawdata[intKey]
|
|
stringKey, intKey = next(npcKeys, stringKey)
|
|
end
|
|
end
|
|
|
|
-- AscensionDB hand-curates spawn coordinates for Ascension-specific zones
|
|
-- (e.g. Sunstrider's Mana Wyrm 15274). When AscensionDB owns this NPC's spawns,
|
|
-- use ONLY those curated coords and discard the learner-record zones that
|
|
-- _MergeOverride deep-merged in: the learner's Sunstrider coords are frequently
|
|
-- stored under the wrong map/zone (Sunstrider 1241 vs Eversong space), which
|
|
-- otherwise leaks in as pins in the wrong corner of the map. In learner mode
|
|
-- rawdata IS the learner record, so the bad zone survives the merge because
|
|
-- AscensionDB has no entry to displace it — this check removes it. Direct
|
|
-- ascensionOverrideKeys lookup (NOT IsAscensionProtected, which returns false
|
|
-- in learner mode).
|
|
if override and QuestieDB.ascensionOverrideKeys and QuestieDB.ascensionOverrideKeys["NPC"]
|
|
and QuestieDB.ascensionOverrideKeys["NPC"][npcId]
|
|
and QuestieDB.ascensionOverrideKeys["NPC"][npcId][7] then
|
|
local curated = override[7] or override.spawns
|
|
if curated and next(curated) then
|
|
npc.spawns = CopySpawnTable(curated)
|
|
end
|
|
end
|
|
|
|
local friendlyToFaction = npc.friendlyToFaction
|
|
npc.friendly = (not friendlyToFaction) and true or factionReactions[friendlyToFaction]
|
|
|
|
if (not npc.spawns or next(npc.spawns) == nil) and mode == "learner" then
|
|
local guidSpawns = rawdata[8]
|
|
local learnedSpawns = _BuildSpawnTableFromGuidEvidence(guidSpawns)
|
|
if learnedSpawns then
|
|
npc.spawns = learnedSpawns
|
|
end
|
|
end
|
|
|
|
_QuestieDB.npcCache[npcId] = npc
|
|
return npc
|
|
end
|
|
|
|
--[[
|
|
https://github.com/cmangos/issues/wiki/AreaTable.dbc
|
|
Example to differentiate between Dungeon and Zone infront of a Dungeon:
|
|
1337 Uldaman = The Dungeon (MapID ~= 0, AreaID = 0)
|
|
1517 Uldaman = Cave infront of the Dungeon (MapID = 0, AreaID = 3 (Badlands))
|
|
|
|
Check `l10n.zoneLookup` for the available IDs
|
|
]]
|
|
---@param zoneId number
|
|
---@return table
|
|
function QuestieDB:GetQuestsByZoneId(zoneId)
|
|
if not zoneId then
|
|
return nil;
|
|
end
|
|
-- is in cache return that
|
|
if _QuestieDB.zoneCache[zoneId] then
|
|
return _QuestieDB.zoneCache[zoneId]
|
|
end
|
|
local zoneQuests = {};
|
|
local alternativeZoneID = ZoneDB:GetAlternativeZoneId(zoneId)
|
|
|
|
local function processQuest(qid)
|
|
if zoneQuests[qid] then
|
|
return
|
|
end
|
|
local quest = QuestieDB.GetQuest(qid);
|
|
if quest then
|
|
if quest.zoneOrSort > 0 then
|
|
if (quest.zoneOrSort == zoneId or (alternativeZoneID and quest.zoneOrSort == alternativeZoneID)) then
|
|
zoneQuests[qid] = quest;
|
|
end
|
|
elseif quest.Starts.NPC and (not zoneQuests[qid]) then
|
|
local npc = QuestieDB:GetNPC(quest.Starts.NPC[1]);
|
|
if npc and npc.friendly and npc.spawns then
|
|
local zone, _ = next(npc.spawns)
|
|
while zone do
|
|
if zone == zoneId or (alternativeZoneID and zone == alternativeZoneID) then
|
|
zoneQuests[qid] = quest;
|
|
break
|
|
end
|
|
zone, _ = next(npc.spawns, zone)
|
|
end
|
|
end
|
|
elseif quest.Starts.GameObject and (not zoneQuests[qid]) then
|
|
local obj = QuestieDB:GetObject(quest.Starts.GameObject[1]);
|
|
if obj and obj.spawns then
|
|
local zone, _ = next(obj.spawns)
|
|
while zone do
|
|
if zone == zoneId or (alternativeZoneID and zone == alternativeZoneID) then
|
|
zoneQuests[qid] = quest;
|
|
break
|
|
end
|
|
zone, _ = next(obj.spawns, zone)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- loop over all quests to populate a zone
|
|
local dataSource = QuestieDB.QuestPointers or QuestieDB.questData
|
|
if dataSource then
|
|
local qid, _ = next(dataSource)
|
|
while qid do
|
|
processQuest(qid)
|
|
qid, _ = next(dataSource, qid)
|
|
end
|
|
end
|
|
|
|
-- Loop over plugin-injected quest overrides
|
|
if QuestieDB.questDataOverrides then
|
|
local qid, _ = next(QuestieDB.questDataOverrides)
|
|
while qid do
|
|
processQuest(qid)
|
|
qid, _ = next(QuestieDB.questDataOverrides, qid)
|
|
end
|
|
end
|
|
|
|
_QuestieDB.zoneCache[zoneId] = zoneQuests;
|
|
return zoneQuests;
|
|
end
|
|
|
|
---------------------------------------------------------------------------------------------------
|
|
-- Modifications to objectDB
|
|
function _QuestieDB:DeleteGatheringNodes()
|
|
local prune = { -- gathering nodes
|
|
1617,1618,1619,1620,1621,1622,1623,1624,1628, -- herbs
|
|
|
|
1731,1732,1733,1734,1735,123848,150082,175404,176643,177388,324,150079,176645,2040,123310 -- mining
|
|
}
|
|
local objectSpawnsKey = QuestieDB.objectKeys.spawns
|
|
for i=1, table.getn(prune) do
|
|
local id = prune[i]
|
|
if QuestieDB.objectData[id] then
|
|
QuestieDB.objectData[id][objectSpawnsKey] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
---------------------------------------------------------------------------------------------------
|
|
-- Modifications to questDB
|
|
|
|
function _QuestieDB:CheckAchievementRequirements(questId)
|
|
-- So far the only Quests that we know of that requires an earned Achievement are the ones offered by:
|
|
-- https://www.wowhead.com/wotlk/npc=35094/crusader-silverdawn
|
|
-- Get Kraken (14108)
|
|
-- The Fate Of The Fallen (14107)
|
|
-- This NPC requires these earned Achievements baseed on a Players home faction:
|
|
-- https://www.wowhead.com/wotlk/achievement=2817/exalted-argent-champion-of-the-alliance
|
|
-- https://www.wowhead.com/wotlk/achievement=2816/exalted-argent-champion-of-the-horde
|
|
if questId == 14101 or questId == 14102 or questId == 14104 or questId == 14105 or questId == 14107 or questId == 14108 then
|
|
local retN = QuestieCompat.Is335 and 4 or 13
|
|
if select(retN, GetAchievementInfo(2817)) or select(retN, GetAchievementInfo(2816)) then
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
end
|
|
|
|
function _QuestieDB:HideClassAndRaceQuests()
|
|
local questKeys = QuestieDB.questKeys
|
|
local _, entry = next(QuestieDB.questData)
|
|
while entry do
|
|
-- check requirements, set hidden flag if not met
|
|
local requiredClasses = entry[questKeys.requiredClasses]
|
|
if (requiredClasses) and (requiredClasses ~= 0) then
|
|
if (not QuestiePlayer.HasRequiredClass(requiredClasses)) then
|
|
entry.hidden = true
|
|
end
|
|
end
|
|
local requiredRaces = entry[questKeys.requiredRaces]
|
|
if (requiredRaces) and (requiredRaces ~= 0) and (requiredRaces ~= 255) then
|
|
if (not QuestiePlayer.HasRequiredRace(requiredRaces)) then
|
|
entry.hidden = true
|
|
end
|
|
end
|
|
_, entry = next(QuestieDB.questData, _)
|
|
end
|
|
Questie:Debug(Questie.DEBUG_DEVELOP, "Other class and race quests hidden");
|
|
end
|
|
|
|
-- This function is intended for usage with Gossip and Greeting frames, where there's a list of quests but no QuestIDs are
|
|
-- obtainable until entering the specific quest dialog.
|
|
-- This is a bruteforce method for obtaining a QuestID with no input other than a quest name, and the ID of the questgiver.
|
|
-- It compares the name of the quest entry with the names of every quest that questgiver can either start or end.
|
|
---@param name string? @The name of the quest entry
|
|
---@param questgiverGUID string? @Should be UnitGUID("questnpc")
|
|
---@param questStarter boolean @Should be True if this is an available quest, False if this is an "active" quest (quest ender)
|
|
---@return number
|
|
function QuestieDB.GetQuestIDFromName(name, questgiverGUID, questStarter)
|
|
local questID = 0 -- worst case, we end up returning an ID of 0 if we can't find a match; any function relying on this one should handle 0 cleanly
|
|
if questgiverGUID then
|
|
local questgiverID = tonumber(questgiverGUID:match("-(%d+)-%x+$"), 10)
|
|
local unit_type = strsplit("-", questgiverGUID)
|
|
local questsStarted
|
|
local questsEnded
|
|
if unit_type == "Creature" then -- if questgiver is an NPC
|
|
questsStarted = QuestieDB.QueryNPCSingle(questgiverID, "questStarts")
|
|
questsEnded = QuestieDB.QueryNPCSingle(questgiverID, "questEnds")
|
|
elseif unit_type == "GameObject" then -- if questgiver is an object (it's rare for an object to have a gossip/greeting frame, but Wanted Boards exist; see object 2713)
|
|
questsStarted = QuestieDB.QueryObjectSingle(questgiverID, "questStarts")
|
|
questsEnded = QuestieDB.QueryObjectSingle(questgiverID, "questEnds")
|
|
else
|
|
return questID; -- If the questgiver is not an NPC or object, bail!
|
|
end
|
|
-- iterate through every questEnds entry in our questgiver's DB, and check if each quest name matches this greeting frame entry
|
|
if questStarter == true then
|
|
if questsStarted then
|
|
local _id, id = next(questsStarted)
|
|
while _id do
|
|
if (name == QuestieDB.QueryQuestSingle(id, "name")) and (QuestieDB.IsDoable(id)) then
|
|
-- the QuestieDB.IsDoable check is important to filter out identically named quests
|
|
questID = id
|
|
end
|
|
_id, id = next(questsStarted, _id)
|
|
end
|
|
elseif Questie.IsSoD == false then -- don't print these errors in SoD, as we expect missing data when new quests release; debug offers will handle these scenarios instead
|
|
Questie:Error("Database mismatch! No entries found that match quest name. Please report this on Github or Discord!")
|
|
Questie:Error("Queststarter is: " .. unit_type .. " " .. questgiverID)
|
|
Questie:Error("Quest name is: " .. name)
|
|
Questie:Error("Client info is: " .. GetBuildInfo() .. "; " .. QuestieLib:GetAddonVersionString())
|
|
end
|
|
else
|
|
if questsEnded then
|
|
local _id, id = next(questsEnded)
|
|
while _id do
|
|
if (name == QuestieDB.QueryQuestSingle(id, "name")) and (QuestieDB.IsDoable(id)) and QuestiePlayer.currentQuestlog[id] then
|
|
questID = id
|
|
end
|
|
_id, id = next(questsEnded, _id)
|
|
end
|
|
elseif Questie.IsSoD == false then -- don't print these errors in SoD, as we expect missing data when new quests release; debug offers will handle these scenarios instead
|
|
Questie:Error("Database mismatch! No entries found that match quest name. Please report this on Github or Discord!")
|
|
Questie:Error("Questender is: " .. unit_type .. " " .. questgiverID)
|
|
Questie:Error("Quest name is: " .. name)
|
|
Questie:Error("Client info is: " .. GetBuildInfo() .. "; " .. QuestieLib:GetAddonVersionString())
|
|
end
|
|
end
|
|
end
|
|
return questID;
|
|
end
|
|
|
|
QuestieDB.waypointPresets = {
|
|
ORGRIMS_HAMMER = {[ZoneDB.zoneIDs.ICECROWN]={{{62.37,30.60},{61.93,30.93},{61.48,31.24},{61.08,31.55},{60.74,31.92},{60.46,32.44},{60.26,33.11},{60.14,33.85},{60.11,34.63},{60.17,35.35},{60.31,36.01},{60.56,36.66},{60.84,37.33},{61.15,38.00},{61.44,38.67},{61.71,39.28},{62.00,39.92},{62.31,40.55},{62.60,41.20},{62.90,41.83},{63.05,42.20},{63.33,42.85},{63.58,43.53},{63.85,44.19},{64.08,44.86},{64.33,45.50},{64.45,45.87},{64.69,46.56},{64.94,47.21},{65.16,47.87},{65.43,48.51},{65.71,49.15},{66.03,49.77},{66.36,50.46},{66.72,51.10},{67.07,51.67},{67.41,52.08},{67.82,52.37},{68.31,52.47},{68.80,52.38},{69.23,51.98},{69.45,51.56},{69.57,51.13},{69.67,50.59},{69.73,49.96},{69.77,49.26},{69.79,48.48},{69.80,47.62},{69.79,46.68},{69.79,45.68},{69.78,44.90},{69.78,44.25},{69.76,43.55},{69.75,42.80},{69.72,42.01},{69.70,41.20},{69.67,40.38},{69.64,39.54},{69.61,38.71},{69.58,37.88},{69.55,37.07},{69.52,36.28},{69.49,35.54},{69.46,34.83},{69.45,34.18},{69.42,33.50},{69.41,32.46},{69.42,31.52},{69.45,30.67},{69.47,29.92},{69.48,29.25},{69.46,28.65},{69.42,28.12},{69.35,27.83},{69.11,27.19},{68.71,26.77},{68.23,26.54},{67.71,26.51},{67.24,26.55},{66.81,26.76},{66.35,27.09},{65.90,27.52},{65.44,27.95},{65.01,28.39},{64.58,28.73},{64.16,29.10},{63.74,29.43},{63.34,29.79},{62.94,30.11},{62.65,30.37},{62.37,30.60}}}},
|
|
THE_SKYBREAKER = {[ZoneDB.zoneIDs.ICECROWN]={{{63.59,52.34},{63.44,51.88},{63.30,51.52},{63.15,51.19},{63.01,50.85},{62.85,50.52},{62.68,50.17},{62.50,49.78},{62.31,49.36},{62.09,48.88},{61.86,48.33},{61.81,48.20},{61.67,47.88},{61.52,47.52},{61.37,47.13},{61.19,46.74},{61.02,46.32},{60.84,45.88},{60.65,45.44},{60.46,44.99},{60.28,44.53},{60.09,44.08},{59.90,43.63},{59.72,43.18},{59.54,42.75},{59.36,42.34},{59.20,41.94},{59.04,41.56},{58.89,41.22},{58.75,40.87},{58.52,40.30},{58.32,39.77},{58.14,39.27},{57.97,38.80},{57.81,38.39},{57.65,38.03},{57.49,37.72},{57.31,37.50},{57.12,37.34},{56.87,37.29},{56.57,37.39},{56.27,37.60},{55.97,37.88},{55.72,38.23},{55.53,38.61},{55.43,38.95},{55.43,39.09},{55.46,39.49},{55.52,39.92},{55.62,40.39},{55.76,40.88},{55.89,41.38},{56.02,41.85},{56.17,42.25},{56.33,42.68},{56.51,43.13},{56.70,43.56},{56.88,43.99},{57.05,44.39},{57.22,44.74},{57.42,45.15},{57.64,45.52},{57.83,45.81},{58.03,46.14},{58.23,46.56},{58.41,46.90},{58.60,47.25},{58.80,47.62},{59.00,48.03},{59.19,48.46},{59.36,48.84},{59.53,49.22},{59.69,49.63},{59.86,50.04},{60.03,50.46},{60.19,50.90},{60.36,51.32},{60.51,51.74},{60.65,52.17},{60.79,52.59},{60.94,53.02},{61.07,53.46},{61.23,53.89},{61.39,54.30},{61.55,54.72},{61.70,55.18},{61.88,55.65},{62.05,56.14},{62.23,56.58},{62.43,56.95},{62.65,57.21},{62.87,57.30},{62.95,57.27},{63.22,57.16},{63.52,56.97},{63.81,56.68},{64.07,56.32},{64.26,55.91},{64.33,55.47},{64.30,55.11},{64.25,54.72},{64.16,54.30},{64.04,53.84},{63.91,53.36},{63.76,52.88},{63.59,52.34}}}},
|
|
}
|
|
|
|
|
|
-- =========================
|
|
-- Ascension custom DB merge
|
|
-- =========================
|
|
local function _Asc_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, "[AscensionDB] 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, "[AscensionDB] executing chunk failed for " .. tostring(label) .. ": " .. tostring(tbl))
|
|
end
|
|
return nil
|
|
end
|
|
return tbl
|
|
end
|
|
return data
|
|
end
|
|
|
|
local function _Asc_ProtectField(dbType, id, key)
|
|
QuestieDB.ascensionOverrideKeys = QuestieDB.ascensionOverrideKeys or {}
|
|
QuestieDB.ascensionOverrideKeys[dbType] = QuestieDB.ascensionOverrideKeys[dbType] or {}
|
|
QuestieDB.ascensionOverrideKeys[dbType][id] = QuestieDB.ascensionOverrideKeys[dbType][id] or {}
|
|
QuestieDB.ascensionOverrideKeys[dbType][id][key] = true
|
|
end
|
|
|
|
local function _Asc_MergeInto(dst, src, dbType)
|
|
if type(dst) ~= "table" or type(src) ~= "table" then return end
|
|
local id, entry = next(src)
|
|
while id do
|
|
local existing = dst[id]
|
|
if type(existing) == "table" and type(entry) == "table" then
|
|
local key, value = next(entry)
|
|
while key do
|
|
existing[key] = value
|
|
_Asc_ProtectField(dbType, id, key)
|
|
key, value = next(entry, key)
|
|
end
|
|
else
|
|
dst[id] = entry
|
|
if type(entry) == "table" then
|
|
local key = next(entry)
|
|
while key do
|
|
_Asc_ProtectField(dbType, id, key)
|
|
key = next(entry, key)
|
|
end
|
|
end
|
|
end
|
|
id, entry = next(src, id)
|
|
end
|
|
end
|
|
|
|
function QuestieDB:LoadAscensionQuestData()
|
|
local A = QuestieLoader:ImportModule("AscensionDB")
|
|
if not A then return end
|
|
local data = _Asc_LoadIfString(A.questData, "AscensionDB.questData")
|
|
QuestieDB.questDataOverrides = QuestieDB.questDataOverrides or {}
|
|
_Asc_MergeInto(QuestieDB.questDataOverrides, data, "QUEST")
|
|
end
|
|
|
|
function QuestieDB:LoadAscensionNpcData()
|
|
local A = QuestieLoader:ImportModule("AscensionDB")
|
|
if not A then return end
|
|
local data = _Asc_LoadIfString(A.npcData, "AscensionDB.npcData")
|
|
QuestieDB.npcDataOverrides = QuestieDB.npcDataOverrides or {}
|
|
_Asc_MergeInto(QuestieDB.npcDataOverrides, data, "NPC")
|
|
end
|
|
|
|
function QuestieDB:LoadAscensionObjectData()
|
|
local A = QuestieLoader:ImportModule("AscensionDB")
|
|
if not A then return end
|
|
local data = _Asc_LoadIfString(A.objectData, "AscensionDB.objectData")
|
|
QuestieDB.objectDataOverrides = QuestieDB.objectDataOverrides or {}
|
|
_Asc_MergeInto(QuestieDB.objectDataOverrides, data, "OBJECT")
|
|
end
|
|
|
|
function QuestieDB:LoadAscensionItemData()
|
|
local A = QuestieLoader:ImportModule("AscensionDB")
|
|
if not A then return end
|
|
local data = _Asc_LoadIfString(A.itemData, "AscensionDB.itemData")
|
|
QuestieDB.itemDataOverrides = QuestieDB.itemDataOverrides or {}
|
|
_Asc_MergeInto(QuestieDB.itemDataOverrides, data, "ITEM")
|
|
end
|
|
|
|
|
|
return QuestieDB
|