---@class QuestieCorrections local QuestieCorrections = QuestieLoader:CreateModule("QuestieCorrections") ---@type QuestieDB local QuestieDB = QuestieLoader:ImportModule("QuestieDB") ---@type ZoneDB local ZoneDB = QuestieLoader:ImportModule("ZoneDB") ---@type QuestieLib local QuestieLib = QuestieLoader:ImportModule("QuestieLib") ---@type RamerDouglasPeucker local RamerDouglasPeucker = QuestieLoader:ImportModule("RamerDouglasPeucker") ---@type QuestieEvent local QuestieEvent = QuestieLoader:ImportModule("QuestieEvent") ---@type QuestieQuestBlacklist local QuestieQuestBlacklist = QuestieLoader:ImportModule("QuestieQuestBlacklist") ---@type QuestieNPCBlacklist local QuestieNPCBlacklist = QuestieLoader:ImportModule("QuestieNPCBlacklist") ---@type QuestieItemBlacklist local QuestieItemBlacklist = QuestieLoader:ImportModule("QuestieItemBlacklist") ---@type HardcoreBlacklist local HardcoreBlacklist = QuestieLoader:ImportModule("HardcoreBlacklist") ---@type SeasonOfDiscovery local SeasonOfDiscovery = QuestieLoader:ImportModule("SeasonOfDiscovery") ---@type QuestieQuestFixes local QuestieQuestFixes = QuestieLoader:ImportModule("QuestieQuestFixes") ---@type QuestieClassicQuestReputationFixes local QuestieClassicQuestReputationFixes = QuestieLoader:ImportModule("QuestieClassicQuestReputationFixes") ---@type QuestieNPCFixes local QuestieNPCFixes = QuestieLoader:ImportModule("QuestieNPCFixes") ---@type QuestieItemFixes local QuestieItemFixes = QuestieLoader:ImportModule("QuestieItemFixes") ---@type QuestieObjectFixes local QuestieObjectFixes = QuestieLoader:ImportModule("QuestieObjectFixes") ---@type QuestieTBCQuestFixes local QuestieTBCQuestFixes = QuestieLoader:ImportModule("QuestieTBCQuestFixes") ---@type QuestieTBCNpcFixes local QuestieTBCNpcFixes = QuestieLoader:ImportModule("QuestieTBCNpcFixes") ---@type QuestieTBCItemFixes local QuestieTBCItemFixes = QuestieLoader:ImportModule("QuestieTBCItemFixes") ---@type QuestieTBCObjectFixes local QuestieTBCObjectFixes = QuestieLoader:ImportModule("QuestieTBCObjectFixes") ---@type QuestieWotlkQuestFixes local QuestieWotlkQuestFixes = QuestieLoader:ImportModule("QuestieWotlkQuestFixes") ---@type QuestieWotlkNpcFixes local QuestieWotlkNpcFixes = QuestieLoader:ImportModule("QuestieWotlkNpcFixes") ---@type QuestieWotlkItemFixes local QuestieWotlkItemFixes = QuestieLoader:ImportModule("QuestieWotlkItemFixes") ---@type QuestieWotlkObjectFixes local QuestieWotlkObjectFixes = QuestieLoader:ImportModule("QuestieWotlkObjectFixes") ---@type IsleOfQuelDanas local IsleOfQuelDanas = QuestieLoader:ImportModule("IsleOfQuelDanas") --- Automatic corrections local QuestieItemStartFixes = QuestieLoader:ImportModule("QuestieItemStartFixes") --- COMPATIBILITY --- local C_Timer = QuestieCompat.C_Timer --[[ This file load the corrections of the database files. It is a separate file so we can upstream those changes easier to cmangos and can still update the database files with a script. Most of the corrections can be done by accessing a specific key instead of copying the whole object over and change it. You can find the keys at the beginning of each file (e.g. 'questKeys' are at the beginning of 'questDB.lua'). Further information on how to use this can be found at the wiki https://github.com/Questie/Questie/wiki/Corrections --]] -- flags that can be used in corrections (currently only blacklists) QuestieCorrections.TBC_ONLY = 1 -- Hide only in TBC QuestieCorrections.CLASSIC_ONLY = 2 -- Hide only in Classic QuestieCorrections.WOTLK_ONLY = 3 -- Hide only in Wotlk QuestieCorrections.TBC_AND_WOTLK = 4 -- Hide in TBC and Wotlk QuestieCorrections.SOD_ONLY = 5 -- Hide when *not* Season of Discovery; use for SoD-only quests QuestieCorrections.HIDE_SOD = 6 -- Hide when Season of Discovery; use to hide quests that are not available in SoD QuestieCorrections.CLASSIC_AND_TBC = 7 -- Hide in both Classic and TBC QuestieCorrections.killCreditObjectiveFirst = {} -- Only used for TBC quests -- this function filters a table of values, if the value is TBC_ONLY or CLASSIC_ONLY, set it to true or nil if that case is met ---@generic T ---@param values T ---@return T local function filterExpansion(values) local isClassic = Questie.IsClassic local isTBC = Questie.IsTBC local isWotlk = Questie.IsWotlk local isSoD = Questie.IsSoD for k, v in pairs(values) do if v == QuestieCorrections.WOTLK_ONLY then if isWotlk then values[k] = true else values[k] = nil end elseif v == QuestieCorrections.TBC_ONLY then if isTBC then values[k] = true else values[k] = nil end elseif v == QuestieCorrections.CLASSIC_ONLY then if isTBC or isWotlk then values[k] = nil else values[k] = true end elseif v == QuestieCorrections.TBC_AND_WOTLK then if isTBC or isWotlk then values[k] = true else values[k] = nil end elseif v == QuestieCorrections.SOD_ONLY then if not isSoD then values[k] = true else values[k] = nil end elseif v == QuestieCorrections.HIDE_SOD then if isSoD then values[k] = true else values[k] = nil end elseif v == QuestieCorrections.CLASSIC_AND_TBC then if isClassic or isTBC then values[k] = true else values[k] = nil end end end return values end do local type, assert = type, assert --- Add runtime overrides for the database ---@param override_table table> ---@param new_overrides table> local function addOverride(override_table, new_overrides) assert(type(override_table) == "table", "Override table must be a table!") assert(type(new_overrides) == "table", "New overrides must be a table!") for id, data in pairs(new_overrides) do assert(type(id) == "number", "Override id must be a number!") assert(type(data) == "table", "Override data must be a table!") -- If no override exist assign it if not override_table[id] then override_table[id] = data else -- Override already exists, merge the new data for key, value in pairs(data) do override_table[id][key] = value end end end end function QuestieCorrections:MinimalInit() -- db already compiled -- Classic Era Corrections addOverride(QuestieDB.itemDataOverrides, QuestieItemFixes:LoadFactionFixes()) addOverride(QuestieDB.npcDataOverrides, QuestieNPCFixes:LoadFactionFixes()) addOverride(QuestieDB.objectDataOverrides, QuestieObjectFixes:LoadFactionFixes()) addOverride(QuestieDB.questDataOverrides, QuestieQuestFixes:LoadFactionFixes()) -- TBC Corrections if (Questie.IsTBC or Questie.IsWotlk) then addOverride(QuestieDB.itemDataOverrides, QuestieTBCItemFixes:LoadFactionFixes()) addOverride(QuestieDB.npcDataOverrides, QuestieTBCNpcFixes:LoadFactionFixes()) addOverride(QuestieDB.objectDataOverrides, QuestieTBCObjectFixes:LoadFactionFixes()) addOverride(QuestieDB.questDataOverrides, QuestieTBCQuestFixes:LoadFactionFixes()) end -- WOTLK Corrections if (Questie.IsWotlk) then addOverride(QuestieDB.npcDataOverrides, QuestieWotlkNpcFixes:LoadFactionFixes()) addOverride(QuestieDB.itemDataOverrides, QuestieWotlkItemFixes:LoadFactionFixes()) addOverride(QuestieDB.objectDataOverrides, QuestieWotlkObjectFixes:LoadFactionFixes()) end -- Season of Discovery Corrections if Questie.IsSoD then addOverride(QuestieDB.questDataOverrides, SeasonOfDiscovery:LoadFactionQuestFixes()) end QuestieCorrections.questItemBlacklist = filterExpansion(QuestieItemBlacklist:Load()) QuestieCorrections.questNPCBlacklist = filterExpansion(QuestieNPCBlacklist:Load()) QuestieCorrections.hiddenQuests = filterExpansion(QuestieQuestBlacklist:Load()) -- TBC Quel Danas Blacklist if Questie.db.global.isleOfQuelDanasPhase == IsleOfQuelDanas.MAX_ISLE_OF_QUEL_DANAS_PHASES then for id, hide in pairs(IsleOfQuelDanas.quests[Questie.db.global.isleOfQuelDanasPhase]) do -- This has to be a nil-check, because the value could be false if (QuestieCorrections.hiddenQuests[id] == nil) then QuestieCorrections.hiddenQuests[id] = hide end end end -- Wotlk Blacklist if (Questie.IsWotlk) then -- We only add blacklist if no blacklist entry for the quest already exists for id, hide in pairs(QuestieQuestBlacklist.LoadAutoBlacklistWotlk()) do -- This has to be a nil-check, because the value could be false if (QuestieCorrections.hiddenQuests[id] == nil) then QuestieCorrections.hiddenQuests[id] = hide end end end -- Hardcore Blacklist if (Questie.IsHardcore) then for id, _ in pairs(HardcoreBlacklist:Load()) do QuestieCorrections.hiddenQuests[id] = true end end if QuestieCompat.Is335 then QuestieCompat.LoadBlacklists() end if Questie.db.profile.showEventQuests then C_Timer.After(1, function() -- This is done with a delay because on startup the Blizzard API seems to be -- very slow and therefore the date calculation in QuestieEvents isn't done -- correctly. QuestieEvent:Load() end) end end end ---@param databaseTableName string The name of the QuestieDB field that should be manipulated (e.g. "itemData", "questData") ---@param corrections table All corrections for the given databaseTableName (e.g. all quest corrections) ---@param reversedKeys table The reverted QuestieDB keys for the given databaseTableName (e.g. QuestieDB.questKeys) ---@param validationTables table Only used by the CI validation scripts to validate the corrections against the original database values and find irrelevant corrections ---@param noOverwrites true? Do not overwrite existing values ---@param noNewEntries true? Do not create new entries in the database local _LoadCorrections = function(databaseTableName, corrections, reversedKeys, validationTables, noOverwrites, noNewEntries) for id, data in pairs(corrections) do for key, value in pairs(data) do -- Create the id if missing unless noNewEntries is set if not QuestieDB[databaseTableName][id] and not noNewEntries then QuestieDB[databaseTableName][id] = {} end if validationTables and QuestieDB[databaseTableName][id] then if value and QuestieLib.equals(QuestieDB[databaseTableName][id][key], value) and validationTables[databaseTableName][id] and QuestieLib.equals(validationTables[databaseTableName][id][key], value) then Questie:Warning("Correction of " .. databaseTableName .. " " .. tostring(id) .. "." .. reversedKeys[key] .. " matches base DB! Value:" .. tostring(value)) end end if QuestieDB[databaseTableName][id] then if noOverwrites and QuestieDB[databaseTableName][id][key] == nil then QuestieDB[databaseTableName][id][key] = value elseif not noOverwrites then QuestieDB[databaseTableName][id][key] = value end end end end end ---@param validationTables table? Only used by the CI validation scripts to validate the corrections against the original database values and find irrelevant corrections function QuestieCorrections:Initialize(validationTables) QuestieQuestFixes:LoadMissingQuests() -- Classic Corrections _LoadCorrections("questData", QuestieClassicQuestReputationFixes:Load(), QuestieDB.questKeysReversed, validationTables) _LoadCorrections("questData", QuestieQuestFixes:Load(), QuestieDB.questKeysReversed, validationTables) _LoadCorrections("npcData", QuestieNPCFixes:Load(), QuestieDB.npcKeysReversed, validationTables) _LoadCorrections("itemData", QuestieItemFixes:Load(), QuestieDB.itemKeysReversed, validationTables) _LoadCorrections("objectData", QuestieObjectFixes:Load(), QuestieDB.objectKeysReversed, validationTables) if Questie.IsTBC or Questie.IsWotlk then _LoadCorrections("questData", QuestieTBCQuestFixes:Load(), QuestieDB.questKeysReversed, validationTables) _LoadCorrections("npcData", QuestieTBCNpcFixes:Load(), QuestieDB.npcKeysReversed, validationTables) _LoadCorrections("itemData", QuestieTBCItemFixes:Load(), QuestieDB.itemKeysReversed, validationTables) _LoadCorrections("objectData", QuestieTBCObjectFixes:Load(), QuestieDB.objectKeysReversed, validationTables) end if Questie.IsWotlk then _LoadCorrections("questData", QuestieWotlkQuestFixes:Load(), QuestieDB.questKeysReversed, validationTables) _LoadCorrections("npcData", QuestieWotlkNpcFixes:LoadAutomatics(), QuestieDB.npcKeysReversed, validationTables) _LoadCorrections("npcData", QuestieWotlkNpcFixes:Load(), QuestieDB.npcKeysReversed, validationTables) _LoadCorrections("itemData", QuestieWotlkItemFixes:Load(), QuestieDB.itemKeysReversed, validationTables) _LoadCorrections("objectData", QuestieWotlkObjectFixes:Load(), QuestieDB.objectKeysReversed, validationTables) end if Questie.IsSoD then _LoadCorrections("questData", SeasonOfDiscovery:LoadBaseQuests(), QuestieDB.questKeysReversed, validationTables) _LoadCorrections("questData", SeasonOfDiscovery:LoadQuests(), QuestieDB.questKeysReversed, validationTables) _LoadCorrections("npcData", SeasonOfDiscovery:LoadBaseNPCs(), QuestieDB.npcKeysReversed, validationTables) _LoadCorrections("npcData", SeasonOfDiscovery:LoadNPCs(), QuestieDB.npcKeysReversed, validationTables) _LoadCorrections("itemData", SeasonOfDiscovery:LoadBaseItems(), QuestieDB.itemKeysReversed, validationTables) _LoadCorrections("itemData", SeasonOfDiscovery:LoadItems(), QuestieDB.itemKeysReversed, validationTables) _LoadCorrections("objectData", SeasonOfDiscovery:LoadBaseObjects(), QuestieDB.objectKeysReversed, validationTables) _LoadCorrections("objectData", SeasonOfDiscovery:LoadObjects(), QuestieDB.objectKeysReversed, validationTables) end --- Corrections that apply to all versions _LoadCorrections("itemData", QuestieItemStartFixes:LoadAutomaticQuestStarts(), QuestieDB.itemKeysReversed, validationTables, true, true) if QuestieCompat.Is335 then QuestieCompat.LoadCorrections(_LoadCorrections, validationTables) end local patchCount = 0 for _, quest in pairs(QuestieDB.questData) do if (not quest[QuestieDB.questKeys.requiredRaces]) or quest[QuestieDB.questKeys.requiredRaces] == 0 then -- check against questgiver local canHorde = false local canAlliance = false local starts = quest[QuestieDB.questKeys.startedBy] if starts then starts = starts[1] if starts then for _, id in pairs(starts) do local npc = QuestieDB.npcData[id] if npc then local friendly = npc[QuestieDB.npcKeys.friendlyToFaction] if friendly then if friendly == "H" then canHorde = true elseif friendly == "A" then canAlliance = true elseif friendly == "AH" then canAlliance = true canHorde = true end end end end end if canAlliance ~= canHorde then patchCount = patchCount + 1 if canAlliance then quest[QuestieDB.questKeys.requiredRaces] = QuestieDB.raceKeys.ALL_ALLIANCE else quest[QuestieDB.questKeys.requiredRaces] = QuestieDB.raceKeys.ALL_HORDE end end end end end QuestieCorrections:MinimalInit() end local WAYPOINT_MIN_DISTANCE = 1.5 -- todo: make this a config value maybe? local ZONE_SCALES = { [ZoneDB.zoneIDs.STORMWIND_CITY] = 0.5, [ZoneDB.zoneIDs.IRONFORGE] = 0.5, [ZoneDB.zoneIDs.TELDRASSIL] = 0.5, [ZoneDB.zoneIDs.ORGRIMMAR] = 0.5, [ZoneDB.zoneIDs.THUNDER_BLUFF] = 0.5, [ZoneDB.zoneIDs.UNDERCITY] = 0.5, } local abs, sqrt = math.abs, math.sqrt local function euclid(x, y, i, e) local xd = abs(x - i) local yd = abs(y - e) return sqrt(xd * xd + yd * yd) end function QuestieCorrections:OptimizeWaypoints(waypointData) local newWaypointZones = {} for zone, waypointList in pairs(waypointData) do local newWaypointList = {} if waypointList[1] and type(waypointList[1][1]) == "number" then waypointList = {waypointList} -- corrections support both {{x,y}, ...} and {{{x,y}, ...}, {{x,y}, ...}, ...} end for _, waypoints in pairs(waypointList) do -- apply RDP algorithm local minDist = WAYPOINT_MIN_DISTANCE * (ZONE_SCALES[zone] or 1) local newWaypoints = RamerDouglasPeucker(waypoints, 0.1, true) waypoints = newWaypoints newWaypoints = {} -- subdivide waypoints where needed -- We do this because the clickable area of waypoint lines can only be a square, so lines need to be broken up in some places local lastWay for _, way in pairs(waypoints) do if lastWay then local dist = euclid(way[1], way[2], lastWay[1], lastWay[2]) if dist > minDist then local divs = math.ceil(dist/minDist) for i=1,divs do local mul0 = i/divs local mul1 = 1-mul0 newWaypoints[#newWaypoints+1] = {way[1] * mul0 + lastWay[1] * mul1, way[2] * mul0 + lastWay[2] * mul1} end else newWaypoints[#newWaypoints+1] = way end else newWaypoints[#newWaypoints+1] = way end lastWay = way end newWaypointList[#newWaypointList+1] = newWaypoints end newWaypointZones[zone] = newWaypointList end return newWaypointZones end function QuestieCorrections:PreCompile() -- this happens only if we are about to compile the database. Run intensive preprocessing tasks here (like ramer-douglas-peucker) local waypointKey = QuestieDB.npcKeys["waypoints"] local npcData = QuestieDB.npcData local count = 0 for id, data in pairs(npcData) do local way = data[waypointKey] if way then npcData[id][waypointKey] = QuestieCorrections:OptimizeWaypoints(way) end if count > 500 then -- 500 seems like a good number count = 0 coroutine.yield() end count = count + 1 end end