-- lua functions local select, setmetatable, error, type, rawget, rawset, pairs, tonumber, strsplit, tContains, unpack, tostring, wipe, tinsert, tsort, tconcat, tremove = select, setmetatable, error, type, rawget, rawset, pairs, tonumber, strsplit, tContains, unpack, tostring, wipe, table.insert, table.sort, table.concat, table.remove -- WoW API local GetNumSpecializations, GetSpecializationInfo, GetItemInfo, IsEquippedItem, GetContainerNumSlots, GetContainerItemID, UnitClass, GetInventorySlotInfo, GetNumGuildMembers, ConvertRGBtoColorString, GetInventoryItemLink, GetContainerItemInfo, GetAddOnMetadata, GetItemSpecInfo, GetItemUniqueness, IsInGuild, GetGuildInfo, GetSpecialization, GetSpecializationInfoByID, IsInRaid, UnitName, IsInGroup, GetGuildRosterInfo, UnitFullName, UnitRace, UnitSex = GetNumSpecializations, GetSpecializationInfo, GetItemInfo, IsEquippedItem, GetContainerNumSlots, GetContainerItemID, UnitClass, GetInventorySlotInfo, GetNumGuildMembers, ConvertRGBtoColorString, GetInventoryItemLink, GetContainerItemInfo, GetAddOnMetadata, GetItemSpecInfo, GetItemUniqueness, IsInGuild, GetGuildInfo, GetSpecialization, GetSpecializationInfoByID, IsInRaid, UnitName, IsInGroup, GetGuildRosterInfo, UnitFullName, UnitRace, UnitSex local E = select(2, ...) local BestInSlot = LibStub("AceAddon-3.0"):NewAddon("BestInSlotRedux", "AceComm-3.0", "AceHook-3.0", "AceSerializer-3.0", "AceTimer-3.0") local AceGUI = LibStub("AceGUI-3.0") local L = LibStub("AceLocale-3.0"):GetLocale("BestInSlotRedux") local AceEvent = LibStub("AceEvent-3.0") E[1] = BestInSlot E[2] = L E[3] = AceGUI BestInSlot.unsafeIDs = {} BestInSlot.options = {} BestInSlot.defaultModuleState = false BestInSlot.options.DEBUG = false -- Authors BestInSlot.Author1 = ("%s%s @ %s"):format("|c"..RAID_CLASS_COLORS.DEMONHUNTER.colorStr, "Beleria".."|r",ConvertRGBtoColorString(PLAYER_FACTION_COLORS[1]).."Argent Dawn-EU|r") BestInSlot.Author2 = ("%s%s @ %s"):format("|c"..RAID_CLASS_COLORS.PALADIN.colorStr, "Anhility".."|r",ConvertRGBtoColorString(PLAYER_FACTION_COLORS[1]).."Ravencrest-EU|r") BestInSlot.Author3 = ("%s%s @ %s"):format("|c"..RAID_CLASS_COLORS.ROGUE.colorStr, "Sar\195\173th".."|r",ConvertRGBtoColorString(PLAYER_FACTION_COLORS[1]).."Tarren Mill-EU|r") BestInSlot.Author0 = ("%s%s @ %s"):format("|c"..RAID_CLASS_COLORS.ROGUE.colorStr, "Swarley".."|r",ConvertRGBtoColorString(PLAYER_FACTION_COLORS[1]).."Burning Legion-NA|r") BestInSlot.Author4 = ("%s%s @ %s"):format("|c"..RAID_CLASS_COLORS.DRUID.colorStr, "Dioxina".."|r",ConvertRGBtoColorString(PLAYER_FACTION_COLORS[1]).."Antonidas-EU|r") --@non-debug@ BestInSlot.version = 20200604165054 --@end-non-debug@ BestInSlot.AlphaVersion = not (GetAddOnMetadata("BestInSlotRedux", "Version"):find("Release") and true or false) local slashCommands = {} local defaults = { char = { ['*'] = { --raidTier ['*'] = { --raidDifficulty ['*'] = { --listType (spec as number, customList as string) ['*'] = nil } }, }, latestVersion = 1, selected = {}, windowpos = {}, customlists = {}, tutorials = {}, options = { windowFixed = false, showBiSTooltip = true, sendAutomaticUpdates = true, receiveAutomaticUpdates = true, minimapButton = true, guildtooltip = true, showBossTooltip = true, keepHistory = false, tooltipCombat = false, historyLength = "30d", historyAutoDelete = true, tooltipSource = true, statsInManager = true, showGuildRankInTooltip = { ['*'] = true }, overviewfilter = {}, }, }, global = { options = { instantAnimation = false, }, customitems = { }, tutorials = true, }, factionrealm = { _history = { ['*'] = {}, --players database }, ['*'] = { --guildname ['*'] = { -- charactername ['*'] = { -- raidTier ['*'] = { -- difficulty } } } } }, profile = { minimap = { hide = false, } }, } --- --Datatypes to be used with some of BestInSlots functions --- BestInSlot.EXPANSION = 1 BestInSlot.RAIDTIER = 2 BestInSlot.INSTANCE = 3 BestInSlot.BOSS = 4 BestInSlot.DIFFICULTY = 5 BestInSlot.SPECIALIZATION = 6 BestInSlot.MSGPREFIX = "BiS" --- --Color codes used by the add-on --- BestInSlot.colorHighlight = RED_FONT_COLOR_CODE BestInSlot.colorNormal = NORMAL_FONT_COLOR_CODE --- --data = { -- raidTiers = { -- [raidTierId] = { -- description = "Raid Tier Description", -- difficulties = {"difficultyName1", "difficultyName2"}, -- expansion = expansionId, -- instances = {}, -- tierTokens = {}, -- tierItems = { -- [Class1] = { -- [difficultyName1] = { -- tierItemId1, -- tierItemId2, -- tierItemId3, -- }, -- [difficultyName2] = { -- tierItemId1, -- tierItemId2, -- tierItemId3, -- }, -- } -- } -- } -- }, -- instances = { -- [instanceId] = { -- raidTier = raidTierID, -- expansion = expansionId, -- description = "Description -- } -- }, -- expansions = { -- [expansionId] = { -- raidTiers = {}, -- instances = {}, -- description = "Description", -- } -- } --} --- local data = { raidTiers = {}, instances = {__default={ difficultyconversion = { [1] = 1, --Dungeon Normal [2] = 2, --Dungeon Heroic [3] = 23, --Dungeon Mythic }, bonusids = { [1] = {3524}, [2] = {3524}, [3] = {3524} }, }}, expansions = {}, bosses = {}, tiertokens = {}, } --- --itemData = { -- [instanceName]={ -- [bossId] = { -- [itemid] = { -- dungeon = "dungeon", -- link = "link", -- isBiS = { -- [difficulty] = { -- [specId] = true -- } -- } -- [difficulty] = .. -- equipSlot = "INVTYPE_[SLOT]" -- } -- } -- } --} -- --itemData's metatable can accept itemids. When it is requested an itemid it'll look in nested tables to find the item in question --- local itemDataCache = {} local itemData = setmetatable({},{ __index = function(tbl, key) local value = itemDataCache[key] if value then return value end for dungeon,dungeonData in pairs(tbl) do --we can do this without error checking because the __newindex metamethod will check if it's a table for bossId, bossData in pairs(dungeonData) do if bossData[key] then itemDataCache[key] = bossData[key] return itemDataCache[key] end end end end, __newindex = function(table, key, value) if type(value) ~= "table" then error("Can only add tables to the itemData table") end rawset(table, key, value) end }) local tierTokenData = {} local customItems = {} BestInSlot.slots = {"HeadSlot", "NeckSlot","ShoulderSlot","BackSlot","ChestSlot","WristSlot","HandsSlot","WaistSlot","LegsSlot","FeetSlot", "Finger0Slot","Finger1Slot","Trinket0Slot","Trinket1Slot", "MainHandSlot","SecondaryHandSlot"} BestInSlot.invSlots = { [1] = "INVTYPE_HEAD", [2] = "INVTYPE_NECK", [3] = "INVTYPE_SHOULDER", [4] = "INVTYPE_BODY", [5] = {"INVTYPE_CHEST", "INVTYPE_ROBE"}, [6] = "INVTYPE_WAIST", [7] = "INVTYPE_LEGS", [8] = "INVTYPE_FEET", [9] = "INVTYPE_WRIST", [10] = "INVTYPE_HAND", [11] = "INVTYPE_FINGER", [12] = "INVTYPE_FINGER", [13] = "INVTYPE_TRINKET", [14] = "INVTYPE_TRINKET", [15] = "INVTYPE_CLOAK", [16] = {"INVTYPE_WEAPON", "INVTYPE_2HWEAPON", "INVTYPE_WEAPONMAINHAND", "INVTYPE_RANGED", "INVTYPE_RANGEDRIGHT", "INVTYPE_NON_EQUIP"}, [17] = {"INVTYPE_WEAPONOFFHAND", "INVTYPE_SHIELD", "INVTYPE_WEAPON", "INVTYPE_HOLDABLE", "INVTYPE_NON_EQUIP"}, --[18] = {"INVTYPE_RANGED", "INVTYPE_THROWN", "INVTYPE_RANGEDRIGHT", "INVTYPE_RELIC"} } BestInSlot.dualWield = {250, 251, 252, 268, 269, 259, 260, 261, 263, 71, 72} ------------------------------------------------------------------------------------------------------------------------------------------------ -- MODULE REGISTRATION ------------------------------------------------------------------------------------------------------------------------------------------------ --- This function can be used by modules to add their data to the add-on. It checks if the proper values are set --@param #string unlocalizedName the Localized Name of the expansion to register --@param #string localizedDescription The localized description of the expansion to add function BestInSlot:RegisterExpansion(unlocalizedName, localizedDescription) if data.expansions[unlocalizedName] then return end data.expansions[unlocalizedName] = {description = localizedDescription, raidTiers = {}, instances = {}} end --- Registers a raid tier to BestInSlot -- @param #string expansion The expansion ID, must have been registered before by using BestInSlot:RegisterExpansion -- @param #number raidTier The number corresponding with the Raid Tier, must be unique. The standard is to use the patch version the raid tier belongs to (e.g. 50400 for patch 5.4) -- @param #string description The description of the raid tier. By default 'Patch 5.4' -- @param #... The next parameters are considered the difficulties you would like to add, can be "Normal", "Heroic", and "Mythic". function BestInSlot:RegisterRaidTier(expansion, raidTier, description, ...) local difficulties = {...} if data.raidTiers[raidTier] then error("This raid tier is already registered!") end if not data.expansions[expansion] then error("The expansion has not been registered yet!") end if not description or type(description) ~= "string" then error("The raid tier needs to provide a description!") end if not difficulties or #difficulties == 0 then error("The raid tier "..description.." needs to provide difficulties!") end for i = 1,#difficulties do if type(difficulties[i]) ~= "string" then error("Difficulty parameter not set correctly") end end data.raidTiers[raidTier] = {description = description, difficulties = difficulties} data.raidTiers[raidTier].expansion = expansion data.raidTiers[raidTier].instances = {} data.raidTiers[raidTier].module = (raidTier >= 69000) and "PvP" or (raidTier < 60000) and "WoDDungeon" or "WoD" tinsert(data.expansions[expansion].raidTiers, raidTier) end local newInstanceMetatable = { --[[__index = function(tbl, key) local value = rawget(tbl, key) if value then return value end for k,v in pairs(tbl) do if v[key] then return v[key] end end end,]] __newindex = function(tbl, key, value) if type(value) ~= "table" then error("Can only add tables with item info inside instance tables") end if key == "tieritems" or key == "misc" or key == "customitems" then rawset(tbl, key, value) return end key = tonumber(key) if not key then error("Key must be a number!") end rawset(tbl, key, value) end } local instanceDefaultIndexMetatable = { __index = function(tbl, key) return data.instances.__default[key] end } ---Register a raid instance to BestInSlot --@param #number raidTier The raidTier ID as used at BestInSlot:RegisterRaidTier --@param #string unlocalizedName An unlocalized name of the raid to add, to identify the instance, must be unique! --@param #string description A localized description of the raid instance to add --@param #table args Optional arguments to override default values. See data.instances.__default function BestInSlot:RegisterRaidInstance(raidTier, unlocalizedName, description, args) if not data.raidTiers[raidTier] then error("The raid tier "..raidTier.." has not been registered yet!") end local localizedExpansion = data.raidTiers[raidTier].expansion if not data.expansions[localizedExpansion] then error("The expansion "..localizedExpansion.." has not been registered yet!") end if not data.raidTiers[raidTier] then error("The raid tier "..raidTier.." has not been registered yet") end if data.instances[unlocalizedName] then error("This raid instance has already been registered!") end data.instances[unlocalizedName] = setmetatable({expansion = localizedExpansion, raidTier = raidTier, description = description}, instanceDefaultIndexMetatable) if args then for k,v in pairs(args) do data.instances[unlocalizedName][k] = v end end tinsert(data.expansions[localizedExpansion].instances, unlocalizedName) tinsert(data.raidTiers[raidTier].instances, unlocalizedName) itemData[unlocalizedName] = setmetatable({}, newInstanceMetatable) if self.db.global.customitems[unlocalizedName] then for itemlink in pairs(self.db.global.customitems[unlocalizedName]) do self:RegisterCustomItem(unlocalizedName, nil, itemlink) --The second parameter will be extracted out of the itemlink if not supplied end end end ---Register tier items that drop in the supplied raidTier --@param #number raidTier The raid tier that drops the tier items --@param #table tierItems A table in the following format: {DEATHKNIGHT = { NORMAL = { normalItem1, normalItem2, ...}, HEROIC = { heroicItem1, heroicItem2},} SHAMAN = {....}} Where the difficulties must correspond with the earlier registered difficulties function BestInSlot:RegisterTierItems(dungeon, tierItems) if not data.instances[dungeon] then error("This dungeon is not registered yet") end local tieritems = {} for class in pairs(RAID_CLASS_COLORS) do if not tierItems[class] then error("You are missing class "..class.." in the tier item list.") end for i=1,#tierItems[class] do local itemid = tierItems[class][i] local _, link, _, _, _, _, _, _, equipSlot = GetItemInfo(i) if not link then self.unsafeIDs[itemid] = true end tieritems[itemid] = { bossid = equipSlot and data.tiertokens[dungeon][self:GetItemSlotID(equipSlot)].bossid, dungeon = dungeon, difficulty = -1, link = link, equipSlot = equipSlot, isBiS = {}, tieritem = class, } end end itemData[dungeon].tieritems = tieritems end ---Register tier tokens, not supported for MoP or lower function BestInSlot:RegisterTierTokens(raidTier, tierTokens) if not data.raidTiers[raidTier] then error("This raid tier is not registered yet") end local raidTierData = data.raidTiers[raidTier] local difficulties = raidTierData.difficulties for slotId, tierSlots in pairs(tierTokens) do for tokenId, tokenClasses in pairs(tierSlots) do tierTokenData[tokenId] = {classes = tokenClasses, raidtier = raidTier, slotid = slotId} end end end ---Adds the named difficulty to the available difficulty --@param #number raidtier The Raidtier to append the difficulty to --@param #string difficulty The name of the difficulty function BestInSlot:AddDifficultyToRaidTier(raidtier, difficulty) if not data.raidTiers[raidtier] then error("Raidtier '"..tostring(raidtier).."' does not exist!") end tinsert(data.raidTiers[raidtier].difficulties, difficulty) end --- Register Miscelaneous items -- @param #number raidTier The Raid tier to add the misc items to -- @param #table miscItems A table containing the miscelaneous items, should be formatted in the following format: {["Legendary Cloak Quest"] = {idCloak1, idCloak2, ...}, ["Ordos"] = {idOrdos1, idOrdos2, ...}} -- @param #bool legionLegendary function BestInSlot:RegisterMiscItems(instance, miscItems, legionLegendary) if not data.instances[instance] then error("This instance is not registered yet") end local misc = {} for miscName,miscLootTable in pairs(miscItems) do if type(miscName) ~= "string" or type(miscLootTable) ~= "table" then error("Misc table is not formatted properly, should be {key = {itemId1, itemId2}} Where key is a description of the source}") end for i=1,#miscLootTable do local itemid = miscLootTable[i] local itemtable if type(itemid) == "table" then itemtable = itemid itemid = itemtable.id if not itemid then self.console:AddError("ItemTable didn't provide id", itemid) end end local link, equipSlot if legionLegendary == true then --fix for Legion Legendaries itemlevel _, link, _, _, _, _, _, _, equipSlot = GetItemInfo(("item:%d::::::::::::1:3630"):format(itemid)) else _, link, _, _, _, _, _, _, equipSlot = GetItemInfo(("item:%d::::::::::::1:3524"):format(itemid)) end if not link then self.unsafeIDs[itemid] = true end misc[itemid] = { dungeon = instance, difficulty = miscLootTable.difficulty, link = link, equipSlot = equipSlot, isBiS = {}, misc = miscName, } end end itemData[instance].misc = misc end local bossNewIndexMetatable = { __newindex = function(tbl, key, value) if type(value) ~= "table" then BestInSlot.console:AddError("Item info must be a table!", key, value) end key = tonumber(key) if not key then BestInSlot.console:AddError("Item info must be a table!", key, value) end rawset(tbl, key, value) end } --- Register boss loot of an instance. Must call this function in the order you want to put the bosses in -- @param #string unlocalizedInstanceName The unlocalized name of the instance to add the loot to. -- @param #table lootTable The table containing the loot for the boss, must be formatted as follows: {["Normal"] = {itemId1, itemId2}, ["Heroic"] = {itemId1, itemId2}} -- @param #string bossName Localized name of the boss, you can use LibBabbleBoss-3.0 for this. -- @param #number tierToken If supplied, registers this item as a boss that drops the supplied tiertoken. 1 = HeadSlot, 3 = ShoulderSlot, 5 = ChestSlot, 7 = LegsSlot, 10 = Handslot, 15 = BackSlot. function BestInSlot:RegisterBossLoot(unlocalizedInstanceName, lootTable, bossName, tierToken, bossId) local instance = data.instances[unlocalizedInstanceName] if not instance then error("The instance \""..unlocalizedInstanceName.."\" has not yet been registered!") end lootTable.info = {name = bossName} local bossLootTable = bossId and itemData[unlocalizedInstanceName][bossId] or setmetatable({}, bossNewIndexMetatable) local addToBoss = bossId ~= nil local bossId = bossId or #itemData[unlocalizedInstanceName] + 1 for i=1,#lootTable do local itemid = lootTable[i] local itemtable if type(itemid) == "table" then itemtable = itemid itemid = itemtable.id if not itemid then self.console:AddError("ItemTable didn't provide id", itemid) end end local item = itemData[itemid] if item and not item.customitem then --The item already existed if not item.multiplesources then item.multiplesources = {} if item.bossid and item.dungeon then item.multiplesources[item.dungeon] = {} item.multiplesources[item.dungeon][item.bossid] = true else self:Print(item) self:Print(self.unsafeIDs) end end item.multiplesources[unlocalizedInstanceName] = item.multiplesources[unlocalizedInstanceName] or {} item.multiplesources[unlocalizedInstanceName][bossId] = true bossLootTable[itemid] = item else if item and item.customitem then self:Print("You have added a custom item that is being registered as a module. This is being removed from your custom items.", true) self:Print("Removing: "..item.link, true) self:UnregisterCustomItem(itemid) end local _, link, _, _, _, _, _, _, equipSlot = GetItemInfo(itemid) if not link then self.unsafeIDs[itemid] = true end bossLootTable[itemid] = { dungeon = unlocalizedInstanceName, bossid = bossId, difficulty = itemtable and itemtable.difficulty or -1, link = link, equipSlot = equipSlot, exceptions = itemtable and itemtable.exceptions, } end end data.bosses[unlocalizedInstanceName] = data.bosses[unlocalizedInstanceName] or {} if not addToBoss then tinsert(itemData[unlocalizedInstanceName], bossLootTable) --add loot to itemData tinsert(data.bosses[unlocalizedInstanceName], bossName) --add Boss info to data end if tierToken then data.tiertokens[unlocalizedInstanceName] = data.tiertokens[unlocalizedInstanceName] or {} data.tiertokens[unlocalizedInstanceName][tierToken] = {dungeon = unlocalizedInstanceName, bossid = #data.bosses[unlocalizedInstanceName]} end BestInSlot.hasModules = true end --Simple helper to check if an array has any key local function hasItems(array) for _ in pairs(array) do return true end return false end --- Called on initializing the add-on function BestInSlot:OnInitialize() self.db = LibStub("AceDB-3.0"):New("BestInSlotDB", defaults) SLASH_BESTINSLOT1, SLASH_BESTINSLOT2 = '/bestinslot', '/bis' self:RegisterComm(self.MSGPREFIX) self.options.instantAnimation = self.db.global.options.instantAnimation self.options.showBiSTooltip = self.db.char.options.showBiSTooltip self.options.windowFixed = self.db.char.options.windowFixed self.options.sendAutomaticUpdates = self.db.char.options.sendAutomaticUpdates self.options.receiveAutomaticUpdates = self.db.char.options.receiveAutomaticUpdates AceEvent:RegisterEvent("GET_ITEM_INFO_RECEIVED", function(event, itemid) BestInSlot:SendEvent("GET_ITEM_INFO_RECEIVED", itemid) end) self:RegisterEvent("GET_ITEM_INFO_RECEIVED", "OnItemInfoGenerated") self:Print((L["has been initialized, use %s to show the GUI"]):format((L["%s or %s"]):format(self.colorHighlight.."/bis"..self.colorNormal, self.colorHighlight.."/bestinslot"..self.colorNormal))) end --- Called on enabling the add-on function BestInSlot:OnEnable() self:HookScript(GameTooltip, "OnTooltipSetItem", "GameTooltip_OnTooltipSetItem") if AtlasLootTooltip then self:HookScript(AtlasLootTooltip, "OnTooltipSetItem", "GameTooltip_OnTooltipSetItem") end self:HookScript(ItemRefTooltip, "OnTooltipSetItem", "GameTooltip_OnTooltipSetItem") AceEvent:RegisterEvent("PLAYER_GUILD_UPDATE", function() BestInSlot:SendEvent("PLAYER_GUILD_UPDATE") end) self:MiniMapButtonVisible(self.db.char.options.minimapButton) self:SetBestInSlotInfo() local coreModules = { "ZoneDetect", "History", "History", "Timer" } for i=1, #coreModules do self:EnableModule(coreModules[i]) end --Enable BiS Core modules local loadOrder = {} local waitList = {} local ZoneDetect = self:GetModule("ZoneDetect") for k,v in pairs(BestInSlot.modules) do if not v.enabledState then if v.dependancy then waitList[v.dependancy] = waitList[v.dependancy] or {} tinsert(waitList[v.dependancy], v) else tinsert(loadOrder, v) end end end while #loadOrder ~= 0 do local module = tremove(loadOrder) local moduleName = module.moduleName module:Enable() module:InitializeZoneDetect(ZoneDetect) if waitList[moduleName] then local addToList = waitList[moduleName] for i=1,#addToList do tinsert(loadOrder, addToList[i]) end end end end function BestInSlot:OnDisable() self:Unhook(GameTooltip, "OnTooltipSetItem") self:Unhook(ItemRefTooltip, "OnTooltipSetItem") end function BestInSlot:GetDifficultyIdForDungeon(bisId, dungeon, toBiS) local returnId, bonusIds = nil if not toBiS then if dungeon and data.instances[dungeon] then returnId, bonusIds = data.instances[dungeon].difficultyconversion[bisId], data.instances[dungeon].bonusids[bisId] else returnId, bonusIds = data.instances.__default.difficultyconversion[bisId], data.instances.__default.bonusids[bisId] end else local tbl = data.instances[dungeon] or data.instances.__default for BiSId, WoWId in pairs(tbl.difficultyconversion) do if bisId == WoWId then returnId, bonusIds = BiSId, tbl.bonusids[BiSId] end end end if not returnId then return end if type(bonusIds) == "table" then return returnId, unpack(bonusIds) else return returnId, bonusIds end end --- Checks wether the player has the supplied item equipped, it should consider normal and warforged items the same -- @param #number itemid The item ID to check if it's equipped -- @return #boolean True if item equipped, otherwise false function BestInSlot:HasItemEquipped(itemid, difficulty) local item = self:GetItem(itemid, difficulty) if not item then return end for i=1,#self.slots do local slotID = GetInventorySlotInfo(self.slots[i]) local link = GetInventoryItemLink("player", slotID) if link then local id, instanceDifficulty = self:GetItemInfoFromLink(link) instanceDifficulty = tonumber(instanceDifficulty) if id == itemid then if difficulty == nil then local bisId = self:GetDifficultyIdForDungeon(instanceDifficulty, item.dungeon, true) if bisId then return {bisId} end elseif self:GetDifficultyIdForDungeon(difficulty, item.dungeon) == instanceDifficulty then return true end end end end if difficulty then return {} end end function BestInSlot:GetItemInfoFromLink(itemlink) local _,itemid, enchantId, gemId1, gemId2, gemId3, gemId4, suffixId, uniqueId, linkLevel, specId, upgradeId, instanceDifficultyID, numBonusId, bonusId1, bonusId2, bonusId3, bonusId4, bonusId5, upgradeVal = (":"):split(itemlink) return tonumber(itemid), tonumber(instanceDifficultyID), bonusId1, bonusId2 end --- Checks if the player has an item in their bags, or an item similar to it (warforged version for example) -- @param #number itemid The itemID to check if the player has it in their bags -- @return #boolean true if the player has the item in their bag, otherwise false function BestInSlot:HasItemInBag(itemid, difficulty) local item = self:GetItem(itemid) if not item then return false end local bags = NUM_BAG_SLOTS local difficulties local name = GetItemInfo(itemid) for i=0,bags do local bagSize = GetContainerNumSlots(i) for j=1,bagSize do local texture, count, locked, quality, readable, lootable, link, isFiltered = GetContainerItemInfo(i, j) if link then local id, instanceDifficulty = self:GetItemInfoFromLink(link) instanceDifficulty = tonumber(instanceDifficulty) if id == itemid then if difficulty == nil then difficulties = difficulties or {} local bisId = self:GetDifficultyIdForDungeon(instanceDifficulty, item.dungeon, true) if bisId then tinsert(difficulties, bisId) end elseif self:GetDifficultyIdForDungeon(difficulty, item.dungeon) == instanceDifficulty then return true end end end end end return difficulties end function BestInSlot:OnItemInfoGenerated(event, itemid) local item = itemData[itemid] if item then local _, link, _, _, _, _, _, _, equipSlot = GetItemInfo(item.customitem or itemid) if not link then return end item.link = link item.equipSlot = equipSlot if item.tieritem then item.bossid = equipSlot and data.tiertokens[item.dungeon][self:GetItemSlotID(equipSlot)].bossid end if self.unsafeIDs[itemid] then self.unsafeIDs[itemid] = nil end end end --- Checks if the player has an item either in their bags, or equipped -- @param #number itemid The itemID to check if the player has -- @param #number [difficulty] The DifficultyId that the item must have. -- @param #bool [checkHigherDifficulties] When true will check higher difficulties and return the difficulty number when found, or nil when not found. -- @return #boolean true if the player has it, otherwise false function BestInSlot:HasItem(itemid, difficulty, checkHigherDifficulties) if not difficulty or not checkHigherDifficulties then return self:HasItemEquipped(itemid, difficulty) or self:HasItemInBag(itemid, difficulty) else local equippedResult = self:HasItemEquipped(itemid) local result = self:HasItemInBag(itemid) or {} if equippedResult then if not tContains(result, equippedResult[1]) then tinsert(result, equippedResult[1]) end end if #result > 0 then tsort(result) if result[#result] >= difficulty then return result[#result] end end end end ------------------------------------------------------------------------------------------------------------------------------------------------ -- Slash Commands ------------------------------------------------------------------------------------------------------------------------------------------------ --- A BestInSlot function to register a custom slash command. This will automatically be displayed at /help and should be able to be called through /bis [cmd] -- @param #string cmd The command to register -- @param #string descr The description to be displayed when '/bis help' is being typed -- @param #function func The function to be called when this slash command is invoked -- @param #number prefOrder The preferred location of this message in the '/bis help' dialog. Can be nil function BestInSlot:RegisterSlashCmd(cmd, descr, func, prefOrder) if type(cmd) ~= "string" then error("Command should be a string") end if type(func) ~= "function" then error("Second argument of RegisterSlashCmd should be the function that should be called when the command is given") end if type(descr) ~= "string" then error("Slashcommand should provide a description as third parameter") end if prefOrder and type(prefOrder) ~= "number" then error("If provided, prefOrder should be a number") end cmd = (cmd):lower() if slashCommands[cmd] then error("Slash command "..cmd.." is already registered!") end slashCommands[cmd] = {func = func, descr = descr, prefOrder = prefOrder} end function SlashCmdList.BESTINSLOT(msg, editbox) local args = {} local first = true local command for w in (msg):gmatch("%w+") do if first then command = w first = false else tinsert(args, w) end end if not command then slashCommands.show.func() else command = (command):lower() if not slashCommands[command] then BestInSlot:Print((L["Command not recognized, try '%s' for help"]):format("/bis help"), true) else slashCommands[command].func(unpack(args)) end end end BestInSlot:RegisterSlashCmd("help", (L["%s - this dialog"]):format("/bis help"), function() local orderedList = {} for k in pairs(slashCommands) do tinsert(orderedList, k) end tsort(orderedList) for i=1,#orderedList do if slashCommands[orderedList[i]].prefOrder then tinsert(orderedList, slashCommands[orderedList[i]].prefOrder, orderedList[i]) tremove(orderedList, i + 1) end end DEFAULT_CHAT_FRAME:AddMessage(BestInSlot.colorHighlight..("-"):rep(5)..BestInSlot.colorNormal.."BestInSlotRedux "..L["commands"]..BestInSlot.colorHighlight..("-"):rep(5).."|r") BestInSlot:Print(("%s: %s (%s)"):format(GAME_VERSION_LABEL, GetAddOnMetadata("BestInSlotRedux", "Version"), BestInSlot.version)) for i=1,#orderedList do BestInSlot:Print(slashCommands[orderedList[i]].descr, true) end DEFAULT_CHAT_FRAME:AddMessage(BestInSlot.colorHighlight..("-"):rep(36).."|r") end) BestInSlot:RegisterSlashCmd("debug", (L["%s - enable/disable debug messages"]):format("/bis debug"), function() if BestInSlot.options.DEBUG then BestInSlot:Print(L["Disabling debug messages"]) BestInSlot.options.DEBUG = false else BestInSlot.options.DEBUG = true BestInSlot:Print(L["Enabling debug messages"]) end BestInSlot:SendEvent("DebugOptionsChanged", BestInSlot.options.DEBUG) end) local itemslotidCache = {} function BestInSlot:GetItemSlotID(equipSlot, spec) if not equipSlot then return end if spec == 72 and equipSlot == "INVTYPE_2HWEAPON" then return 16,17 end --fury warrior 2-handers if itemslotidCache[equipSlot] then if #itemslotidCache[equipSlot] == 1 then return itemslotidCache[equipSlot][1] else return itemslotidCache[equipSlot][1], itemslotidCache[equipSlot][2] end end local result = {} for i=1,#self.invSlots do if type(self.invSlots[i]) == "string" then if self.invSlots[i] == equipSlot then tinsert(result, i) end elseif type(self.invSlots[i]) == "table" then for j=1,#self.invSlots[i] do if self.invSlots[i][j] == equipSlot then tinsert(result, i) end end end end itemslotidCache[equipSlot] = result return unpack(result) end ------------------------------------------------------------------------------------------------------------------------------------------------ -- Getters for data ------------------------------------------------------------------------------------------------------------------------------------------------ --- -- This function returns an array of items by id to define in what order it should show the items provided in the itemArray -- @param #array itemArray The array of items to sort -- @param #string mode The mode to sort at, currently only supports "SORT_MODE_ILVL" and defaults to that --- function BestInSlot:GetLootOrder(itemArray, mode) local sortArray = {} local mode = mode or "SORT_MODE_ILVL" if mode == "SORT_MODE_ILVL" then for k,v in pairs(itemArray) do if #sortArray == 0 then tinsert(sortArray, k) else local position for i=1,#sortArray do if sortArray[i] > k then position = i break end end position = position or #sortArray + 1 tinsert(sortArray, position, k) end end end return sortArray end --- Retrieve the loot table that's personalized for the player. -- @param #number raidTier The raidtier to retrieve the loot table for -- @param #number slotID The slotID to retrieve the loot table for -- @param #number difficulty The difficulty ID to retrieve the loot table for -- @param #number specializationId The specializationID to retrieve the loot for -- @param #boolean lowerRaidTiers Show loot for lower raid tiers as well -- @param #number The specialization to use to compare uniquness for -- @return #table The loot table for the player function BestInSlot:GetPersonalizedLootTableBySlot(raidTier, slotId, difficulty, specializationId, lowerRaidTiers, uniquenessSpec) local specRole, class = select(6, GetSpecializationInfoByID(specializationId)) uniquenessSpec = uniquenessSpec or specializationId if specializationId == 72 and slotId == 17 then --Fury warriors can wield everything in their offhand return self:GetPersonalizedLootTableBySlot(raidTier, 16, difficulty, specializationId, lowerRaidTiers) --return main hand loot list instead end local items = self:GetLootTableBySlot(raidTier, slotId, difficulty, lowerRaidTiers) if not items then return end for id, item in pairs(items) do local canUse local statFilter = GetItemSpecInfo(item.itemid) if item.exceptions then local checks = {specRole, class} for i, check in pairs({"role", "class"}) do local checkitem = item.exceptions[check] if checkitem and type(checkitem) == "table" and tContains(checkitem, checks[i]) or checkitem == checks[i] then exceptions[item.itemid] = true break end end end if statFilter then if #statFilter == 0 then --There is no itemspecinfo available for this item, normally the table should be nil if raidTier > 70000 and (slotId == 2 or slotId == 11 or slotId == 12) and item.misc ~= LOOT_JOURNAL_LEGENDARIES then canUse = true else canUse = item.customitem ~= nil end else canUse = tContains(statFilter, specializationId) end else canUse = false end if canUse and tContains(data.raidTiers[raidTier].instances, item.dungeon) then --check item uniqueness local family, count = GetItemUniqueness(item.itemid) if count == 1 and self:IsItemBestInSlot(item.itemid, difficulty, uniquenessSpec) then canUse = false end end if canUse and slotId == 17 and item.equipSlot == "INVTYPE_WEAPON" then canUse = false for i=1,#self.dualWield do if self.dualWield[i] == specializationId then canUse = true break end end end if not canUse then items[id] = nil end end local addSpec if specializationId == 73 then --Prot warriors addSpec = 71 --Arms elseif specializationId == 104 then --Guardian Druid addSpec = 103 --Feral Druid elseif specializationId == 66 then --Prot Pally addSpec = 70 --Ret Pally elseif specializationId == 250 then --Blood DK addSpec = 252 --Unholy DK elseif specializationId == 268 then --Brewmaster Monk addSpec = 269 --Windwalker Monk elseif specializationId == 105 then --Resto Druid addSpec = 102 --Balance Druid elseif specializationId == 264 then --Resto Shaman addSpec = 262 --Elemental Shaman elseif specializationId == 257 or specializationId == 256 then --Both Healing Priests addSpec = 258 --Shadow Priest end --ToDo Implement fix for paladin if addSpec then local dpsItems = self:GetPersonalizedLootTableBySlot(raidTier, slotId, difficulty, addSpec, lowerRaidTiers, specializationId) for itemid, item in pairs(dpsItems) do if not items[itemid] then items[itemid] = item end end end return items end local function addLootToTableByFilter(tbl, itemlist, slotId, difficulty) for id in pairs(itemlist) do local item = BestInSlot:GetItem(id, difficulty) if (not slotId) or (type(BestInSlot.invSlots[slotId]) == "string" and BestInSlot.invSlots[slotId] == item.equipSlot) or (type(BestInSlot.invSlots[slotId]) == "table" and tContains(BestInSlot.invSlots[slotId],item.equipSlot)) then if (not difficulty) or (not item.difficulty or (item.difficulty == -1 or item.difficulty == difficulty or (type(item.difficulty) == "table") and tContains(item.difficulty, difficulty)) ) then tbl[id] = item end end end end --- Gets the loottable for the supplied dungeon -- @param #string dungeon Unlocalized name of the dungeon -- @param #number slotId Optional slotId to add function BestInSlot:GetLootTableByDungeon(dungeon, slotId, difficulty) local items = {} local dungeonData = itemData[dungeon] for bossId=1,#dungeonData do addLootToTableByFilter(items, dungeonData[bossId], slotId, difficulty) end if dungeonData.tieritems then addLootToTableByFilter(items, dungeonData.tieritems, slotId, difficulty) end if dungeonData.misc then addLootToTableByFilter(items, dungeonData.misc, slotId, difficulty) end if dungeonData.customitems then addLootToTableByFilter(items, dungeonData.customitems, slotId, difficulty) end return items end local function helperFullLootTable(tbl, itemlist, difficulty) for id in pairs(itemlist) do local item = BestInSlot:GetItem(id, difficulty) if (not difficulty) or (not item.difficulty or (item.difficulty == -1 or item.difficulty == difficulty or (type(item.difficulty) == "table") and tContains(item.difficulty, difficulty)) ) then tbl[id] = item end end end function BestInSlot:GetFullLootTableForRaidTier(raidTier, difficulty) local items = {} for _, dungeon in pairs(self:GetInstances(self.RAIDTIER, raidTier)) do local dungeonData = itemData[dungeon] for bossId=1,#dungeonData do helperFullLootTable(items, dungeonData[bossId], difficulty) end if dungeonData.tieritems then helperFullLootTable(items, dungeonData.tieritems, difficulty) end if dungeonData.misc then helperFullLootTable(items, dungeonData.misc, difficulty) end if dungeonData.customitems then helperFullLootTable(items, dungeonData.customitems, difficulty) end end return items end --- Get the loottable for the supplied raidTier, slot, and difficulty -- @param #number raidTier The raidtier to request the loot table off -- @param #number slotId The Slot ID to request -- @param #number difficulty The difficulty ID of the raidTier to request the data off -- @param #boolean lowerRaidTiers Get data for lower raid tiers as well -- @return #table The loot table function BestInSlot:GetLootTableBySlot(raidTier, slotId, difficulty, lowerRaidTiers) local items = {} local dungeons = data.raidTiers[raidTier].instances for i=1,#dungeons do for id, item in pairs(self:GetLootTableByDungeon(dungeons[i], slotId, difficulty)) do items[id] = item end end if lowerRaidTiers then local module = data.raidTiers[raidTier].module local raidTiers = self:GetRaidTiers() for i=1,#raidTiers do if raidTiers[i] == raidTier then break end --stop the loop at the raidTier that we already have data from if module == data.raidTiers[raidTiers[i]].module then for id, item in pairs(self:GetLootTableBySlot(raidTiers[i], slotId, difficulty)) do items[id] = item end end end end return items end function BestInSlot:ItemExists(itemid) if itemData[itemid] ~= nil then return true, "item" elseif tierTokenData[itemid] ~= nil then return true, "tiertoken" else return false end end --- Gets an item string for use in the WoWAPI --@param #number itemid The itemid of the itemstring function BestInSlot:GetItemString(itemid, difficulty) if not itemid then error("You should provide an itemid!") end difficulty = difficulty or 1 local instanceDifficulty, bonusID1, bonusID2, bonusID3, bonusID4, bonusID5 = self:GetDifficultyIdForDungeon(difficulty, itemData[itemid] and itemData[itemid].dungeon) numBonusIDs = (bonusID5 ~= nil and 5) or (bonusID4 ~= nil and 4) or (bonusID3 ~= nil and 3) or (bonusID2 ~= nil and 2) or (bonusID1 ~= nil and 1) or 0 --item:itemId:enchantId:gemId1:gemId2:gemId3:gemId4:suffixId:uniqueId:linkLevel:specializationID:upgradeId:instanceDifficultyId:numBonusIds:bonusId1:bonusId2:upgradeValue return ("item:%d:::::::::::%d:%d:%d:%d:%d:%d:%d:"):format(itemid, instanceDifficulty, numBonusIDs, bonusID1, bonusID2, bonusID3, bonusID4, bonusID5) end --- Gets the internal item table for the specified itemid --@param #number itemid The ID of the item --@param #string difficulty The difficulty of the item --@return #table The internal item table function BestInSlot:GetItem(itemid, difficulty) if type(itemid) == "string" then itemid = tonumber(itemid) end if itemid then if self.unsafeIDs[itemid] then self:OnItemInfoGenerated(nil, itemid) end if self.unsafeIDs[itemid] then self.console:AddError("Couldn't fetch data for itemid: "..itemid) end if itemData[itemid] then local newItemTable = setmetatable({}, {__index=itemData[itemid]}) if difficulty and newItemTable.difficulty == -1 then --This is an item that has multiple states, therefore we need to set it's state newItemTable.itemstr = self:GetItemString(itemid, difficulty) local link = select(2,GetItemInfo(newItemTable.itemstr)) newItemTable.link = link newItemTable.difficulty = difficulty else newItemTable.itemstr = "item:"..itemid end newItemTable.itemid = itemid return newItemTable end end end function BestInSlot:GetItemSources(itemid) if self:ItemExists(itemid) then local item = itemData[itemid] local sources = {} sources[item.dungeon] = {} sources[item.dungeon][item.bossid] = true if item.multiplesources then for k,v in pairs(item.multiplesources) do sources[k] = sources[k] or {} for bossId in pairs(v) do sources[k][bossId] = true end end end return sources end end function BestInSlot:AddCustomItem(itemid, itemstr, dungeon, updatePrevious)--, warlordsCrafted, stage, suffix) if updatePrevious then local item = itemData[itemid] if item then itemData[item.dungeon].customitems[itemid] = nil rawset(itemData, itemid, nil) self.db.global.customitems[item.dungeon][item.customitem] = nil end end self.db.global.customitems[dungeon] = self.db.global.customitems[dungeon] or {} self.db.global.customitems[dungeon][itemstr] = true self:RegisterCustomItem(dungeon, itemid, itemstr) end local function helperGetCustomItems(dungeon) local result = {} local count = 0 if itemData[dungeon] and itemData[dungeon].customitems then for itemid, item in pairs(itemData[dungeon].customitems) do tinsert(result, itemid) count = count + 1 end end return result, count end function BestInSlot:GetCustomItems(dungeon) if not dungeon then local result = {} local totalcount = 0 for _, instance in pairs(self:GetInstances()) do local count result[instance], count = helperGetCustomItems(instance) totalcount = totalcount + count end return result, totalcount end return helperGetCustomItems(dungeon) end function BestInSlot:RegisterCustomItem(dungeon, itemid, itemlink) if not itemData[dungeon] then error("Invalid dungeon given to RegisterCustomItem!") end if not itemid then itemid = self:GetItemInfoFromLink(itemlink) itemid = tonumber(itemid) if not itemid then error("Couldn't convert itemlink to itemid!") end end itemData[dungeon].customitems = itemData[dungeon].customitems or {} local _, link, _, _, _, _, _, _, equipSlot = GetItemInfo(itemlink) if not link then self.unsafeIDs[itemid] = true end itemData[dungeon].customitems[itemid] = { dungeon = dungeon, isBiS = { }, link = link, equipSlot = equipSlot, customitem = itemlink, } end function BestInSlot:UnregisterCustomItem(itemid) local item = itemData[itemid] self:Print(item) if not item or not item.customitem then return end local dungeon = item.dungeon local raidtier = self:GetRaidTiers(self.INSTANCE, dungeon) for difficulty, difficTable in pairs(item.isBiS) do for specId, bis in pairs(difficTable) do if bis then self:SetItemBestInSlot(raidtier, difficulty, specId, self:GetItemSlotID(item.equipSlot), nil) end end end self.db.global.customitems[dungeon][item.customitem] = nil itemData[dungeon].customitems[itemid] = nil rawset(itemData, itemid, nil) --rawSet overrides default prevention of removing item info. end --- Gets a localized description for the supplied unlocalized identifier -- @param #number datatype Optional datatype to query can be BestInSlot.EXPANSION, BestInSlot_TYPE_RAIDTIER, BestInSlot.INSTANCE, BestInSlot.BOSS or BestInSlot.DIFFICULTY -- @param #multiple arg1 The filter for the supplied datatype -- @param #multiple arg2 The second filter for the supplied datatype, needed for TYPE_BOSS and TYPE_DIFFUCLTY function BestInSlot:GetDescription(datatype, arg1, arg2) if not arg1 or not datatype then error("This function requires atleast 2 arguments, a datatype and an argument for that datatype") end if datatype == self.RAIDTIER then arg1 = tonumber(arg1) if not arg1 or not data.raidTiers[arg1] then return "" end return data.raidTiers[arg1].description elseif datatype == self.EXPANSION then if not data.expansions[arg1] then return "" end return data.expansions[arg1].description elseif datatype == self.INSTANCE then if not data.instances[arg1] then return "" end return data.instances[arg1].description elseif datatype == self.BOSS then arg2 = tonumber(arg2) if not arg2 then error("The GetDescription function for datatype TYPE_BOSS requires 2 arguments") end if not data.bosses[arg1] then return "" end return data.bosses[arg1][arg2] elseif datatype == self.DIFFICULTY then arg2 = tonumber(arg2) if not arg2 then error("The GetDescription function for datatype TYPE_DIFFICULTY requires 2 argumets") end if type(arg1) == "number" then --assume it's a raid tier return data.raidTiers[arg1].difficulties[arg2] elseif type(arg1) == "string" then --assume it's a dungeon return data.raidTiers[data.instances[arg1].raidTier].difficulties[arg2] else error(tostring(arg1).." is not a string or number.") end end error(tostring(datatype).." is an invalid datatype!") end --- Gets the expansions -- @param #number datatype Optional datatype to query, if nil it will give all expansions as result can be BestInSlot.EXPANSION, BestInSlot_TYPE_RAIDTIER, BestInSlot.INSTANCE, BestInSlot.BOSS or BestInSlot.DIFFICULTY -- @param #multiple arg The filter for the supplied datatype function BestInSlot:GetExpansions(datatype, arg) if not datatype then local expansions = {} for k,v in pairs(data.expansions) do tinsert(expansions, k) end return expansions elseif datatype == self.RAIDTIER then if data.raidTiers[arg] then return data.raidTiers[arg].expansion end return "" elseif datatype == self.INSTANCE then if data.instances[arg] then return data.instances[arg].expansion end return "" else error("Invalid type given!") end end --- Gets the raidtiers -- @param #number datatype Optional datatype to query, if nil it will give everything as result can be BestInSlot.EXPANSION, BestInSlot_TYPE_RAIDTIER, BestInSlot.INSTANCE -- @param #multiple arg The filter for the supplied datatype function BestInSlot:GetRaidTiers(datatype, arg) local raidTiers = {} -- No Filter if not datatype or datatype == self.RAIDTIER then for k,v in pairs(data.raidTiers) do if not datatype or (v.module == data.raidTiers[arg].module and k < arg) then tinsert(raidTiers, k) end end tsort(raidTiers) return raidTiers elseif not arg then return -- Expansion filter elseif datatype == self.EXPANSION then if not data.expansions[arg] then return raidTiers end for i=1,#data.expansions[arg].raidTiers do tinsert(raidTiers, data.expansions[arg].raidTiers[i]) end tsort(raidTiers) return raidTiers -- Instance filter, will return a single raidTier, not as table! elseif datatype == self.INSTANCE then if data.instances[arg] then return data.instances[arg].raidTier else return end end end --- Gets the difficulties -- @param #number datatype Optional datatype to query, if nil it will give everything as result can be BestInSlot.EXPANSION, BestInSlot_TYPE_RAIDTIER, BestInSlot.INSTANCE -- @param #multiple The filter for the supplied datatype function BestInSlot:GetDifficulties(datatype, arg) local difficulties = {} if datatype == self.RAIDTIER then if not data.raidTiers[arg] then return difficulties end for i=1,#data.raidTiers[arg].difficulties do tinsert(difficulties, data.raidTiers[arg].difficulties[i]) end return difficulties elseif datatype == self.INSTANCE then if not arg or not data.instances[arg] then return difficulties end local raidTier = data.instances[arg].raidTier for i=1,#data.raidTiers[raidTier].difficulties do tinsert(difficulties, data.raidTiers[raidTier].difficulties[i]) end return difficulties end error("Invalid datatype given!") end --- Gets the instances -- @param #number datatype Optional datatype to query, if nil it will give everything as result can be BestInSlot.EXPANSION, BestInSlot_TYPE_RAIDTIER, BestInSlot.INSTANCE -- @param #multiple The filter for the supplied datatype function BestInSlot:GetInstances(datatype, arg) local instances = {} if not datatype then for k,v in pairs(data.instances) do if k ~= "__default" then tinsert(instances, k) end end return instances elseif datatype == BestInSlot.RAIDTIER then if not data.raidTiers[arg] then return instances end for i=1, #data.raidTiers[arg].instances do tinsert(instances, data.raidTiers[arg].instances[i]) end return instances end error("Invalid datatype given!") end function BestInSlot:GetLatest(datatype, filterdatatype, arg) -- Get Latest Expansion if datatype == self.EXPANSION then local selectedExpansion local selectedRaidTier = 0 for k,v in pairs(data.expansions) do for i=1,#v.raidTiers do if v.raidTiers[i] > selectedRaidTier then selectedRaidTier = v.raidTiers[i] selectedExpansion = k end end end return selectedExpansion -- Get Latest Raid Tier elseif datatype == self.RAIDTIER then local selected = 0 for k,v in pairs(data.raidTiers) do local isFiltered = false if filterdatatype then if filterdatatype == self.EXPANSION and v.expansion ~= arg then isFiltered = true end end if not isFiltered then selected = math.max(selected, k) end end return selected -- Get Latest Instance (based on latest raid tier, if no args given) elseif datatype == self.INSTANCE then local raidTier if arg then if filterdatatype == self.RAIDTIER then raidTier = arg else error("This has not been implemented yet!") end else raidTier = self:GetLatest(self.RAIDTIER) end return data.raidTiers[raidTier].instances[#data.raidTiers[raidTier].instances] elseif datatype == self.DIFFICULTY then if arg then if filterdatatype == self.INSTANCE then local raidTier = self:GetRaidTiers(self.INSTANCE, arg) return self:GetLatest(self.DIFFICULTY, self.RAIDTIER, raidTier) elseif filterdatatype == self.RAIDTIER then local raidTierData = data.raidTiers[arg].difficulties return #raidTierData end end error("This has not been implemented yet!") end error(tostring(datatype).." is an invalid datatype") end ---Gets the bosses for the instance --@param #string instance The instance to get the bosses for --@return #table Table with boss names, the id of the boss is the same as the table index function BestInSlot:GetInstanceBosses(instance) local bosses = {} local instanceData = data.bosses[instance] if not instanceData then return bosses end for i=1,#instanceData do bosses[i] = instanceData[i] end return bosses end local function helperGetBestInSlotItems(raidTier, difficulty, specialization, slotId) if not raidTier or not difficulty or not specialization then BestInSlot.console:AddError("Not enough parameters given for function 'helperGetBestInSlotItems'", raidTier, difficulty, specialization, slotId) if slotId then return nil else return {} end end local slots = BestInSlot.slots local requiredItems = {} local slotInfo = {} if not data.raidTiers[raidTier] then return requiredItems end for i=1,#slots do local slotid = GetInventorySlotInfo(slots[i]) local itemid = BestInSlot.db.char[raidTier][difficulty][specialization][slotid] if type(itemid) == "number" then local item = BestInSlot:GetItem(itemid) local itemRecord = {item = itemid, obtained = BestInSlot:HasItem(itemid, difficulty, true) or false, customitem = item and item.customitem} requiredItems[i] = itemRecord slotInfo[i] = slotid end end if raidTier >= 70000 and raidTier < 80000 and BestInSlot.Artifacts then local relics = { BestInSlot.Artifacts:GetBestInSlotRelics(raidTier, difficulty, specialization) } for i=1,3 do if relics[i] then requiredItems[29 + i] = {item = relics[i], obtained = BestInSlot:HasItem(relics[i], difficulty, true) or false} slotInfo[29 + i] = 29 + i end end end return requiredItems, slotInfo end --- Get the current BestInSlot items for the raidTier and difficulty -- @param #number raidTier The raidTier to get the BestInSlot from -- @param #number difficulty The Difficulty to request function BestInSlot:GetBestInSlotItems(raidTier, difficulty, specialization, slotId) if not raidTier or not difficulty then BestInSlot.console:AddError("BestInSlot:GetBestInSlot() missed parameter RaidTier or difficulty", raidTier, difficulty, specialization) return {} end if not specialization then local bisItems = {} local slotInfo = {} for i=1,GetNumSpecializations() do local specId = GetSpecializationInfo(i) bisItems[specId], slotInfo[specId] = helperGetBestInSlotItems(raidTier,difficulty,specId, slotId) end bisItems.spec = GetSpecializationInfo(self:GetSpecialization()) return bisItems, slotInfo else return helperGetBestInSlotItems(raidTier,difficulty,specialization, slotId) end end local function helperOrderBiSItems(table, orderTable) local newTable = {} for i in pairs(orderTable) do newTable[orderTable[i]] = table[i] end return newTable end --- Get the current BestInSlot items for the raidTier and difficulty ordered by SlotId instead of a plain list -- @param #number raidTier The raidTier to get the BestInSlot from -- @param #number difficulty The Difficulty to request -- @param #number specialization The specialization ID to request function BestInSlot:GetOrderedBestInSlotItems(raidTier, difficulty, specialization) local BiSList, OrderedList = self:GetBestInSlotItems(raidTier, difficulty, specialization) if specialization then return helperOrderBiSItems(BiSList, OrderedList) else local newTable = {} for k,v in pairs(BiSList) do if v and OrderedList[k] then newTable[k] = helperOrderBiSItems(v, OrderedList[k]) end end return newTable end end --- function BestInSlot:SetBestInSlotInfo() local raidTiers = self:GetRaidTiers() local specs = self:GetCustomLists(self:GetAllSpecializations()) local result for i=1,#raidTiers do local raidTier = raidTiers[i] local bisList = self.db.char[raidTier] local difficulties = self:GetDifficulties(self.RAIDTIER, raidTier) for difficId in pairs(bisList) do for specId in pairs(specs) do local specBiS = bisList[difficId][specId] for j in pairs(specBiS) do local item = itemData[specBiS[j]] if item then item.isBiS = item.isBiS or {} item.isBiS[difficId] = item.isBiS[difficId] or {} item.isBiS[difficId][specId] = true end end end end end end function BestInSlot:IsItemTierToken(itemId) return (tierTokenData[itemId] ~= nil) end local function helperIsTokenBestInSlot(BiSList, difficulty, specId, slotid) for i, iteminfo in pairs(BiSList) do local item = BestInSlot:GetItem(iteminfo.item, difficulty) if item.tieritem and BestInSlot:GetItemSlotID(item.equipSlot) == slotid then return BestInSlot:IsItemBestInSlot(item.itemid, difficulty, specId) end end end function BestInSlot:IsTokenBestInSlot(tokenItemId, difficulty, specId) if not self:IsItemTierToken(tokenItemId) then return false end local iteminfo = tierTokenData[tokenItemId] local _, class = UnitClass("player") if not tContains(iteminfo.classes, class) then return false end if not specId then local array = {} for specId, BiSList in pairs(self:GetBestInSlotItems(iteminfo.raidtier, difficulty)) do if specId ~= "spec" then array[specId] = helperIsTokenBestInSlot(BiSList, difficulty, specId, iteminfo.slotid) end end return array; else return helperIsTokenBestInSlot(self:GetBestInSlotItems(iteminfo.raidtier, difficulty, specId), difficulty, specId, iteminfo.slotid) end end --- Checks wether the supplied itemid is BestInSlot -- @param number itemdId The itemid to check -- @param number difficulty The difficulty for which to query the list -- @return table Table with the specs for which this item is BiS. -- @return boolean false if not best in slot function BestInSlot:IsItemBestInSlot(itemId, difficulty, specId) if self:IsItemTierToken(itemId) then return self:IsTokenBestInSlot(itemId, difficulty, specId) end local item = BestInSlot:GetItem(itemId) if item then if not item.isBiS then self:SetBestInSlotInfo() if not item.isBiS then --assume theere are no BiS items itemData[itemId].isBiS = {} end end if difficulty == nil then return item.isBiS elseif specId == nil then return item.isBiS[difficulty] else if item.isBiS[difficulty] then return item.isBiS[difficulty][specId] else return item.isBiS[difficulty] end end end end local automaticUpdateQueue = {} local automaticUpdateTimers local function doAutomaticUpdate() for i=1,#automaticUpdateQueue do BestInSlot:SendAddonMessage("automaticUpdate",automaticUpdateQueue[i], "GUILD") end wipe(automaticUpdateQueue) end local function queueAutomaticUpdate(data) local wasPresent = false for i=1,#automaticUpdateQueue do local item = automaticUpdateQueue[i] if item.raidTier == data.raidTier and item.difficulty == data.difficulty and item.spec == data.spec and item.spec == data.spec then item.bis = data.bis if automaticUpdateTimers ~= nil then automaticUpdateTimers:Cancel() end automaticUpdateTimers = C_Timer.NewTimer(10, doAutomaticUpdate) wasPresent = true break end end if not wasPresent then tinsert(automaticUpdateQueue, data) if automaticUpdateTimers == nil or automaticUpdateTimers._remainingIterations == 0 then automaticUpdateTimers = C_Timer.NewTimer(10, doAutomaticUpdate) end end end --- Set the item as BestInSlot item -- @param #number raidTier The raid tier to add the item to -- @param #number difficultyId The difficulty to add the item to -- @param #number specialization The specialization ID -- @param #number slotId The slot to add the item to -- @param #number itemId The item ID to add function BestInSlot:SetItemBestInSlot(raidTier, difficultyId, specialization, slotId, itemId) local currentId = self.db.char[raidTier][difficultyId][specialization][slotId] if type(currentId) == "number" then --Checks if the number was fetched, default return from DB is an empty array if itemData[currentId] then --checks if the item is present in the cahce, if so we need to set it's BiS to false if itemData[currentId].isBiS and itemData[currentId].isBiS[difficultyId] then local lowerRaidTiers = self:GetRaidTiers(self.RAIDTIER, raidTier) local isStillBiS = false local compareSlots if slotId == 11 or slotId == 12 then compareSlots = {11,12} elseif slotId == 13 or slotId == 14 then compareSlots = {13,14} elseif slotId == 16 or slotId == 17 then compareSlots = {16,17} else compareSlots = {slotId} end local i = 1 while i <= #lowerRaidTiers and not isStillBiS do for j=1,#compareSlots do local id = self.db.char[lowerRaidTiers[i]][difficultyId][specialization][compareSlots[j]] if id == currentId then isStillBiS = true break end end i = i + 1 end if not isStillBiS then itemData[currentId].isBiS[difficultyId][specialization] = nil end end end end self.db.char[raidTier][difficultyId][specialization][slotId] = itemId if IsInGuild() and self.options.sendAutomaticUpdates and type(specialization) ~= "string" then queueAutomaticUpdate({raidTier = raidTier, difficulty = difficultyId, bis = self:GetBestInSlotItems(raidTier, difficultyId, specialization), spec = specialization}) end if itemId and itemData[itemId] then itemData[itemId].isBiS = itemData[itemId].isBiS or {} itemData[itemId].isBiS[difficultyId] = itemData[itemId].isBiS[difficultyId] or {} itemData[itemId].isBiS[difficultyId][specialization] = true end end local function helperSaveBestInSlotList(tierdb, guildname, guildplayer, bislist, raidtier, difficulty, spec, registerHistory) if not tierdb[spec] then if registerHistory and not BestInSlot.History:HasHistory(guildplayer, raidtier, difficulty, spec) then BestInSlot.History:Add(guildplayer, {raidtier = raidtier, difficulty = difficulty, spec = spec} , BestInSlot.History.NEWLIST) end tierdb[spec] = bislist else for slot, slotdata in pairs(bislist) do local previtem = tierdb[spec][slot] if not previtem or previtem.item ~= slotdata.item then tierdb[spec][slot] = slotdata if registerHistory then BestInSlot.History:Add(guildplayer, {raidtier = raidtier, difficulty = difficulty, slot = slot, previtem = previtem and previtem.item, newitem = slotdata.item}) end end end end end function BestInSlot:SaveGuildBestInSlotList(guildname, guildplayer, bislist, raidtier, difficulty, spec, version) local chardb = self.db.factionrealm[guildname][guildplayer] chardb.activeSpec = bislist.spec local tierdb = chardb[raidtier][difficulty] local registerHistory = version > 275 self.console:Add("Save guild bis list", guildname, guildplayer, raidtier, difficulty, spec, version, registerHistory, bislist) if not spec then for specid, specdata in pairs(bislist) do if specid ~= "spec" then helperSaveBestInSlotList(tierdb, guildname, guildplayer, specdata, raidtier, difficulty, specid, registerHistory) end end else helperSaveBestInSlotList(tierdb, guildname, guildplayer, bislist, raidtier, difficulty, spec, registerHistory) end self:SendEvent("GuildCacheUpdated") end local function tableHasItems(table) if type(table) == "table" then for _ in pairs(table) do return true end end return false end local GetCacheDataCache = {} local GuildBiSCache = {} local function refreshCache() BestInSlot:UnregisterEvent("GuildCacheUpdated", GetCacheDataCache.eventId) wipe(GetCacheDataCache) wipe(GuildBiSCache) end --- Gets the cached BiS lists -- @return The Bis lists function BestInSlot:GetCacheData() if #GetCacheDataCache > 0 then return unpack(GetCacheDataCache) end local db = self.db.factionrealm local result = {} local raidTiers = {} local guilds = {} local difficulties = {} for guildName, guildData in pairs(db) do if guildName ~= "_history" then if tableHasItems(guildData) then tinsert(guilds, guildName) for playerName, playerData in pairs(guildData) do if tableHasItems(playerData) then --checks if the player allready exists in the table result[playerName] = {guild = guildName} for raidTier, raidTierData in pairs(playerData) do if tableHasItems(raidTierData) then --checks if the raidTier allready exists in the table local addRaidTier = true for i=1,#raidTiers do if raidTiers[i] == raidTier then addRaidTier = false break end end if addRaidTier then tinsert(raidTiers, raidTier) end for difficulty, bisList in pairs(raidTierData) do if tableHasItems(bisList) then difficulties[raidTier] = difficulties[raidTier] or {} local addDifficulty = true for i=1,#difficulties[raidTier] do if difficulties[raidTier][i] == difficulty then addDifficulty = false break end end if addDifficulty then tinsert(difficulties[raidTier], difficulty) tsort(difficulties[raidTier]) end tinsert(result[playerName], {raidTier, difficulty}) end end end end end end end end end GetCacheDataCache = { result, raidTiers, guilds, difficulties } GetCacheDataCache.eventId = self:RegisterEvent("GuildCacheUpdated", refreshCache) return unpack(GetCacheDataCache) end ---Gets a list with players who need the specified itemid -- @return table Table with player as key, and their specs as table as value function BestInSlot:GetGuildMembersByItemID(itemid, difficulty) if GuildBiSCache[itemid] and GuildBiSCache[itemid][difficulty] then return GuildBiSCache[itemid][difficulty] end if not IsInGuild() then return {} end local guildName = GetGuildInfo("player") local playerData, raidData, guildData, difficultyData = self:GetCacheData() if not tContains(guildData, guildName) then return {} end local result = {} local tokenData = tierTokenData[itemid] for player, playerInfo in pairs(playerData) do local class = self:GetPlayerClass(player) for i=1, #playerInfo do local raidInfo = playerInfo[i] if raidInfo[2] == difficulty then --Check if the player has set the difficulty that we ask local raidBiSList = self.db.factionrealm[guildName][player][raidInfo[1]][raidInfo[2]] for specId, specData in pairs(raidBiSList) do if type(specData) == "table" then for j in pairs(specData) do if not tokenData then if specData[j].item == itemid then result[player] = result[player] or {} result[player][specId] = specData[j].obtained break end else local itemid2 = specData[j].item local item2 = itemData[itemid2] if item2 then local raidtier = self:GetRaidTiers(self.INSTANCE, item2.dungeon) local equipslot = BestInSlot:GetItemSlotID(item2.equipSlot) if tokenData.raidtier == raidtier and tContains(tokenData.classes, class) and equipslot == tokenData.slotid then result[player] = result[player] or {} result[player][specId] = specData[j].obtained break end end end end end end end end end if not GuildBiSCache[itemid] then GuildBiSCache[itemid] = {} end GuildBiSCache[itemid][difficulty] = result return result end --- Function to catch the nil that GetSpecialization() returns -- @return 1,2,3,4 depending on the character's specialization, defaults to 1 if none selected function BestInSlot:GetSpecialization() return GetSpecialization() or 1 end --- Function to get all the specializations of the player -- @return table Table with as index the global ID and value the localized description function BestInSlot:GetAllSpecializations() local result = {} for i=1,GetNumSpecializations() do local id, name = GetSpecializationInfo(i) result[id] = name end return result end local playerClassCache = setmetatable({DEFAULT = {}}, { __index = function(table, key) if rawget(table, key) then return rawget(table, key) end local searchRealm local searchName = key if (key):find("-") then searchName, searchRealm = (key):match("(%D+)-(%D+)") end if IsInRaid() then for i=1,40 do local name, realm = UnitName("raid"..i) if name and (not realm or realm == searchRealm) and name == searchName then local class, localized = UnitClass("raid"..i) --1 = Hunter, 2==HUNTER table[key] = {localized, class} return {class, localized} end end elseif IsInGroup() then for i=1,5 do local name, realm = UnitName("party"..i) if name and (not realm or realm == searchRealm) and name == searchName then local localized, class = UnitClass("party"..i) --1 = Hunter, 2==HUNTER table[key] = {class, localized} return {class, localized} end end end if IsInGuild() then local playerRealm = select(2, UnitFullName("player")) for i=1,GetNumGuildMembers() do local nameWithRealm, _, _, _, localizedClass, _, _, _, _, _, class = GetGuildRosterInfo(i) --Warrior, WARRIOR local playerName, realm = (nameWithRealm):match("(%D+)-(%D+)") if playerName == searchName and ((not searchRealm and realm == playerRealm) or realm == searchRealm) then table[key] = {class, localizedClass} return {class, localizedClass} end end end return rawget(table, "DEFAULT") end }) --- Lookup what class a player in the guild is -- @param #string name The name of the player to lookup -- @return #string The class of the player if found -- @return #nil Nil if the player was not found function BestInSlot:GetPlayerClass(name) if not name then return end return playerClassCache[name][1], playerClassCache[name][2] end function BestInSlot:GetPlayerString(name) local playerClass = self:GetPlayerClass(name) if not playerClass then return ("|cffb5b4ff%s|r"):format(name or "Unknown name") else return ("%s%s|r"):format(self:GetClassColor(playerClass) or "|cffb5b4ff", name or "Unknown name") end end function BestInSlot:GetClassColor(class) if RAID_CLASS_COLORS[class] then return "|c"..RAID_CLASS_COLORS[class].colorStr end end function BestInSlot:GetClassString(class) return (self:GetClassColor(class) or "|cffb5ffb4")..(LOCALIZED_CLASS_NAMES_MALE[class] or "Unknown class").."|r" end local guildRankCache = {} function BestInSlot:GetGuildRank(player) if not guildRankCache[player] then local searchName, searchRealm = player, nil local playerRealm = select(2, UnitFullName("player")) if((player):find("-")) then searchName, searchRealm = (player):match("(%D+)-(%D+)") end for i=1,GetNumGuildMembers() do local nameWithRealm, rankDescr, guildRank = GetGuildRosterInfo(i) local playerName, realmName = (nameWithRealm):match("(%D+)-(%D+)") if playerName == searchName and ((not searchRealm and realmName == playerRealm) or (searchRealm and realmName == searchRealm) )then guildRankCache[player] = {guildRank + 1, rankDescr} end end end if guildRankCache[player] then return unpack(guildRankCache[player]) end end function BestInSlot:GetPlayerInfo() return {race = select(2, UnitRace("player")), sex = UnitSex("player") - 2, name = UnitName("player"), class = select(2, UnitClass("player"))} end