Files
Questie-X/Database/Corrections/QuestieCorrections.lua
T

437 lines
20 KiB
Lua

---@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<number, table<number, string|table|number>>
---@param new_overrides table<number, table<number, string|table|number>>
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