local BtWQuests = BtWQuests; local L = BtWQuests.L; local OUTDATED_LEVEL = 110; local INTERFACE_NUMBER = select(4, GetBuildInfo()) local wipe = table.wipe local format = string.format local lower = string.lower local gsub = string.gsub local gmatch = string.gmatch local concat = string.concat local LE_EXPANSION_LEVEL_CURRENT = LE_EXPANSION_LEVEL_CURRENT or 0; --@REMOVE AFTER 9.0 local GetLogIndexForQuestID = C_QuestLog.GetLogIndexForQuestID local GetQuestWatchType = C_QuestLog.GetQuestWatchType local AddQuestWatch = C_QuestLog.AddQuestWatch local RemoveQuestWatch = C_QuestLog.RemoveQuestWatch if INTERFACE_NUMBER < 90000 then GetLogIndexForQuestID = GetQuestLogIndexByID function GetQuestWatchType(questID) return IsQuestWatched(GetLogIndexForQuestID(questID)) and 0 or nil end AddQuestWatch = AddQuestWatchForQuestID RemoveQuestWatch = RemoveQuestWatchForQuestID end -- [[ Helper functions ]] function BtWQuestsItem_GetItems(item, character) if item.items ~= nil then return item.items end if item.type == "chain" then return BtWQuests.Database.Chains[item.id].items elseif item.type == "category" then return BtWQuests.Database.Categories[item.id].items elseif item.type == "expansion" then return BtWQuests.Database.Expansions[item.id].items end return nil end function BtWQuestsItem_Active(item, character) if item.type == "quest" then local ids = item.ids or {item.id} for _,id in ipairs(ids) do if character:IsQuestActive(id) then return true end end elseif item.type == "time" then return GetServerTime() < item.time end return false end function BtWQuestsItem_ActiveOrCompleted(item, character) if item.type == "quest" then local ids = item.ids or {item.id} for _,id in ipairs(ids) do if character:IsQuestActive(id) or character:IsQuestCompleted(id) then return true end end end return false end --[[ relationship = { breadcrumb = 52065, blockers = {51711, 51752}, }, ]] function BtWQuestsItem_RelationshipBlockersVisible(item, character) local blockers = item.relationship.blockers or {item.relationship.blocker} for _,questID in ipairs(blockers) do if character:IsQuestActive(questID) or character:IsQuestCompleted(questID) then return false end end return true end function BtWQuestsItem_RelationshipSourceVisible(item, character) if character:IsQuestCompleted(item.relationship.breadcrumb) then return true end return BtWQuestsItem_RelationshipBlockersVisible(item, character) end function BtWQuestsItem_RelationshipSourceActive(item, character) return character:IsQuestActive(item.relationship.breadcrumb) or character:IsQuestCompleted(item.relationship.breadcrumb) end local function GetVariation(database, item, character) if item.variations == nil then return item; end if character ~= nil then for _,variation in ipairs(item.variations) do if not variation.initialized then variation.initialized = true; for k,v in pairs(item) do if k ~= "variations" and variation[k] == nil then variation[k] = v; end end end local details = database:GetItemType(variation.type); if details:IsValidForCharacter(database, variation, character) then return variation; end end end local variation = item.variations[#item.variations]; if not variation.initialized then variation.initialized = true; for k,v in pairs(item) do if k ~= "variations" and variation[k] == nil then variation[k] = v; end end end return variation end local function CheckTargetStatus(target, item, character) assert(target ~= nil, format("Missing Data %s - %d", item.type, item.id or 0)) if item.status ~= nil then for _,status in ipairs(item.status) do if status == "available" and target:IsAvailable(character) then return true elseif status == "active" and target:IsActive(character) then return true elseif status == "completed" and target:IsCompleted(character) then return true elseif status == "notactive" and not target:IsActive(character) then return true elseif status == "notcompleted" and not target:IsCompleted(character) then return true end end elseif item.active == true then if target:IsActive(character) then return true end elseif item.active == false then if not target:IsActive(character) then return true end elseif item.completed == false then if not target:IsCompleted(character) then return true end else if target:IsCompleted(character) then return true end end return false end local function CheckPetStatus(id, item, character) if item.status == 'summon' then local guid = C_PetJournal.GetSummonedPetGUID() if not guid then return false; end return C_PetJournal.GetPetInfoByPetID(guid) == id; else return select(1, C_PetJournal.GetNumCollectedInfo(id)) > 0 end end local function CheckMountStatus(id, item, character) return select(11, C_MountJournal.GetMountInfoByID(id)) end local function CheckItemStatus(id, item, character) return GetItemCount(id) > 0 end local function CheckQuestStatus(id, item, character) if item.status ~= nil then for _,status in ipairs(item.status) do if status == "pending" and (not character:IsQuestActive(id) and not character:IsQuestCompleted(id)) then return true elseif status == "active" and character:IsQuestActive(id) then return true elseif status == "completed" and character:IsQuestCompleted(id) then return true elseif status == "notactive" and not character:IsQuestActive(id) then return true elseif status == "notcompleted" and not character:IsQuestCompleted(id) then return true end end elseif item.active == true then if character:IsQuestActive(id) then return true end elseif item.active == false then if not character:IsQuestActive(id) then return true end elseif item.completed == false then if not character:IsQuestCompleted(id) then return true end else if character:IsQuestCompleted(id) then return true end end return false end local function CheckChainStatus(id, item, character) if item.status ~= nil then for _,status in ipairs(item.status) do if status == "pending" and (not character:IsChainActive(id) and not character:IsChainCompleted(id)) then return true elseif status == "active" and character:IsChainActive(id) then return true elseif status == "completed" and character:IsChainCompleted(id) then return true elseif status == "notactive" and not character:IsChainActive(id) then return true elseif status == "notcompleted" and not character:IsChainCompleted(id) then return true end end elseif item.active == true then if character:IsChainActive(id) then return true end elseif item.active == false then if not character:IsChainActive(id) then return true end elseif item.completed == false then if not character:IsChainCompleted(id) then return true end else if character:IsChainCompleted(id) then return true end end return false end local function CheckCategoryStatus(id, item, character) if item.status ~= nil then for _,status in ipairs(item.status) do if status == "pending" and (not character:IsCategoryActive(id) and not character:IsCategoryCompleted(id)) then return true elseif status == "active" and character:IsCategoryActive(id) then return true elseif status == "completed" and character:IsCategoryCompleted(id) then return true elseif status == "notactive" and not character:IsCategoryActive(id) then return true elseif status == "notcompleted" and not character:IsCategoryCompleted(id) then return true end end return false elseif item.active == true then if character:IsCategoryActive(id) then return true end elseif item.active == false then if not character:IsCategoryActive(id) then return true end elseif item.completed == false then if not character:IsCategoryCompleted(id) then return true end else if character:IsCategoryCompleted(id) then return true end end return false end local function CheckStatusCount(amount, item) local count = item.count or 1 local lessthan = item.lessthan and true or false local morethan = item.morethan and true or false local notequals = item.notequals and true or false local equals = item.equals and true or false local morethanorequals = not lessthan and not morethan and not notequals and not equals if lessthan and amount < count then return true elseif morethan and amount > count then return true elseif notequals and amount ~= count then return true elseif equals and amount == count then return true elseif morethanorequals and amount >= count then return true end return false end local function StatusCompleted(item, character, callback) local amount = 0 if item.ids then for _,id in ipairs(item.ids) do if callback(id, item, character) then amount = amount + 1 end end else if callback(item.id, item, character) then amount = amount + 1 end end return CheckStatusCount(amount, item) end local function TableOnClick(tbl, item) if tbl[1] ~= nil then for _,v in ipairs(tbl) do TableOnClick(v, item) end else if tbl.type == "category" then BtWQuestsFrame:SelectCategory(tbl.id, tbl.scrollTo) elseif tbl.type == "chain" then BtWQuestsFrame:SelectChain(tbl.id, tbl.scrollTo) elseif tbl.type == "coords" then BtWQuests_ShowMapWithWaypoint(tbl.mapID, tbl.x, tbl.y, tbl.name or item:GetName()) end end end local timezones = { [BTWQUESTS_REGION_ID_US] = { ["Aegwynn"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Aerie Peak"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Agamaggan"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Aggramar"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Akama"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Alexstrasza"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Alleria"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Altar of Storms"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Alterac Mountains"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Aman'Thul"] = BTWQUESTS_TIMEZONE_ID_AUSTRALIA_MELBOURN, ["Andorhal"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Anetheron"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Antonidas"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Anub'arak"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Anvilmar"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Arathor"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Archimonde"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Area 52"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Argent Dawn"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Arthas"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Arygos"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Auchindoun"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Azgalor"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Azjol-Nerub"] = BTWQUESTS_TIMEZONE_ID_AMERICA_DENVER, ["Azralon"] = BTWQUESTS_TIMEZONE_ID_AMERICA_SAOPAULO, ["Azshara"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Azuremyst"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Baelgun"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Balnazzar"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Barthilas"] = BTWQUESTS_TIMEZONE_ID_AUSTRALIA_MELBOURN, ["Black Dragonflight"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Blackhand"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Blackrock"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Blackwater Raiders"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Blackwing Lair"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Blade's Edge"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Bladefist"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Bleeding Hollow"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Blood Furnace"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Bloodhoof"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Bloodscalp"] = BTWQUESTS_TIMEZONE_ID_AMERICA_DENVER, ["Bonechewer"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Borean Tundra"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Boulderfist"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Bronzebeard"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Burning Blade"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Burning Legion"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Caelestrasz"] = BTWQUESTS_TIMEZONE_ID_AUSTRALIA_MELBOURN, ["Cairne"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Cenarion Circle"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Cenarius"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Cho'gall"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Chromaggus"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Coilfang"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Crushridge"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Daggerspine"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Dalaran"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Dalvengyr"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Dark Iron"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Darkspear"] = BTWQUESTS_TIMEZONE_ID_AMERICA_DENVER, ["Darrowmere"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Dath'Remar"] = BTWQUESTS_TIMEZONE_ID_AUSTRALIA_MELBOURN, ["Dawnbringer"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Deathwing"] = BTWQUESTS_TIMEZONE_ID_AMERICA_DENVER, ["Demon Soul"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Dentarg"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Destromath"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Dethecus"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Detheroc"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Doomhammer"] = BTWQUESTS_TIMEZONE_ID_AMERICA_DENVER, ["Draenor"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Dragonblight"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Dragonmaw"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Drak'Tharon"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Drak'thul"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Draka"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Drakkari"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Dreadmaul"] = BTWQUESTS_TIMEZONE_ID_AUSTRALIA_MELBOURN, ["Drenden"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Dunemaul"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Durotan"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Duskwood"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Earthen Ring"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Echo Isles"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Eitrigg"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Eldre'Thalas"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Elune"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Emerald Dream"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Eonar"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Eredar"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Executus"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Exodar"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Farstriders"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Feathermoon"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Fenris"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Firetree"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Fizzcrank"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Frostmane"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Frostmourne"] = BTWQUESTS_TIMEZONE_ID_AUSTRALIA_MELBOURN, ["Frostwolf"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Galakrond"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Gallywix"] = BTWQUESTS_TIMEZONE_ID_AMERICA_SAOPAULO, ["Garithos"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Garona"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Garrosh"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Ghostlands"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Gilneas"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Gnomeregan"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Goldrinn"] = BTWQUESTS_TIMEZONE_ID_AMERICA_SAOPAULO, ["Gorefiend"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Gorgonnash"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Greymane"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Grizzly Hills"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Gul'dan"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Gundrak"] = BTWQUESTS_TIMEZONE_ID_AUSTRALIA_MELBOURN, ["Gurubashi"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Hakkar"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Haomarush"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Hellscream"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Hydraxis"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Hyjal"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Icecrown"] = BTWQUESTS_TIMEZONE_ID_AMERICA_DENVER, ["Illidan"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Jaedenar"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Jubei'Thos"] = BTWQUESTS_TIMEZONE_ID_AUSTRALIA_MELBOURN, ["Kael'thas"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Kalecgos"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Kargath"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Kel'Thuzad"] = BTWQUESTS_TIMEZONE_ID_AMERICA_DENVER, ["Khadgar"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Khaz Modan"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Khaz'goroth"] = BTWQUESTS_TIMEZONE_ID_AUSTRALIA_MELBOURN, ["Kil'jaeden"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Kilrogg"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Kirin Tor"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Korgath"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Korialstrasz"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Kul Tiras"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Laughing Skull"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Lethon"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Lightbringer"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Lightning's Blade"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Lightninghoof"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Llane"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Lothar"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Madoran"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Maelstrom"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Magtheridon"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Maiev"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Mal'Ganis"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Malfurion"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Malorne"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Malygos"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Mannoroth"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Medivh"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Misha"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Mok'Nathal"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Moon Guard"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Moonrunner"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Mug'thol"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Muradin"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Nagrand"] = BTWQUESTS_TIMEZONE_ID_AUSTRALIA_MELBOURN, ["Nathrezim"] = BTWQUESTS_TIMEZONE_ID_AMERICA_DENVER, ["Nazgrel"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Nazjatar"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Nemesis"] = BTWQUESTS_TIMEZONE_ID_AMERICA_SAOPAULO, ["Ner'zhul"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Nesingwary"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Nordrassil"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Norgannon"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Onyxia"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Perenolde"] = BTWQUESTS_TIMEZONE_ID_AMERICA_DENVER, ["Proudmoore"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Quel'Thalas"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Quel'dorei"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Ragnaros"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Ravencrest"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Ravenholdt"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Rexxar"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Rivendare"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Runetotem"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Sargeras"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Saurfang"] = BTWQUESTS_TIMEZONE_ID_AUSTRALIA_MELBOURN, ["Scarlet Crusade"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Scilla"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Sen'jin"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Sentinels"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Shadow Council"] = BTWQUESTS_TIMEZONE_ID_AMERICA_DENVER, ["Shadowmoon"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Shadowsong"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Shandris"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Shattered Halls"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Shattered Hand"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Shu'halo"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Silver Hand"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Silvermoon"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Sisters of Elune"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Skullcrusher"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Skywall"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Smolderthorn"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Spinebreaker"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Spirestone"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Staghelm"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Steamwheedle Cartel"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Stonemaul"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Stormrage"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Stormreaver"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Stormscale"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Suramar"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Tanaris"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Terenas"] = BTWQUESTS_TIMEZONE_ID_AMERICA_DENVER, ["Terokkar"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Thaurissan"] = BTWQUESTS_TIMEZONE_ID_AUSTRALIA_MELBOURN, ["The Forgotten Coast"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["The Scryers"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["The Underbog"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["The Venture Co"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Thorium Brotherhood"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Thrall"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Thunderhorn"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Thunderlord"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Tichondrius"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Tol Barad"] = BTWQUESTS_TIMEZONE_ID_AMERICA_SAOPAULO, ["Tortheldrin"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Trollbane"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Turalyon"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Twisting Nether"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Uldaman"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Uldum"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Undermine"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Ursin"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Uther"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Vashj"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Vek'nilash"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Velen"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Warsong"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Whisperwind"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Wildhammer"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Windrunner"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Winterhoof"] = BTWQUESTS_TIMEZONE_ID_AMERICA_CHICAGO, ["Wyrmrest Accord"] = BTWQUESTS_TIMEZONE_ID_AMERICA_LOSANGELAS, ["Ysera"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Ysondre"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Zangarmarsh"] = BTWQUESTS_TIMEZONE_ID_AMERICA_DENVER, ["Zul'jin"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, ["Zuluhed"] = BTWQUESTS_TIMEZONE_ID_AMERICA_NEWYORK, }, [BTWQUESTS_REGION_ID_KOREA] = BTWQUESTS_TIMEZONE_ID_ASIA_SEOUL, [BTWQUESTS_REGION_ID_EUROPE] = BTWQUESTS_TIMEZONE_ID_EUROPE_PARIS, [BTWQUESTS_REGION_ID_TAIWAN] = BTWQUESTS_TIMEZONE_ID_ASIA_TAIPEI, } function BtWQuests_GetTimeZone(region, realm) if region == nil then region = GetCurrentRegion() elseif type(region) == "string" then return BtWQuests_GetTimeZone(nil, region) end if realm == nil then realm = GetRealmName() end if type(timezones[region]) == "table" then return timezones[region][realm] else return timezones[region] end end local function GetPlayerPosition(targetMapID) local sourceMapID = C_Map.GetBestMapForUnit("player") local sourceCoords = C_Map.GetPlayerMapPosition(sourceMapID, "player") if sourceMapID == targetMapID then return sourceCoords else local continentID, coords = C_Map.GetWorldPosFromMapPos(sourceMapID, sourceCoords) if coords == nil then return nil end local _, coords = C_Map.GetMapPosFromWorldPos(continentID, coords, targetMapID) if coords == nil then return nil end return coords end end function BtWQuests_GetBestLocation(database, locations, relativeMapID) -- , relativeX, relativeY Should these be added? if relativeMapID == nil then local possibleLocations = {} for mapID in pairs(locations) do local _, item = BtWQuests_GetBestLocation(database, locations, mapID) if item then possibleLocations[#possibleLocations+1] = item end end table.sort(possibleLocations, function (a, b) return (a.distanceSq or 0) < (b.distanceSq or 0) end) local result = possibleLocations[1] return result.mapID, result end if locations[relativeMapID] == nil then -- This'll take a while local sourceMapID, sourceCoords = BtWQuests_GetBestLocation(database, locations) if sourceCoords == nil then return nil end local continentID, coords = C_Map.GetWorldPosFromMapPos(sourceMapID, sourceCoords) if coords == nil then return nil end local _, coords = C_Map.GetMapPosFromWorldPos(continentID, coords, relativeMapID) if coords == nil then return nil end return relativeMapID, coords else local filtered = database:FilterItems(locations[relativeMapID], BtWQuestsCharacters:GetPlayer()) local result = filtered[1] local resultDistanceSq if #filtered > 1 then local playerPos = GetPlayerPosition(relativeMapID) if playerPos then resultDistanceSq = CalculateDistanceSq(result.x, result.y, playerPos.x, playerPos.y) for i=2,#filtered do local item = filtered[i] local itemDistanceSq = CalculateDistanceSq(item.x, item.y, playerPos.x, playerPos.y) if itemDistanceSq < resultDistanceSq then result = item resultDistanceSq = itemDistanceSq end end end end if not result then return nil end result = CreateVector2D(result.x, result.y) result.mapID = relativeMapID result.distanceSq = resultDistanceSq return relativeMapID, result end end local ConditionMixin = {} function ConditionMixin:EvalFor(character) return self.database:EvalRequirement(self, self, character); end local DataMixin = {}; function DataMixin:GetID() return self.id; end function DataMixin:GetName() return self.database:EvalText(self.name, self) end function DataMixin:GetUserdata() return self.userdata end function DataMixin:IsValidForCharacter(character) return self.database:IsItemValidForCharacter(self, character); end function DataMixin:Visible(character) if self.visible ~= nil then return self.database:EvalRequirement(self.visible, self, character); end return true; end function DataMixin:IsAvailable(character) assert(character ~= nil); if self:IsCompleted(character) then return false; end if self:IsActive(character) then return false; end if self.prerequisites ~= nil then return self.database:EvalRequirement(self.prerequisites, self, character); end return true; end function DataMixin:IsActive(character) assert(character ~= nil); if self:IsCompleted(character) then return false; end if self.active ~= nil then return self.database:EvalRequirement(self.active, self, character, true); end if self.prerequisites ~= nil then return self.database:EvalRequirement(self.prerequisites, self, character); end return true; end function DataMixin:IsCompleted(character) if self.completed ~= nil then return self.database:EvalRequirement(self.completed, self, character); end return false end function DataMixin:GetPrerequisites() if self.prerequisitesItems == nil then self.prerequisitesItems = {} if self.prerequisites then if self.prerequisites[1] == nil then self.prerequisitesItems[#self.prerequisitesItems+1] = self.database:CreateItem(-1, self.prerequisites, self); else for _,prerequisite in ipairs(self.prerequisites) do self.prerequisitesItems[#self.prerequisitesItems+1] = self.database:CreateItem(-1, prerequisite, self); end end end end return self.prerequisitesItems, self.hasLowPriorityPrerequisites; end function DataMixin:GetRestrictions() if self.restrictionsItems == nil then self.restrictionsItems = {} if self.restrictions then local restrictions = self.restrictions if type(restrictions) == "number" then restrictions = self.database:GetConditionByID(restrictions) end if restrictions[1] == nil then self.restrictionsItems[#self.restrictionsItems+1] = self.database:CreateItem(-1, restrictions, self); else for _,restriction in ipairs(restrictions) do self.restrictionsItems[#self.restrictionsItems+1] = self.database:CreateItem(-1, restriction, self); end end end end return self.restrictionsItems; end function DataMixin:GetRewards() if self.rewardsItems == nil then self.rewardsItems = {} if self.rewards then for _,reward in ipairs(self.rewards) do self.rewardsItems[#self.rewardsItems+1] = self.database:CreateItem(-1, reward, self, self); end end end return self.rewardsItems; end local QuestMixin = CreateFromMixins(DataMixin); function QuestMixin:GetContentTuningID() return self.contentTuningID or -1 end function QuestMixin:GetLevel() return self.level or -1 end function QuestMixin:GetRequiredLevel() return self.requiredLevel or -1 end function QuestMixin:GetMaxLevel() return self.maxLevel or 255 end function QuestMixin:GetLevelFlag() return self.levelFlag or 0 end if INTERFACE_NUMBER < 90000 then function QuestMixin:GetLink() if self.link == nil then self.link = format("\124cffffff00\124Hquest:%d:%d:%d:%d:%d\124h[%s]\124h\124r", self:GetID(), self:GetLevel(), self:GetRequiredLevel(), self:GetMaxLevel(), self:GetLevelFlag(), self:GetName()) end return self.link end else function QuestMixin:GetLink() if self.link == nil then self.link = format("\124cffffff00\124Hquest:%d:%d\124h[%s]\124h\124r", self:GetID(), self:GetContentTuningID(), self:GetName()) end return self.link end end function QuestMixin:IsActive(character) return character:IsQuestActive(self.id) end function QuestMixin:IsCompleted(character) return character:IsQuestCompleted(self.id) end function QuestMixin:GetUserdata() -- if self.userdata == nil then -- self.userdata = self.item.userdata or {} -- self.userdata.link = self.userdata.link or self:GetLink() -- end return self.userdata end function QuestMixin:GetSource(character) if self.source ~= nil then return self.database:CreateItem(-1, self.source, self, self); end return nil; end function QuestMixin:HasTarget() return self.target ~= nil or self.targets ~= nil end function QuestMixin:GetTargetMapID() return (self.target and self.target.uiMapID) or (self.targets and self.targets.mapID) end function QuestMixin:HasObjectives() return self.objectives ~= nil end function QuestMixin:GetCurrentObjectiveMapID() for index,objective in ipairs(C_QuestLog.GetQuestObjectives(self:GetID())) do if not objective.finished then return self.objectives[index].uiMapID or self.objectives[index].mapID end end end local MissionMixin = CreateFromMixins(DataMixin); function MissionMixin:IsBreadcrumb() return true end function MissionMixin:IsAvailable(character) local mission = C_Garrison.GetBasicMissionInfo(self.id or self.ids[1]) return mission ~= nil end function MissionMixin:IsActive(character) local mission = C_Garrison.GetBasicMissionInfo(self.id or self.ids[1]) return mission and mission.inProgress or false end function MissionMixin:IsCompleted(character) return false end local NPCMixin = CreateFromMixins(DataMixin); function NPCMixin:IsBreadcrumb() return true end function NPCMixin:IsAvailable(character) return false end function NPCMixin:IsActive(character) return false end function NPCMixin:IsCompleted(character) return false end function NPCMixin:GetLocation(...) if self.locations == nil then return nil end return BtWQuests_GetBestLocation(self.database, self.locations, ...) end local ObjectMixin = CreateFromMixins(NPCMixin); local ChainMixin = CreateFromMixins(DataMixin); function ChainMixin:GetSubtext(character, small) if self:IsCompleted(character) then return L["BTWQUESTS_COMPLETED"] elseif self:IsActive(character) then return L["BTWQUESTS_ACTIVE"] elseif self:IsAvailable(character) then return L["BTWQUESTS_AVAILABLE"] end end function ChainMixin:GetLink() if self.link == nil then self.link = format("\124cffffff00\124Hbtwquests:chain:%s\124h[%s]\124h\124r", self:GetID(), self:GetName()) end return self.link end function ChainMixin:GetExpansion() return self.expansion end function ChainMixin:GetCategory() return self.category end function ChainMixin:GetNumPrerequisites() if self.prerequisites == nil then return 0 end if self.prerequisites[1] == nil then return 1 end return #self.prerequisites end function ChainMixin:GetNumItems() return #self.items end -- Check if the character is ignoring this chain function ChainMixin:IsIgnored(character) return character:IsChainIgnored(self:GetID()) end function ChainMixin:IsAvailable(character) assert(character ~= nil); if self:IsIgnored(character) then return false end return DataMixin.IsAvailable(self, character); end function ChainMixin:IsActive(character) assert(character ~= nil); if self:IsIgnored(character) then return false end return DataMixin.IsActive(self, character); end function ChainMixin:IsMajor() return self.major == true end function ChainMixin:GetButtonImage() if type(self.buttonImage) ~= "table" then return self.buttonImage end return self.buttonImage.texture, unpack(self.buttonImage.texCoords) end function ChainMixin:GetListImage() if type(self.listImage) ~= "table" then return self.listImage end return self.listImage.texture, unpack(self.listImage.texCoords) end function ChainMixin:GetItem(index, character) local index = tonumber(index); if index == nil or self.items[index] == nil then return nil; end local item = GetVariation(self.database, self.items[index], character) local result = self.database:CreateItem(index, item, self, self); if result:GetType() == "chain" and result:IsEmbed() then local connections = result:GetConnections(); if connections then if connections[1] ~= nil and type(connections[1]) ~= "table" then connections[result:GetNumItems()] = {connections[1]}; connections[1] = nil; end end end return result; end function ChainMixin:GetNextItem(character) for i = 1,self:GetNumItems() do local item = self:GetItem(i, character) if item and item:IsValidForCharacter(character) and not item:IsAside(character) and not item:IsBreadcrumb(character) and item:Visible(character) and not item:IsCompleted(character) then return item end end end function ChainMixin:GetAlternative(character) if self.alternatives == nil then return nil end for _,v in ipairs(self.alternatives) do if self.database:GetChainByID(v):IsValidForCharacter(character) then return v; end end return nil; end local CategoryMixin = CreateFromMixins(DataMixin); function CategoryMixin:GetProgress(character) local majorProgress = 0 local majorTotal = 0 local minorProgress = 0 local minorTotal = 0 for _,v in ipairs(self.items) do if v.type == 'chain' then if not character:IsChainIgnored(v.id) and self.database:IsItemValidForCharacter(v, character) then local chain = self.database:GetChainByID(v.id); if v.major or (chain and chain:IsMajor()) then if self.database:IsChainCompleted(v.id, character) then majorProgress = majorProgress + 1 end majorTotal = majorTotal + 1 else if self.database:IsChainCompleted(v.id, character) then minorProgress = minorProgress + 1 end minorTotal = minorTotal + 1 end end elseif v.type == 'category' then if not character:IsCategoryIgnored(v.id) then if self.database:IsCategoryCompleted(v.id, character) then minorProgress = minorProgress + 1 end minorTotal = minorTotal + 1 end end end if majorTotal == 0 then majorProgress = minorProgress majorTotal = minorTotal minorProgress = 0 minorTotal = 0 end if minorTotal == 0 then return majorProgress, majorTotal end return majorProgress, majorTotal, minorProgress, minorTotal end function CategoryMixin:GetSubtext(character, small) if self:IsCompleted(character) then return L["BTWQUESTS_COMPLETED"] end local majorProgress, majorTotal, minorProgress, minorTotal = self:GetProgress(character) if minorTotal == nil then return string.format(L["BTWQUESTS_PROGRESS"], majorProgress, majorTotal) elseif small then return string.format(L["BTWQUESTS_PROGRESS"], minorProgress + majorProgress, minorTotal + majorTotal) else return string.format(L["BTWQUESTS_PROGRESS_SIDE"], majorProgress, majorTotal, minorProgress + majorProgress, minorTotal + majorTotal) end end function CategoryMixin:GetExpansion() return self.expansion end function CategoryMixin:GetParent() return self.parent end function CategoryMixin:GetLink() if self.link == nil then self.link = format("\124cffffff00\124Hbtwquests:category:%s\124h[%s]\124h\124r", self:GetID(), self:GetName()) end return self.link end function CategoryMixin:GetItemList(character, noHeaders, filterCompleted, filterIgnored, filterMajor, includeChildren) local major = {} local others = {} local completed = {} local ignored = {} local header = nil local index = 1 local item = self:GetItem(index, character) while item do if item:IsValidForCharacter(character) and item:Visible(character) then if item:GetType() == "header" then header = item else if includeChildren and item:GetType() == "category" then local children = item:GetItemList(character, noHeaders, false, false, false, includeChildren) if filterIgnored and character:IsCategoryIgnored(item:GetID()) then for _,child in ipairs(children) do table.insert(ignored, child) end else for _,child in ipairs(children) do if filterIgnored and child:GetType() == "chain" and character:IsChainIgnored(child:GetID()) then table.insert(ignored, child) elseif filterCompleted and child:IsCompleted(character) then table.insert(completed, child) elseif filterMajor and child:GetType() == "chain" and child:IsMajor() then table.insert(major, child) else table.insert(others, child) end end end elseif filterIgnored and item:GetType() == "category" and character:IsCategoryIgnored(item:GetID()) then table.insert(ignored, item) elseif filterIgnored and item:GetType() == "chain" and character:IsChainIgnored(item:GetID()) then table.insert(ignored, item) elseif filterCompleted and item:IsCompleted(character) then table.insert(completed, item) elseif filterMajor and item:GetType() == "chain" and item:IsMajor() then table.insert(major, item) else if header ~= nil then if not noHeaders then table.insert(others, header) end header = nil end table.insert(others, item) end end end index = index + 1 item = self:GetItem(index, character) end local results = {} if #major > 0 then if not noHeaders then table.insert(results, self.database:CreateItem(-1, {type = "header", name = L["BTWQUESTS_MAJOR"]}, self, self)) end for _,item in ipairs(major) do table.insert(results, item) end end for _,item in ipairs(others) do table.insert(results, item) end if #completed > 0 then if not noHeaders then table.insert(results, self.database:CreateItem(-1, {type = "header", name = L["BTWQUESTS_COMPLETED"]}, self, self)) end for _,item in ipairs(completed) do table.insert(results, item) end end if #ignored > 0 then if not noHeaders then table.insert(results, self.database:CreateItem(-1, {type = "header", name = L["BTWQUESTS_IGNORED"]}, self, self)) end for _,item in ipairs(ignored) do table.insert(results, item) end end return results end function CategoryMixin:GetItem(index, character) local index = tonumber(index) if index == nil or self.items == nil or self.items[index] == nil then return nil end local item = GetVariation(self.database, self.items[index], character) return self.database:CreateItem(index, item, self, self); end function CategoryMixin:IsMajor() return self.major == true end function CategoryMixin:GetButtonImage() if type(self.buttonImage) ~= "table" then return self.buttonImage end return self.buttonImage.texture, unpack(self.buttonImage.texCoords) end function CategoryMixin:GetListImage() if type(self.listImage) ~= "table" then return self.listImage end return self.listImage.texture, unpack(self.listImage.texCoords) end function CategoryMixin:IsCompleted(character) for _,v in ipairs(self.items) do if v.type == 'chain' then local item = self.database:GetChainByID(v.id) if not character:IsChainIgnored(v.id) and item:IsValidForCharacter(character) and not item:IsCompleted(character) then return false end elseif v.type == 'category' then local item = self.database:GetCategoryByID(v.id) if not character:IsCategoryIgnored(v.id) and not item:IsCompleted(character) then return false end end end return true end function CategoryMixin:IsActive(character) return false end function CategoryMixin:GetAlternative(character) if self.alternatives == nil then return nil end for _,v in ipairs(self.alternatives) do if self.database:GetCategoryByID(v):IsValidForCharacter(character) then return v; end end return nil; end local ExpansionMixin = CreateFromMixins(CategoryMixin); function ExpansionMixin:GetLink() if self.link == nil then self.link = format("\124cffffff00\124Hbtwquests:expansion:%s\124h[%s]\124h\124r", self:GetID(), self:GetName()) end return self.link end function ExpansionMixin:GetImage() if type(self.image) ~= "table" then return self.image end return self.image.texture, unpack(self.image.texCoords) end -- Gets 3 items about the expansions function ExpansionMixin:GetMajorItems(character) local actives, available, upcoming = {}, {}, {} local items = self:GetItemList(character, true, true, true, true, true) for _,item in ipairs(items) do if item:IsActive(character) then actives[#actives+1] = item if #actives == 3 then break end elseif item:IsAvailable(character) then available[#available+1] = item elseif not item:IsCompleted(character) then upcoming[#upcoming+1] = item end end local items = actives for _,item in ipairs(available) do if #items == 3 then break end items[#items+1] = item end for _,item in ipairs(upcoming) do if #items == 3 then break end items[#items+1] = item end return {items[1], items[2], items[3]} end function ExpansionMixin:IsCompleted() return GetAccountExpansionLevel() >= self.id end function ExpansionMixin:IsLoaded() if type(self.addons) ~= "table" or next(self.addons) == nil then return true end for addon in pairs(self.addons) do if not IsAddOnLoaded(addon) then return false end end return true end function ExpansionMixin:SupportAutoLoad() return type(self.addons) == "table" and next(self.addons) ~= nil end function ExpansionMixin:IsAutoLoad() if type(self.addons) ~= "table" or next(self.addons) == nil then return true end for addon in pairs(self.addons) do if not BtWQuests_AutoLoad[addon] then return false end end return true end function ExpansionMixin:SetAutoLoad(value) if type(self.addons) ~= "table" or next(self.addons) == nil then return end for addon in pairs(self.addons) do BtWQuests_AutoLoad[addon] = value end end function ExpansionMixin:Load() wipe(self.database.questCache); for addon in pairs(self.addons) do LoadAddOn(addon) end end local ItemMixin = {}; function ItemMixin:EqualsItem(database, item, other) local type = self:GetType(database, item) if type ~= other.type then return false end if type == "chain" or type == "category" or type == "npc" or type == "quest" or type == "achievement" or type == "mission" or type == "faction" or type == "race" or type == "class" then return tonumber(self:GetID(database, item)) == tonumber(other.id) elseif type == "level" then return tonumber(self:GetLevel(database, item)) == tonumber(other.level) else return false end end function ItemMixin:GetType(database, item) return item.type; end function ItemMixin:GetID(database, item, index) if item.id then return item.id; end local index = index or 1; if item.ids and item.ids[index] then return item.ids[index]; end return nil; end function ItemMixin:GetVariation(database, item, character) if item.variations == nil then return nil; end return database:CreateItem(item.index, GetVariation(database, item, character), item, self:GetRoot(database, item)); end function ItemMixin:IsValidForCharacter(database, item, character) if item.restrictions ~= nil then return database:IsItemValidForCharacter(item, character); end return true; end function ItemMixin:Visible(database, item, character, showAll) if item.visible ~= nil then return (showAll or not item.lowPriority) and database:EvalRequirement(item.visible, item, character); end return (showAll or not item.lowPriority); end function ItemMixin:GetName(database, item, character) if item.name then return database:EvalText(item.name, item, character); end return "Unnamed" end function ItemMixin:GetSubtext(database, item, character, small) if item.subtext then return database:EvalText(item.subtext, item, character); end end function ItemMixin:GetAlternative(database, item, character) if item.alternatives ~= nil then for _,v in ipairs(item.alternatives) do if database:IsItemValidForCharacter({type = item.type, id = v}, character) then return v; end end end return nil; end function ItemMixin:IsAvailable(database, item, character) if self:IsCompleted(database, item, character) then return false; end if self:IsActive(database, item, character) then return false; end if item.prerequisites ~= nil then return database:EvalRequirement(item.prerequisites, item, character); end return true end function ItemMixin:IsActive(database, item, character) if self:IsCompleted(database, item, character) then return false; end if item.active ~= nil then return database:EvalRequirement(item.active, item, character, true); end return false; end function ItemMixin:IsCompleted(database, item, character, ...) if item.completed ~= nil then return database:EvalRequirement(item.completed, item, character); end return false end function ItemMixin:IsBreadcrumb(database, item, character) if item.breadcrumb ~= nil then return item.breadcrumb; end return false end function ItemMixin:IsAside(database, item, character) if item.aside ~= nil then return item.aside; end return false end function ItemMixin:OnClick(database, item, character, ...) if type(item.onClick) == "table" then return TableOnClick(item.onClick, database:CreateItem(0, item), character, ...); elseif type(item.onClick) == "function" then return item.onClick(item, character, ...); end return nil; end function ItemMixin:OnEnter(database, item, character, ...) if item.onEnter ~= nil then return item.onEnter(item, character, ...); end return nil; end function ItemMixin:OnLeave(database, item, character, ...) if item.onLeave ~= nil then return item.onLeave(item, character, ...); end return nil; end function ItemMixin:GetPrerequisites(database, item) if item.prerequisites ~= nil then local result = {} for _,prerequisite in ipairs(item.prerequisites) do result[#result+1] = database:CreateItem(-1, prerequisite, item, self:GetRoot(database, item)); end return result, item.hasLowPriorityPrerequisites; end end function ItemMixin:GetRewards(database, item) if item.rewards ~= nil then local result = {} for _,reward in ipairs(item.rewards) do result[#result+1] = database:CreateItem(-1, reward, item, self:GetRoot(database, item)); end return result; end end function ItemMixin:GetDifficulty(database, item) return item.difficulty; end function ItemMixin:GetTagID(database, item) return item.tagID; end function ItemMixin:GetUserdata(database, item) if item.userdata ~= nil then return item.userdata; end return nil; end function ItemMixin:GetStatus(database, item, character) if item.status ~= nil then return database:EvalText(item.status, item, character) end if self:IsCompleted(database, item, character) then return "complete" end if self:IsBreadcrumb(database, item, character) and self:HasConnections(database, item, character) then local completed = false local index = 1 local connection = self:GetConnection(database, item, index, character) while connection do if connection:IsValidForCharacter(character) and connection:Visible(character) and connection:GetStatus(character) ~= nil then completed = true break end index = index + 1 connection = self:GetConnection(database, item, index, character) end if completed then return "complete" end end if self:IsActive(database, item, character) then return "active" end return nil end function ItemMixin:GetX(database, item) return item.x; end function ItemMixin:GetY(database, item) return item.y end function ItemMixin:GetParent(database, item) return item.parent end function ItemMixin:GetRoot(database, item) return item.root end function ItemMixin:GetIndex(database, item) return item.index end function ItemMixin:HasConnections(database, item) return item.connections and #item.connections > 0 end function ItemMixin:GetConnection(database, item, index, character, overrideConnections, overrideChain) local index = tonumber(index) if overrideConnections then local connections = overrideConnections; if index == nil or connections == nil or connections[index] == nil then return nil; end local connection = tostring(connections[index]); local match = string.gmatch(connection, "[^%.]+"); local result = overrideChain:GetItem(tonumber(match()), character); for value in match do result = result:GetItem(tonumber(value), character); end while result and result:GetType() == "chain" and result:IsEmbed() do result = result:GetItem(1, character); end return result; else local connections = item.connections; if index == nil or connections == nil or connections[index] == nil then return nil; end local connection = tostring(connections[index]); local match = string.gmatch(connection, "[^%.]+"); local result = self:GetRoot(database, item):GetItem(item.index + tonumber(match()), character); for value in match do result = result:GetItem(tonumber(value), character); end while result and result:GetType() == "chain" and result:IsEmbed() do result = result:GetItem(1, character); end return result; end end function ItemMixin:GetConnections(database, item) return item.connections; end function ItemMixin:GetAtlas(database, item) return item.atlas end function ItemMixin:GetSource(database, item, character) if item.source ~= nil then return database:CreateItem(-1, item.source, item, self:GetRoot(database, item)); end return nil; end local TargetItemMixin = CreateFromMixins(ItemMixin); function TargetItemMixin:GetTargetType(database, item) return item.type; end function TargetItemMixin:GetTarget(database, item, index) local type = self:GetTargetType(database, item); if type == nil or not database:HasDataType(type) then return nil; end if item.id then return database:GetData(type, item.id); end local index = index or 1; if item.ids and item.ids[index] then return database:GetData(type, item.ids[index]); end return nil; end function TargetItemMixin:TargetCount(database, item) return item.ids and #item.ids or 1; end function TargetItemMixin:IsValidForCharacter(database, item, character) if item.restrictions ~= nil then return ItemMixin.IsValidForCharacter(self, database, item, character); end local target = self:GetTarget(database, item); if target then return target:IsValidForCharacter(character); end return true; end function TargetItemMixin:Visible(database, item, character, showAll) if not showAll and item.lowPriority then return false end if item.visible ~= nil then return ItemMixin.Visible(self, database, item, character); end local target = self:GetTarget(database, item); if target then return target:Visible(character); end return true; end function TargetItemMixin:GetName(database, item, character) if item.name then return ItemMixin.GetName(self, database, item, character); end local target = self:GetTarget(database, item); if target then return target:GetName(character); end return "Unnamed" end function TargetItemMixin:GetSubtext(database, item, character, small) if item.subtext then return ItemMixin.GetSubtext(self, database, item, character); end local target = self:GetTarget(database, item); if target then return target:GetSubtext(character, small); end end function TargetItemMixin:GetAlternative(database, item, character) if item.alternatives ~= nil then return ItemMixin.GetAlternative(self, database, item, character); end local target = self:GetTarget(database, item); if target then return target:GetAlternative(character); end end function TargetItemMixin:IsAvailable(database, item, character) if item.prerequisites ~= nil then return ItemMixin.IsAvailable(self, database, item, character); end for i=1,self:TargetCount(database, item) do local target = self:GetTarget(database, item, i); if target and target:IsAvailable(character) then return true end end return false end function TargetItemMixin:IsActive(database, item, character) if item.active ~= nil then return ItemMixin.IsActive(self, database, item, character); end for i=1,self:TargetCount(database, item) do local target = self:GetTarget(database, item, i); if target and target:IsActive(character) then return true end end return false end function TargetItemMixin:IsCompleted(database, item, character, ...) if item.completed ~= nil then return ItemMixin.IsCompleted(self, database, item, character); end local type = self:GetTargetType(database, item); if type == nil or not database:HasDataType(type) then return false; end local amount = 0 if item.ids then for _,id in ipairs(item.ids) do if CheckTargetStatus(database:GetData(type, id), item, character) then amount = amount + 1 end end else if CheckTargetStatus(database:GetData(type, item.id), item, character) then amount = amount + 1 end end return CheckStatusCount(amount, item) end function TargetItemMixin:GetPrerequisites(database, item, character) if item.prerequisites ~= nil then return ItemMixin.GetPrerequisites(self, database, item, character); end local target = self:GetTarget(database, item); if target then return target:GetPrerequisites(character); end end function TargetItemMixin:GetRewards(database, item, character) if item.rewards ~= nil then return ItemMixin.GetRewards(self, database, item, character); end local target = self:GetTarget(database, item); if target then return target:GetRewards(character); end end local HeaderItemMixin = CreateFromMixins(ItemMixin); local QuestItemMixin = CreateFromMixins(TargetItemMixin); -- Using this instead of the target system because some quests wont be in our database function QuestItemMixin:IsCompleted(database, item, character, ...) if item.completed ~= nil then return ItemMixin.IsCompleted(self, database, item, character) end return StatusCompleted(item, character, CheckQuestStatus) end function QuestItemMixin:GetContentTuningID(database, item) return item.contentTuningID or self:GetTarget(database, item):GetContentTuningID(); end function QuestItemMixin:GetLevel(database, item) return item.level or self:GetTarget(database, item):GetLevel(); end function QuestItemMixin:GetRequiredLevel(database, item) return item.requiredLevel or self:GetTarget(database, item):GetRequiredLevel(); end function QuestItemMixin:GetMaxLevel(database, item) return item.requiredLevel or self:GetTarget(database, item):GetMaxLevel(); end function QuestItemMixin:GetLevelFlag(database, item) return item.levelFlag or self:GetTarget(database, item):GetLevelFlag(); end if INTERFACE_NUMBER < 90000 then function QuestItemMixin:GetLink(database, item) return format("\124cffffff00\124Hquest:%d:%d:%d:%d:%d\124h[%s]\124h\124r", self:GetID(database, item), self:GetLevel(database, item), self:GetRequiredLevel(database, item), self:GetMaxLevel(database, item), self:GetLevelFlag(database, item), self:GetName(database, item)); end else function QuestItemMixin:GetLink(database, item) return format("\124cffffff00\124Hquest:%d:%d\124h[%s]\124h\124r", self:GetID(database, item), self:GetContentTuningID(database, item), self:GetName(database, item)); end end function QuestItemMixin:OnClick(database, item, character, button, frame, tooltip) if item.onClick ~= nil then return ItemMixin.OnClick(self, database, item, character, button, frame, tooltip) end if self:GetTarget(database, item) and ChatEdit_TryInsertChatLink(self:GetLink(database, item)) then return end local questID = self:GetID(database, item) local questLogIndex = GetLogIndexForQuestID(questID) if IsModifiedClick("QUESTWATCHTOGGLE") then if GetQuestWatchType(questID) ~= nil then RemoveQuestWatch(questID) else AddQuestWatch(questID) end return end if questLogIndex and questLogIndex ~= 0 then if BtWQuestsFrame:SelectFromLink(self:GetLink(database, item)) then return end else local source = self:GetSource(database, item, character) if source then local mapID, coords = source:GetLocation() if mapID and coords then BtWQuests_ShowMapWithWaypoint(mapID, coords.x, coords.y, source:GetName()) end end end end function QuestItemMixin:OnEnter(database, item, character, button, frame, tooltip) if item.onEnter ~= nil then return ItemMixin.OnEnter(self, database, item, character, button, frame, tooltip) end if tooltip ~= nil then local target = self:GetTarget(database, item) local userdata = self:GetUserdata(database, item) local link = userdata and userdata.link or (target and self:GetLink(database, item)) if link then tooltip:SetPoint("TOPLEFT", button, "TOPRIGHT") tooltip:SetOwner(button, "ANCHOR_PRESERVE"); tooltip:SetHyperlink(link, character) end end end function QuestItemMixin:OnLeave(database, item, character, button, frame, tooltip) if item.onLeave ~= nil then return ItemMixin.OnLeave(self, database, item, character, button, frame, tooltip) end if tooltip ~= nil then tooltip:Hide() end end function QuestItemMixin:GetSource(database, item, character) if item.source ~= nil then return database:CreateItem(-1, item.source, item, self:GetRoot(database, item)); end local target = self:GetTarget(database, item); if target then return target:GetSource(character); end return nil; end local ExpansionItemMixin = CreateFromMixins(TargetItemMixin); local CategoryItemMixin = CreateFromMixins(TargetItemMixin); function CategoryItemMixin:GetLink(database, item) return self:GetTarget(database, item):GetLink(); end function CategoryItemMixin:IsMajor(database, item) return item.major or self:GetTarget(database, item):IsMajor(); end function CategoryItemMixin:GetItemList(database, item, ...) return self:GetTarget(database, item):GetItemList(...); end function CategoryItemMixin:GetButtonImage(database, item) if item.buttonImage == nil then return self:GetTarget(database, item):GetButtonImage(); end if type(item.buttonImage) ~= "table" then return item.buttonImage end return item.buttonImage.texture, unpack(item.buttonImage.texCoords) end function CategoryItemMixin:GetListImage(database, item) if item.listImage == nil then return self:GetTarget(database, item):GetListImage(); end if type(item.listImage) ~= "table" then return item.listImage end return item.listImage.texture, unpack(item.listImage.texCoords) end function CategoryItemMixin:OnClick(database, item, character, button, frame, tooltip) if ChatEdit_TryInsertChatLink(self:GetLink(database, item)) then return end local userdata = self:GetUserdata(database, item) if frame:SelectFromLink(self:GetLink(database, item), userdata and userdata.scrollTo) then PlaySound(SOUNDKIT.IG_SPELLBOOK_OPEN) return end tooltip:Hide() end function CategoryItemMixin:OnEnter(database, item, character, button, frame, tooltip) end function CategoryItemMixin:OnLeave(database, item, character, button, frame, tooltip) tooltip:Hide() end local ChainItemMixin = CreateFromMixins(TargetItemMixin); function ChainItemMixin:GetName(database, item, character) local name = TargetItemMixin.GetName(self, database, item, character) local uptoType = type(item.upto) if uptoType == "table" then elseif uptoType == "number" then local quest = database:GetQuestByID(item.upto) if quest then return string.format(L["UP_TO"], name, quest:GetName()) end end return name end function ChainItemMixin:GetLink(database, item) return self:GetTarget(database, item):GetLink(); end function ChainItemMixin:IsMajor(database, item) if item.major ~= nil then return item.major; end return self:GetTarget(database, item):IsMajor(); end function ChainItemMixin:GetButtonImage(database, item) if item.buttonImage == nil then return self:GetTarget(database, item):GetButtonImage(); end if type(item.buttonImage) ~= "table" then return item.buttonImage end return item.buttonImage.texture, unpack(item.buttonImage.texCoords) end function ChainItemMixin:GetListImage(database, item) if item.listImage == nil then return self:GetTarget(database, item):GetListImage(); end if type(item.listImage) ~= "table" then return item.listImage end return item.listImage.texture, unpack(item.listImage.texCoords) end function ChainItemMixin:GetItem(database, item, index, character) return self:GetTarget(database, item):GetItem(index, character); end function ChainItemMixin:GetNumItems(database, item) return self:GetTarget(database, item):GetNumItems(); end function ChainItemMixin:OnClick(database, item, character, button, frame, tooltip) if ChatEdit_TryInsertChatLink(self:GetLink(database, item)) then return end local userdata = self:GetUserdata(database, item) if frame:SelectFromLink(self:GetLink(database, item), userdata and userdata.scrollTo) then return end tooltip:Hide() end function ChainItemMixin:OnEnter(database, item, character, button, frame, tooltip) local userdata = self:GetUserdata(database, item) local link = userdata and userdata.link or self:GetLink(database, item) tooltip:SetPoint("TOPLEFT", button, "TOPRIGHT"); tooltip:SetOwner(button, "ANCHOR_PRESERVE"); tooltip:SetHyperlink(link, character); end function ChainItemMixin:OnLeave(database, item, character, button, frame, tooltip) tooltip:Hide() end function ChainItemMixin:IsEmbed(database, item) return item.embed end function ChainItemMixin:IsCompleted(database, item, character, ...) if item.completed ~= nil then return ItemMixin.IsCompleted(self, database, item, character); end local uptoType = type(item.upto) if uptoType == "table" then elseif uptoType == "number" then return character:IsQuestCompleted(item.upto) end return TargetItemMixin.IsCompleted(self, database, item, character, ...) end local MissionItemMixin = CreateFromMixins(TargetItemMixin); function MissionItemMixin:IsBreadcrumb() return true end local NPCItemMixin = CreateFromMixins(TargetItemMixin); function NPCItemMixin:GetTargetType() return "npc" end function NPCItemMixin:GetName(database, item, character) return string.format(L["BTWQUESTS_GO_TO"], TargetItemMixin.GetName(self, database, item, character)) end function NPCItemMixin:IsBreadcrumb(database, item, character) if item.breadcrumb ~= nil then return item.breadcrumb; end return true end function NPCItemMixin:GetLocation(database, item, ...) if item.locations ~= nil then return BtWQuests_GetBestLocation(database, item.locations, ...) end local target = self:GetTarget(database, item); if target then return target:GetLocation(...); end return nil; end function NPCItemMixin:OnClick(database, item, character, button, frame, tooltip) local mapID, coords = self:GetLocation(database, item) if mapID and coords then BtWQuests_ShowMapWithWaypoint(mapID, coords.x, coords.y, self:GetName(database, item)) end end local KillItemMixin = CreateFromMixins(NPCItemMixin); function KillItemMixin:GetName(database, item, character) return string.format(L["BTWQUESTS_KILL"], TargetItemMixin.GetName(self, database, item, character)) end local TalkItemMixin = CreateFromMixins(NPCItemMixin); function TalkItemMixin:GetName(database, item, character) return string.format(L["BTWQUESTS_TALK_TO"], TargetItemMixin.GetName(self, database, item, character)) end local ObjectItemMixin = CreateFromMixins(NPCItemMixin); function ObjectItemMixin:GetTargetType() return "object" end local LootItemMixin = CreateFromMixins(ObjectItemMixin); function LootItemMixin:GetName(database, item, character) return string.format(L["BTWQUESTS_LOOT"], TargetItemMixin.GetName(self, database, item, character)) end local LevelItemMixin = CreateFromMixins(ItemMixin); function LevelItemMixin:GetName(database, item, character) if item.name then return ItemMixin.GetName(self, database, item, character); end return string.format(L["LEVEL_TO"], item.level); end function LevelItemMixin:IsActive(database, item, character) if self:IsCompleted(database, item, character) then return false; end return true end function LevelItemMixin:IsCompleted(database, item, character) if item.atmost then return character:AtmostLevel(item.level); else return character:AtleastLevel(item.level); end end local ExperienceItemMixin = CreateFromMixins(ItemMixin); function ExperienceItemMixin:GetName(database, item, character) if item.name then return ItemMixin.GetName(self, database, item, character); end local amount; if item.amounts then local level = math.min(math.max(character:GetLevel(), item.minLevel), item.maxLevel) - item.minLevel + 1 if level > #item.amounts then if character:GetLevel() > OUTDATED_LEVEL and item.outdated then amount = item.outdated; else amount = item.amounts[#item.amounts]; end else amount = item.amounts[level]; end else amount = item.amount; end local modifier = 1 + character:GetXPModifier(); if character:IsWarModeDesired() and not item.noWarModeBonus then modifier = modifier + (character:GetWarModeRewardBonus() * 0.01); end return format(GAIN_EXPERIENCE, math.floor(amount * modifier + .5)) end function ExperienceItemMixin:Visible(database, item, character) return character:GetLevel() < MAX_PLAYER_LEVEL end function ExperienceItemMixin:IsActive(database, item, character) return true end function ExperienceItemMixin:IsCompleted(database, item, character) return false end local Races = {} for i=1,100 do local race = C_CreatureInfo.GetRaceInfo(i); if race then Races[race.clientFileString] = race end end local RaceItemMixin = CreateFromMixins(ItemMixin); function RaceItemMixin:GetName(database, item, character, variation) local name if item.name then name = ItemMixin.GetName(self, database, item, character); else local race = Races[item.id] or C_CreatureInfo.GetRaceInfo(item.id); name = race and race.raceName end return name end function RaceItemMixin:IsCompleted(database, item, character) if item.id then return character:IsRace(item.id); else return character:InRaces(item.ids); end end local ClassItemMixin = CreateFromMixins(ItemMixin); function ClassItemMixin:GetName(database, item, character, variation) local name if item.name then name = ItemMixin.GetName(self, database, item, character); else local class = C_CreatureInfo.GetClassInfo(item.id); name = class and class.className end return name end function ClassItemMixin:IsCompleted(database, item, character) if item.id then return character:IsClass(item.id); else return character:InClasses(item.ids); end end local FactionItemMixin = CreateFromMixins(ItemMixin); function FactionItemMixin:GetName(database, item, character, variation) local name if item.name then name = ItemMixin.GetName(self, database, item, character); elseif item.id == "Horde" then name = FACTION_HORDE elseif item.id == "Alliance" then name = FACTION_ALLIANCE end return name end function FactionItemMixin:IsCompleted(database, item, character) return character:IsFaction(item.id); end local ReputationItemMixin = CreateFromMixins(ItemMixin); function ReputationItemMixin:GetName(database, item, character, variation) local name if item.name then name = ItemMixin.GetName(self, database, item, character); end local factionName, standing, barMin, _, value = (character or BtWQuestsCharacters:GetPlayer()):GetFactionInfoByID(item.id) if factionName == nil then factionName = L["UNKNOWN"] end if item.standing == nil then if item.amount ~= nil then local amount = item.amount -- if character:IsRace(BTWQUESTS_RACE_ID_HUMAN) then -- amount = amount * 1.1 -- end name = string.format(L["REPUTATION_WITH"], amount, factionName) elseif variation == "reward" or variation == "prerequisite" then name = string.format(L["BTWQUESTS_PREFIX"], L["BTWQUESTS_FACTION"], factionName) else name = factionName; end else local gender = character:GetSex() local standingText = getglobal("FACTION_STANDING_LABEL" .. item.standing .. (gender == 3 and "_FEMALE" or "")) if item.amount ~= nil then name = string.format(name or L["BTWQUESTS_REPUTATION_AMOUNT_STANDING"], item.amount, standingText, factionName) else name = string.format(name or L["BTWQUESTS_REPUTATION_STANDING"], standingText, factionName) end end return name end function ReputationItemMixin:IsActive(database, item, character) assert(character ~= nil); if self:IsCompleted(database, item, character) then return false; end if item.active ~= nil then return database:EvalRequirement(item.active, self, character, true); end return true end function ReputationItemMixin:IsCompleted(database, item, character) local function Callback(id, item, character) local factionName, standing, barMin, _, value = character:GetFactionInfoByID(item.id) if standing == nil then return false elseif item.amount ~= nil then return standing > item.standing or (standing == item.standing and value - barMin >= item.amount) else return standing >= item.standing end end return StatusCompleted(item, character, Callback) end local FriendshipItemMixin = CreateFromMixins(ItemMixin); function FriendshipItemMixin:GetName(database, item, character) local name if item.name then name = ItemMixin.GetName(self, database, item, character); end return name end function FriendshipItemMixin:IsActive(database, item, character) assert(character ~= nil); if self:IsCompleted(database, item, character) then return false; end if item.active ~= nil then return database:EvalRequirement(item.active, self, character, true); end return true end function FriendshipItemMixin:IsCompleted(database, item, character) local factionInfo = character:GetFriendshipReputation(item.id) return (factionInfo and factionInfo.standing or 0) >= item.amount end local AchievementItemMixin = CreateFromMixins(ItemMixin); function AchievementItemMixin:GetName(database, item, character) if item.name then return ItemMixin.GetName(self, database, item, character); end local id = self:GetID(database, item); if item.criteria then return select(1, GetAchievementCriteriaInfo(id, item.criteria)) elseif item.criteriaId then return select(1, GetAchievementCriteriaInfoByID(id, item.criteriaId)) elseif item.reward then return select(11, GetAchievementInfo(id)) else return select(2, GetAchievementInfo(id)) end end function AchievementItemMixin:IsActive(database, item, character) return true end function AchievementItemMixin:IsCompleted(database, item, character) if item.criteria then if item.completed == false then return not select(3, character:GetAchievementCriteriaInfo(item.id, item.criteria)) else return select(3, character:GetAchievementCriteriaInfo(item.id, item.criteria)) end elseif item.criteriaId then if item.completed == false then return not select(3, character:GetAchievementCriteriaInfoByID(item.id, item.criteriaId)) else return select(3, character:GetAchievementCriteriaInfoByID(item.id, item.criteriaId)) end elseif item.anyone then if item.completed == false then return not select(4, GetAchievementInfo(item.id)) else return select(4, GetAchievementInfo(item.id)) end else if item.completed == false then return not select(13, character:GetAchievementInfo(item.id)) else return select(13, character:GetAchievementInfo(item.id)) end end end local MoneyItemMixin = CreateFromMixins(ItemMixin); function MoneyItemMixin:GetName(database, item, character) if item.name then return ItemMixin.GetName(self, database, item, character); end if item.amounts then local level = math.min(math.max(character:GetLevel(), item.minLevel), item.maxLevel) - item.minLevel + 1 return GetCoinTextureString(item.amounts[level]) end return GetCoinTextureString(item.amount) end function MoneyItemMixin:IsCompleted(database, item, character) return false end function MoneyItemMixin:IsActive(database, item, character) return true end local CurrencyItemMixin = CreateFromMixins(ItemMixin); function CurrencyItemMixin:GetName(database, item, character) if item.name then return ItemMixin.GetName(self, database, item, character); end local name = L["BTWQUESTS_CURRENCY"]; local info = C_CurrencyInfo.GetBasicCurrencyInfo(item.id, item.amount); return format(name, info.icon, info.displayAmount, info.name); end function CurrencyItemMixin:IsCompleted(database, item, character) return character:GetCurrencyQuantity(item.id) >= item.amount end function CurrencyItemMixin:IsActive(database, item, character) return true end local TimeItemMixin = CreateFromMixins(ItemMixin); function TimeItemMixin:GetName(database, item, character) if item.name then return ItemMixin.GetName(self, database, item, character); end local total,days,hours,minutes,seconds = difftime(item.time, GetServerTime()) if total <= 0 then return L["BTWQUESTS_PASSED"] end days = floor(total / 86400) total = total % 86400 hours = floor(total / 3600) total = total % 3600 minutes = floor(total / 60) seconds = total % 60 if days ~= nil and days ~= 0 then return string.format(L["BTWQUESTS_COUNTDOWN_DHM"], days, hours, minutes) elseif hours ~= nil and hours ~= 0 then return string.format(L["BTWQUESTS_COUNTDOWN_HMS"], hours, minutes, seconds) elseif minutes ~= nil and minutes ~= 0 then return string.format(L["BTWQUESTS_COUNTDOWN_MS"], minutes, seconds) else return string.format(L["BTWQUESTS_COUNTDOWN_S"], seconds) end end function TimeItemMixin:IsCompleted(database, item, character) return GetServerTime() >= item.time end function TimeItemMixin:IsActive(database, item, character) return true end local TimeZoneItemMixin = CreateFromMixins(ItemMixin); function TimeZoneItemMixin:IsCompleted(database, item, character) return BtWQuests_GetTimeZone(character:GetRealm()) == item.timezone end local CoordsItemMixin = CreateFromMixins(ItemMixin); function CoordsItemMixin:IsBreadcrumb(database, item, character) if item.breadcrumb ~= nil then return item.breadcrumb; end return true end function CoordsItemMixin:GetLocation(database, item, relativeMapID, ...) if item.locations ~= nil then return BtWQuests_GetBestLocation(database, item.locations, relativeMapID, ...) end if relativeMapID == nil or item.mapID == relativeMapID then return item.mapID, CreateVector2D(item.x, item.y) else local sourceMapID, sourceCoords = self:GetLocation(database, item) if sourceCoords == nil then return nil end local continentID, coords = C_Map.GetWorldPosFromMapPos(sourceMapID, sourceCoords) if coords == nil then return nil end local _, coords = C_Map.GetMapPosFromWorldPos(continentID, coords, relativeMapID) if coords == nil then return nil end return relativeMapID, coords end end function CoordsItemMixin:OnClick(database, item, character, button, frame, tooltip) local mapID, coords = self:GetLocation(database, item) if mapID and coords then BtWQuests_ShowMapWithWaypoint(mapID, coords.x, coords.y, self:GetName(database, item)) end end local PetItemMixin = CreateFromMixins(ItemMixin); function PetItemMixin:GetName(database, item, character, variation) if item.name then return ItemMixin.GetName(self, database, item, character); end local id = self:GetID(database, item) local name = C_PetJournal.GetPetInfoBySpeciesID(id) or "" if variation == "reward" or variation == "prerequisite" then return string.format(L["BTWQUESTS_PREFIX"], L["BTWQUESTS_PET"], name) elseif item.status == 'summon' then return string.format(L["BTWQUESTS_SUMMON"], name) else return name end end function PetItemMixin:IsCompleted(database, item, character) return StatusCompleted(item, character, CheckPetStatus) end local MountItemMixin = CreateFromMixins(ItemMixin); function MountItemMixin:GetName(database, item, character, variation) if item.name then return ItemMixin.GetName(self, database, item, character); end local id = self:GetID(database, item) local name = C_MountJournal.GetMountInfoByID(id) or "" if variation == "reward" then return string.format(L["BTWQUESTS_PREFIX"], L["BTWQUESTS_MOUNT"], name) else return name end end function MountItemMixin:IsCompleted(database, item, character) return StatusCompleted(item, character, CheckMountStatus) end local ToyItemMixin = CreateFromMixins(ItemMixin); function ToyItemMixin:GetName(database, item, character, variation) if item.name then return ItemMixin.GetName(self, database, item, character); end local name = select(2, C_ToyBox.GetToyInfo(item.id or item.ids[1])) or "" if variation == "reward" then return format(L["BTWQUESTS_PREFIX"], L["BTWQUESTS_TOY"], name) else return name end end function ToyItemMixin:IsCompleted(database, item, character) return StatusCompleted(item, character, PlayerHasToy) end local AuraItemMixin = CreateFromMixins(ItemMixin); function AuraItemMixin:IsCompleted(database, item, character) local id = self:GetID(database, item); local index = 1 local name, _, count, _, _, _, _, _, _, spellId = UnitAura("player", index) while name do if spellId == id then return true end index = index + 1 name, _, count, _, _, _, _, _, _, spellId = UnitAura("player", index) end end local ProfessionItemMixin = CreateFromMixins(ItemMixin); function ProfessionItemMixin:GetName(database, item, character, variation) if item.name then return ItemMixin.GetName(self, database, item, character); end local id = self:GetID(database, item) local name = C_TradeSkillUI.GetTradeSkillDisplayName(id) if item.level then return string.format(L["BTWQUESTS_SKILL_LEVEL"], item.level, name or id) else return name or id end end function ProfessionItemMixin:IsCompleted(database, item, character) local id = self:GetID(database, item) if item.level then local level, maxLevel = character:GetSkillInfo(id) return level >= item.level else local level, maxLevel = character:GetSkillInfo(id) if level ~= 0 then return true else return character:HasProfession(id) -- Fallback end end end local ItemItemMixin = CreateFromMixins(ItemMixin); function ItemItemMixin:GetName(database, item, character, variation) if item.name then return ItemMixin.GetName(self, database, item, character); end local id = self:GetID(database, item); local name = GetItemInfo(id); if variation == "reward" then return name or L["UNKNOWN"]; else return string.format(L["BTWQUESTS_COLLECT"], name or L["UNKNOWN"]); end end function ItemItemMixin:IsCompleted(database, item, character) if character:IsPlayer() then return StatusCompleted(item, character, CheckItemStatus); end end local EquippedItemMixin = CreateFromMixins(ItemMixin); function EquippedItemMixin:GetName(database, item, character, variation) if item.name then return ItemMixin.GetName(self, database, item, character); end local id = self:GetID(database, item); local name = GetItemInfo(id); return string.format(L["BTWQUESTS_EQUIP"], name); end function EquippedItemMixin:IsCompleted(database, item, character) if character:IsPlayer() then return StatusCompleted(item, character, IsEquippedItem); end end local QuestLineItemMixin = CreateFromMixins(ItemMixin); function QuestLineItemMixin:IsCompleted(database, item, character) if item.questID and (character:IsQuestActive(item.questID) or character:IsQuestCompleted(item.questID)) then return true end if character:IsPlayer() then local questLines = C_QuestLine.GetAvailableQuestLines(item.mapID) for _,questLine in ipairs(questLines) do if questLine.questLineID == item.id then if item.questID == nil then return true else return questLine.questID == item.questID end end end end return false end local FollowerItemMixin = CreateFromMixins(ItemMixin); function FollowerItemMixin:GetName(database, item, character) local follower = C_Garrison.GetFollowerInfo(item.id) return follower and follower.name end function FollowerItemMixin:IsCompleted(database, item, character) return false end local GarrisonTalentTreeItemMixin = CreateFromMixins(ItemMixin); function GarrisonTalentTreeItemMixin:GetName(database, item, character) local info = C_Garrison.GetTalentTreeInfo(item.id) if item.rank then return string.format(L["RANK"], info and info.title or L["UNKNOWN"], item.rank) else return info and info.title or L["UNKNOWN"] end end function GarrisonTalentTreeItemMixin:IsActive(database, item, character) local treeInfo = C_Garrison.GetTalentTreeInfo(item.id); for _,talent in ipairs(treeInfo.talents) do if talent.tier + 1 == item.rank then return talent.isBeingResearched end end return false end function GarrisonTalentTreeItemMixin:IsCompleted(database, item, character) local rank = C_Garrison.GetTalentPointsSpentInTalentTree(item.id) if item.rank then return item.rank <= rank else return rank >= 0 end end local GarrisonTalentItemMixin = CreateFromMixins(ItemMixin); function GarrisonTalentItemMixin:GetName(database, item, character) local info = C_Garrison.GetTalentInfo(item.id) if item.rank then return string.format(L["RESEARCH_RANK"], info and info.name or L["UNKNOWN"], item.rank) else return string.format(L["RESEARCH"], info and info.name or L["UNKNOWN"]) end end function GarrisonTalentItemMixin:IsActive(database, item, character) local info = C_Garrison.GetTalentInfo(item.id); return info.isBeingResearched end function GarrisonTalentItemMixin:IsCompleted(database, item, character) local info = C_Garrison.GetTalentInfo(item.id); if item.rank then return item.rank <= info.talentRank else return info.researched end end local CampaignItemMixin = CreateFromMixins(ItemMixin); function CampaignItemMixin:GetName(database, item, character) local info = C_CampaignInfo.GetCampaignInfo(item.id) return info and info.name or L["UNKNOWN"] end local AreaItemMixin = CreateFromMixins(CoordsItemMixin); function AreaItemMixin:GetName(database, item, character) return string.format(L["BTWQUESTS_GO_TO"], (C_Map.GetAreaInfo(item.id))) end local ChromieTimeItemMixin = CreateFromMixins(ItemMixin); function ChromieTimeItemMixin:IsCompleted(database, item, character) local chromieId = character:GetChromieTimeID() if item.id then return item.id == chromieId else return chromieId >= 0 end end local DatabaseItemMetatable = {}; function DatabaseItemMetatable.__index(tbl, key) local details; if tbl.item.type ~= nil then details = tbl.database.ItemTypes[tbl.item.type]; else details = ItemMixin; end return details[key] and function (self, ...) return details[key](details, tbl.database, tbl.item, ...); end end local function CreateTable(database, mixin) local target, sources = {}, {}; setmetatable(target, { __index = function (self, key) if key ~= nil then local tbl; for _,source in ipairs(sources) do if source[key] then tbl = Mixin({database = database, id = key}, source[key], mixin); break; end end self[key] = tbl; return tbl; end end, }); return target, sources; end local function SplitRanges(...) local i = 0 local tbl = {...} return function () i = i + 1 local range = tbl[i] if not range then return nil end local from, to = strsplit("-", range, 2) if to == nil then to = from end from = tonumber(from) to = tonumber(to) assert(from and to, "Range number be number-number") return from, to end end local function GetRanges(str) return SplitRanges(strsplit(",", str)) end local ConditionCacheChildMetatable = { } local Database = {}; function Database:Init() do local database = self self.ConditionCache = setmetatable({}, { __index = function (self, character) if type(character) == "table" then local result = setmetatable({}, { __index = function (self, id) if type(id) == "number" then local result = database:GetData("condition", id):EvalFor(character); self[id] = result return result end end }); self[character] = result return result end end }); end self.DataTypes = {}; self.ItemTypes = {}; self.buckets = {}; self.questCache = {}; self.QuestIDToItem = {}; self.MapIDToItem = {}; self.Continents = {}; end function Database:RegisterDataType(dataType, mixin) self.DataTypes[dataType] = mixin; self[dataType], self[dataType.."List"] = CreateTable(self, mixin); self[dataType.."Ranges"] = {} end function Database:AddData(dataType, id, item) assert(self[dataType] ~= nil, format("Missing data type %s", dataType)); self[dataType][tonumber(id)] = CreateFromMixins({database = self, id = tonumber(id), items = {}}, item, self.DataTypes[dataType]); return self[dataType][tonumber(id)]; end function Database:AddDataTable(dataType, items) assert(self[dataType] ~= nil, format("Missing data type %s", dataType)); local list = self[dataType.."List"]; list[#list+1] = items; end function Database:UpdateDataTable(dataType, items) assert(self[dataType] ~= nil, format("Missing data type %s", dataType)); local list = self[dataType.."List"]; for _,listItems in pairs(list) do for itemID,item in pairs(items) do if listItems[itemID] then Mixin(listItems[itemID], item); end end end end function Database:HasDataType(dataType) return self[dataType] ~= nil; end function Database:GetData(dataType, id) assert(self[dataType] ~= nil, format("Missing data type %s", dataType)); return self[dataType][tonumber(id)]; end function Database:AddDataRanges(dataType, str, target) assert(self[dataType] ~= nil, format("Missing data type %s", dataType)); assert(target ~= nil, format("Must have a target")) local tbl = self[dataType.."Ranges"]; for from,to in GetRanges(str) do tbl[#tbl+1] = {from=from,to=to,target=target} end table.sort(tbl, function (a, b) return a.from < b.from end) end function Database:LoadExpansionForDataID(dataType, id) assert(self[dataType] ~= nil, format("Missing data type %s", dataType)); local tbl = self[dataType.."Ranges"]; id = tonumber(id) for _,item in ipairs(tbl) do if item.from <= id and item.to >= id then self:GetExpansionByID(item.target):Load() end end end function Database:RegisterItemType(itemType, mixin) self.ItemTypes[itemType] = mixin; end function Database:GetItemType(itemType) return itemType and self.ItemTypes[itemType] or ItemMixin; end function Database:CreateItem(index, item, parent, root) item.index = index; item.parent = parent; item.root = root or parent; local result = {database = self, item = item}; setmetatable(result, DatabaseItemMetatable); return result; end function Database:GetItemTypeDetails(itemType) local details = self.ItemTypes[itemType]; assert(details ~= nil, format("Unknown item type %s", itemType)); return details; end function Database:IsItemValidForCharacter(item, character) -- In effect its the same as ItemMixin:IsValidForCharacter if item == nil then return false; end if item.restrictions ~= nil and not self:EvalRequirement(item.restrictions, item, character) then return false; end return true; end function Database:FilterItems(items, character, tbl) tbl = tbl or {} if items[1] == nil then if self:IsItemValidForCharacter(items, character) then tbl[#tbl+1] = items end else for _,item in ipairs(items) do if self:IsItemValidForCharacter(item, character) then tbl[#tbl+1] = item end end end return tbl end -- /dump BtWQuestsDatabase:EvalRequirement() function Database:EvalRequirement(requirement, item, character, one) if type(requirement) == "boolean" then return requirement elseif type(requirement) == "number" then return self:EvalCondition(requirement, character) elseif type(requirement) == "function" then return self:EvalRequirement(requirement(item, character), item, character) elseif type(requirement) == "table" then if requirement[1] ~= nil then one = one and true or false -- Should we only require 1 item to be true local filtered = {} for _, v in ipairs(requirement) do if self:IsItemValidForCharacter(v, character) then table.insert(filtered, v) end end for _, v in ipairs(filtered) do if self:EvalItemRequirement(v, character) == one then return one end end return not one else return self:EvalItemRequirement(requirement, character) end end assert(requirement == nil, "Invalid requirement type " .. type(requirement)) end function Database:EvalItemRequirement(item, character) local item = GetVariation(self, item, character) if item.type == "quest" then local ids = item.ids or {item.id} local amount = 0 for _,id in ipairs(ids) do if CheckQuestStatus(id, item, character) then amount = amount + 1 end end return CheckStatusCount(amount, item) elseif item.type == "chain" then local ids = item.ids or {item.id} local amount = 0 for _,id in ipairs(ids) do if CheckChainStatus(id, item, character) then amount = amount + 1 end end return CheckStatusCount(amount, item) elseif item.type == "category" then local ids = item.ids or {item.id} local amount = 0 for _,id in ipairs(ids) do if CheckCategoryStatus(id, item, character) then amount = amount + 1 end end return CheckStatusCount(amount, item) elseif item.type ~= nil then assert(self.ItemTypes[item.type] ~= nil, format("Unknown type %s", item.type)); return self.ItemTypes[item.type]:IsCompleted(self, item, character); else return self:EvalRequirement(item.onEval or item.completed, item, character) end end function Database:EvalText(text, item, character) local result = text; if text == nil then result = "Unnamed" elseif type(text) == "function" then result = self:EvalText(text(item, character), item, character) elseif type(text) == "table" then if text[1] ~= nil then result = "Unnamed" for _,t in ipairs(text) do if self:IsItemValidForCharacter(t, character) then result = self:EvalItemName(t, character) break; end end else result = self:EvalItemName(text, character) end end return result ~= nil and tostring(result) or nil; end function Database:EvalItemName(item, character) if item == nil then return "Unnamed" end if item.name then return self:EvalText(item.name, item, character) end if self.DataTypes[item.type] then local item = self:GetData(item.type, item.id or item.ids[1]); if item == nil then return "Unnamed" end return item:GetName(); end assert(self.ItemTypes[item.type] ~= nil, format("Unknown type %s", item.type)); return self.ItemTypes[item.type]:GetName(self, item, character); end function Database:AddCondition(id, item) return self:AddData("condition", id, item); end function Database:AddConditionTable(items) self:AddDataTable("condition", items); end function Database:GetConditionByID(id) return self:GetData("condition", id); end function Database:EvalCondition(id, character) return self.ConditionCache[character][id]; end do local eventHandler = CreateFrame("Frame") eventHandler:SetScript("OnEvent", function () wipe(Database.ConditionCache[BtWQuestsCharacters:GetPlayer()]) end) function Database:RegisterConditionClearCacheEvent(event) eventHandler:RegisterEvent(event) end end -- Expansion function Database:AddExpansion(id, item) local expansion = self:GetData("expansion", id); if not expansion then if item.name == nil then item.name = L['BTWQUESTS_EXPANSION_NAME' .. id]; end expansion = self:AddData("expansion", id, item); end return expansion; end function Database:AddExpansionsTable(items) self:AddDataTable("expansion", items); end function Database:GetExpansionByID(id) return self:GetData("expansion", id); end function Database:AddExpansionItem(id, t) local expansion = self:GetData("expansion", id); table.insert(expansion.items, t); end function Database:AddExpansionItems(id, t) for _,v in ipairs(t) do self:AddExpansionItem(id, v); end end function Database:HasMultipleExpansion() local first = next(self.expansion) return first ~= nil and next(self.expansion, first) ~= nil end function Database:GetFirstExpansion() return (select(2, next(self.expansion))) end function Database:GetLoadedExpansion() local loadedExpansion = nil for i=0,LE_EXPANSION_LEVEL_CURRENT do local expansion = self:GetExpansionByID(i); if expansion and expansion:IsLoaded() then loadedExpansion = loadedExpansion == nil and expansion or false end end return loadedExpansion end function Database:HasExpansion(id) local expansion = self:GetData("expansion", id); return expansion ~= nil -- and expansion.items ~= nil and #expansion.items > 0; end function Database:GetExpansionList() local items = {} for i=0,LE_EXPANSION_LEVEL_CURRENT do local expansion = self:GetExpansionByID(i); if expansion then items[#items+1] = expansion; end end return items end local chromieTimeExpansionMap = { [5] = 0, [6] = 1, [7] = 2, [8] = 4, [9] = 5, [10] = 6, } function Database:GetBestExpansionForCharacter(character) local first = next(self.expansion) if next(self.expansion, first) == nil then return first end -- Do fancy chromie time stuff here local chromieTimeID = character:GetChromieTimeID() local expansion = chromieTimeExpansionMap[chromieTimeID] if self:HasExpansion(expansion) then return expansion end -- Not in chromie time so use player level local playerLevel = character:GetLevel() expansion = GetExpansionForLevel(playerLevel) if self:HasExpansion(expansion) then return expansion end -- Find the best expansion closest to the players current level for variance = 1,GetNumExpansions()-1 do if expansion+variance < GetNumExpansions() and self:HasExpansion(expansion+variance) then return expansion+variance end if expansion-variance >= 0 and self:HasExpansion(expansion-variance) then return expansion-variance end end return first end function Database:AddCategory(id, item) return self:AddData("category", id, item); end function Database:AddCategoriesTable(items) self:AddDataTable("category", items); end function Database:GetCategoryByID(id) return self:GetData("category", id); end function Database:IsCategoryActive(id, character) local item = self:GetCategoryByID(id) if item == nil then return nil end return item:IsActive(character) end function Database:IsCategoryCompleted(id, character) local item = self:GetCategoryByID(id) if item == nil then return nil end return item:IsCompleted(character) end function Database:GetCategoryName(id) if not id then return nil; end local item = self:GetCategoryByID(id); if not item then return nil; end return item:GetName(); end function Database:LoadCategory(id) self:LoadExpansionForDataID("category", id) return self:GetData("category", id); end function Database:AddCategoryRanges(str, target) self:AddDataRanges("category", str, target) end function Database:AddChain(id, item) return self:AddData("chain", id, item); end function Database:AddChainsTable(items) self:AddDataTable("chain", items); end function Database:GetChainByID(id) return self:GetData("chain", id); end function Database:IsChainActive(chainID, character) local chain = self:GetChainByID(chainID) if chain == nil then return nil end return chain:IsActive(character) end function Database:IsChainCompleted(chainID, character) local chain = self:GetChainByID(chainID) if chain == nil then return nil end return chain:IsCompleted(character) end function Database:GetChainName(id) if not id then return nil; end local item = self:GetChainByID(id); if not item then return nil; end return item:GetName(); end function Database:LoadChain(id) self:LoadExpansionForDataID("chain", id) return self:GetData("chain", id); end function Database:AddChainRanges(str, target) self:AddDataRanges("chain", str, target) end function Database:AddQuest(id, item) return self:AddData("quest", id, item); end function Database:AddQuestsTable(items) self:AddDataTable("quest", items); end function Database:UpdateQuestsTable(items) self:UpdateDataTable("quest", items); end function Database:GetQuestByID(id) return self:GetData("quest", id); end function Database:GetQuestName(id) if not id then return nil; end local quest = self:GetQuestByID(id); if not quest then return nil; end return quest:GetName(); end function Database:AddQuestItem(id, t, replace) if self.QuestIDToItem[id] == nil or replace then self.QuestIDToItem[id] = {} end table.insert(self.QuestIDToItem[id], t) end function Database:AddQuestItems(id, t, replace) if self.QuestIDToItem[id] == nil or replace then self.QuestIDToItem[id] = {} end for _,v in ipairs(t) do self:AddQuestItem(id, v) end end function Database:AddQuestItemsForOtherChain(chainID, otherChain, replace) local chain = self:GetChainByID(otherChain) assert(chain ~= nil) local items = chain.items local index = 1 local item = items[index] while item do if item[1] ~= nil then for _,subitem in ipairs(item) do local target = { type = "chain", id = chainID, restrictions = subitem.restrictions } local ids = subitem.ids or {subitem.id} for _,id in ipairs(ids) do self:AddQuestItem(id, target, replace) end end elseif item.type == "quest" then local target = { type = "chain", id = chainID, restrictions = item.restrictions } local ids = item.ids or {item.id} for _,id in ipairs(ids) do self:AddQuestItem(id, target, replace) end end index = index + 1 item = items[index] end end function Database:AddQuestItemsForChain(chainID, replace) local chain = self:GetChainByID(chainID) assert(chain ~= nil) local items = chain.items local index = 1 local item = items[index] while item do if item[1] ~= nil then for _,subitem in ipairs(item) do local target = { type = "chain", id = chainID, restrictions = subitem.restrictions } local ids = subitem.ids or {subitem.id} for _,id in ipairs(ids) do self:AddQuestItem(id, target, replace) end end elseif item.type == "quest" then local target = { type = "chain", id = chainID, restrictions = item.restrictions } local ids = item.ids or {item.id} for _,id in ipairs(ids) do self:AddQuestItem(id, target, replace) end end index = index + 1 item = items[index] end end function Database:GetQuestItem(questID, character) questID = tonumber(questID) local item = self.QuestIDToItem[questID] if item == nil then return nil end for i = 1,#item do if self:IsValidForCharacter(item[i], character) then return self:CreateItem(0, item[i]); end end return nil end function Database:AddMission(id, item) return self:AddData("mission", id, item); end function Database:AddMissionsTable(items) self:AddDataTable("mission", items); end function Database:UpdateMissionsTable(items) self:UpdateDataTable("mission", items); end function Database:GetMissionByID(id) return self:GetData("mission", id); end function Database:GetMissionName(id) if not id then return nil; end local item = self:GetMissionByID(id); if not item then return nil; end return item:GetName(); end function Database:AddNPC(id, item) return self:AddData("npc", id, item); end function Database:AddNPCsTable(items) self:AddDataTable("npc", items); end function Database:UpdateNPCsTable(items) self:UpdateDataTable("npc", items); end function Database:GetNPCByID(id) return self:GetData("npc", id); end function Database:AddObject(id, item) return self:AddData("object", id, item); end function Database:AddObjectsTable(items) self:AddDataTable("object", items); end function Database:UpdateObjectsTable(items) self:UpdateDataTable("object", items); end function Database:GetObjectByID(id) return self:GetData("object", id); end -- Search function Database:AddSearchBucket(key, t) if self.buckets[key] == nil then self.buckets[key] = {} end local u = self.buckets[key] for _,v in ipairs(t) do u[#u+1] = v -- table.insert(u,v) end end function Database:AddSearchBuckets(t) for k,v in pairs(t) do self:AddSearchBucket(k,v) end end function Database:SearchScore(a, b) local startChar, endChar = b:find(a) if not startChar then return 0 end return (endChar - startChar + 1) / b:len() end function Database:SearchTargetExists(item) local _, itemType, itemID = strsplit(':', item.link) if itemType == "expansion" then return self:GetExpansionByID(tonumber(itemID)) ~= nil elseif itemType == "category" then return self:GetCategoryByID(tonumber(itemID)) ~= nil elseif itemType == "chain" then return self:GetChainByID(tonumber(itemID)) ~= nil end return true end local keywords, results, keywordCharacters = {}, {}, {} function Database:Search(tbl, query, character) local tbl = tbl or {}; local query = string.gsub(query:lower(), "[,.?:;!'\"%-%(%)]", "") if self.questCache[character] == nil then self.questCache[character] = {} end if self.questCache[character][query] ~= nil then for _,v in ipairs(self.questCache[character][query]) do tbl[#tbl+1] = v; end return tbl; end local prefixlist local start = "" for character in gmatch(query, "[\32-\127\192-\247][\128-\191]*") do start = start .. character if self.buckets[start] ~= nil then prefixlist = self.buckets[start] break end end wipe(keywords); for keyword in string.gmatch(query, "[^%s]+") do keywords[#keywords+1] = keyword end wipe(results); if prefixlist == nil then return results end local item for i=1,#prefixlist do item = prefixlist[i] if results[item] == nil and self:IsItemValidForCharacter(item, character) and self:SearchTargetExists(item) then results[item] = 0 end end local keyword, result for k=1,#keywords do keyword = keywords[k] -- Filter items based on other keywords for item in pairs(results) do if item.keywords == nil and item.name ~= nil then item.keywords = gsub(lower(item.name), "[,.?:;!'\"%-%(%)]", "") end if type(item.keywords) == "string" then local keywords = item.keywords result = {}; for keyword in gmatch(keywords, "[^%s]+") do wipe(keywordCharacters) for character in gmatch(keyword, "[\32-\127\192-\247][\128-\191]*") do keywordCharacters[#keywordCharacters + 1] = character end for i=1,#keywordCharacters do for j=#keywordCharacters,i,-1 do local word = table.concat(keywordCharacters, "", i, j) result[word] = (result[word] or 0) + ((j - i + 1) / #keywordCharacters) / ((result[word] or 0) + 1) end end end item.keywords = result; end results[item] = results[item] + (item.keywords[keyword] or 0) end end wipe(tbl); for item,score in pairs(results) do if score > 0 then tbl[#tbl+1] = { name = item.name, item = item, score = score, } end end table.sort(tbl, function (a, b) if a.score == b.score then return (a.item.type or "") < (b.item.type or "") end return a.score > b.score end) self.questCache[character][query] = {unpack(tbl)} return tbl end -- Map function Database:AddMap(id, t, replace) if t[1] ~= nil then for _,v in ipairs(t) do self:AddMap(id, v) end else if self.MapIDToItem[id] ~= nil then if replace then self.MapIDToItem[id] = {t} else if self.MapIDToItem[id][1] == nil then self.MapIDToItem[id] = {self.MapIDToItem[id]} end table.insert(self.MapIDToItem[id], t) end else self.MapIDToItem[id] = t end end end function Database:AddMapRecursive(id, t, force, replace) self:AddMap(id, t, replace) local children = C_Map.GetMapChildrenInfo(id, nil, true) or {} for _,map in ipairs(children) do if force or self.MapIDToItem[map.mapID] == nil then self:AddMap(map.mapID, t, replace) end end end function Database:GetMapItemByID(mapID, character) local item = self.MapIDToItem[mapID] if item == nil then return nil end if item[1] ~= nil then for i = 1, #item do if self:IsItemValidForCharacter(item[i], character) then return item[i] end end elseif self:IsItemValidForCharacter(item, character) then return item end return nil end -- Adds quest chains to a list based on the continent they are on, later used to display map indicators function Database:AddContinentItem(id, t) local mapInfo = C_Map.GetMapInfo(id) while mapInfo and mapInfo.mapType ~= Enum.UIMapType.Continent and mapInfo.mapType ~= Enum.UIMapType.World and mapInfo.mapType ~= Enum.UIMapType.Cosmic do id = mapInfo.parentMapID mapInfo = C_Map.GetMapInfo(id) end assert(mapInfo ~= nil, string.format("Continent %d doesnt exist", id)) if self.Continents[id] == nil then self.Continents[id] = {t} else table.insert(self.Continents[id], t) end end function Database:AddContinentItems(id, t) local mapInfo = C_Map.GetMapInfo(id) while mapInfo and mapInfo.mapType ~= Enum.UIMapType.Continent and mapInfo.mapType ~= Enum.UIMapType.World and mapInfo.mapType ~= Enum.UIMapType.Cosmic do id = mapInfo.parentMapID mapInfo = C_Map.GetMapInfo(id) end assert(mapInfo ~= nil, string.format("Continent %d doesnt exist", id)) if self.Continents[id] == nil then self.Continents[id] = {} end for _,item in ipairs(t) do if item.type == "chain" then assert(self:GetChainByID(item.id) ~= nil, string.format("Chain %d doesnt exist", item.id)) end table.insert(self.Continents[id], item) end end function Database:GetAvailableMapItems(mapID, character) local continentID = mapID local mapInfo = C_Map.GetMapInfo(continentID); while mapInfo and mapInfo.mapType ~= Enum.UIMapType.Continent and mapInfo.mapType ~= Enum.UIMapType.World and mapInfo.mapType ~= Enum.UIMapType.Cosmic do continentID = mapInfo.parentMapID mapInfo = C_Map.GetMapInfo(continentID); end local result = {} if mapInfo and self.Continents[continentID] then local items = self.Continents[continentID] for _,item in ipairs(items) do local chain = self:GetChainByID(item.id) assert(chain ~= nil, string.format("Missing chain %d on map %d within continent %d", item.id, mapID, continentID)) if chain:IsValidForCharacter(character) and not chain:IsCompleted(character) and (chain:IsAvailable(character) or chain:IsActive(character)) then local item = chain:GetNextItem(character) if item and not item:IsActive(character) then local source = item:GetSource(character) if source ~= nil then local _, coords = source:GetLocation(mapID) if coords ~= nil then local x, y = coords:GetXY() if x >= 0 and x <= 1 and y >= 0 and y <= 1 then -- Within the map table.insert(result, { chainID = chain:GetID(), chainName = chain:GetName(), itemName = item:GetName(character), x = x, y = y, }) end end end end end end end return result end -- Achievement function BtWQuests_GetAchievementName(achievementID) return select(2, GetAchievementInfo(achievementID)) end -- Achievement info isnt always loaded so sometimes need to delay reading achievement names function BtWQuests_GetAchievementNameDelayed(achievementID) return function () return select(2, GetAchievementInfo(achievementID)) end end -- The first return value for GetAchievementCriteriaInfo is the name anyway so no need for the overhead of using another function BtWQuests_GetAchievementCriteriaName = GetAchievementCriteriaInfo -- Achievement info isnt always loaded so sometimes need to delay reading achievement names function BtWQuests_GetAchievementCriteriaNameDelayed(achievementID, criteriaIndex) return function () return BtWQuests_GetAchievementCriteriaName(achievementID, criteriaIndex) end end function BtWQuests_GetAchievementCriteriaFullName(achievementID, criteriaIndex) return string.format("%s: %s", BtWQuests_GetAchievementName(achievementID), BtWQuests_GetAchievementCriteriaName(criteriaIndex)) end -- Achievement info isnt always loaded so sometimes need to delay reading achievement names function BtWQuests_GetAchievementCriteriaFullNameDelayed(achievementID, criteriaIndex) return function () return BtWQuests_GetAchievementCriteriaFullName(achievementID, criteriaIndex) end end function BtWQuests.GetAreaName(areaID) return C_Map.GetAreaInfo(areaID) end function BtWQuests.GetMapName(mapID) return ((C_Map.GetMapInfo(mapID) or {}).name or "Unnamed") end BtWQuests_GetAreaName = BtWQuests.GetAreaName; BtWQuests_GetMapName = BtWQuests.GetMapName; Database.ItemMixin = ItemMixin Database:Init(); Database:RegisterDataType("condition", ConditionMixin); Database:RegisterDataType("expansion", ExpansionMixin); Database:RegisterDataType("category", CategoryMixin); Database:RegisterDataType("chain", ChainMixin); Database:RegisterDataType("quest", QuestMixin); Database:RegisterDataType("npc", NPCMixin); Database:RegisterDataType("object", ObjectMixin); Database:RegisterDataType("mission", MissionMixin); Database:RegisterItemType("header", HeaderItemMixin); Database:RegisterItemType("quest", QuestItemMixin); Database:RegisterItemType("expansion", ExpansionItemMixin); Database:RegisterItemType("category", CategoryItemMixin); Database:RegisterItemType("chain", ChainItemMixin); Database:RegisterItemType("mission", MissionItemMixin); Database:RegisterItemType("npc", NPCItemMixin); Database:RegisterItemType("kill", KillItemMixin); Database:RegisterItemType("talk", TalkItemMixin); Database:RegisterItemType("object", ObjectItemMixin); Database:RegisterItemType("loot", LootItemMixin); Database:RegisterItemType("level", LevelItemMixin); Database:RegisterItemType("experience", ExperienceItemMixin); Database:RegisterItemType("race", RaceItemMixin); Database:RegisterItemType("class", ClassItemMixin); Database:RegisterItemType("faction", FactionItemMixin); Database:RegisterItemType("reputation", ReputationItemMixin); Database:RegisterItemType("friendship", FriendshipItemMixin); Database:RegisterItemType("achievement", AchievementItemMixin); Database:RegisterItemType("money", MoneyItemMixin); Database:RegisterItemType("currency", CurrencyItemMixin); Database:RegisterItemType("time", TimeItemMixin); Database:RegisterItemType("timezone", TimeZoneItemMixin); Database:RegisterItemType("coords", CoordsItemMixin); Database:RegisterItemType("pet", PetItemMixin); Database:RegisterItemType("mount", MountItemMixin); Database:RegisterItemType("toy", ToyItemMixin); Database:RegisterItemType("aura", AuraItemMixin); Database:RegisterItemType("profession", ProfessionItemMixin); Database:RegisterItemType("item", ItemItemMixin); Database:RegisterItemType("equipped", EquippedItemMixin); Database:RegisterItemType("questline", QuestLineItemMixin); Database:RegisterItemType("follower", FollowerItemMixin); Database:RegisterItemType("garrisontalenttree", GarrisonTalentTreeItemMixin); Database:RegisterItemType("garrisontalent", GarrisonTalentItemMixin); Database:RegisterItemType("campaign", CampaignItemMixin); Database:RegisterItemType("spell", ItemMixin); -- Is just used to track with rewards spells are used Database:RegisterItemType("area", AreaItemMixin); Database:RegisterItemType("chromietime", ChromieTimeItemMixin); Database:AddCondition(-1, { type = "chromietime" }); Database:AddCondition(923, { type = "faction", id = "Horde" }); Database:AddCondition(924, { type = "faction", id = "Alliance" }); Database:RegisterConditionClearCacheEvent("QUEST_ACCEPTED") Database:RegisterConditionClearCacheEvent("QUEST_AUTOCOMPLETE") Database:RegisterConditionClearCacheEvent("QUEST_COMPLETE") Database:RegisterConditionClearCacheEvent("QUEST_FINISHED") Database:RegisterConditionClearCacheEvent("QUEST_TURNED_IN") Database:RegisterConditionClearCacheEvent("QUEST_WATCH_LIST_CHANGED") Database:RegisterConditionClearCacheEvent("QUEST_WATCH_UPDATE") if INTERFACE_NUMBER >= 70200 then Database:RegisterConditionClearCacheEvent("QUEST_LOG_CRITERIA_UPDATE") end if C_QuestSession then Database:RegisterConditionClearCacheEvent("QUEST_SESSION_JOINED") Database:RegisterConditionClearCacheEvent("QUEST_SESSION_LEFT") end BtWQuestsDatabase = Database; BtWQuests.Database = Database;