Mercurial > wow > askmrrobot
diff Gear.lua @ 124:e31b02b24488
Updated for 8.0 pre-patch and BfA.
author | yellowfive |
---|---|
date | Tue, 17 Jul 2018 09:57:39 -0700 |
parents | f1da233629be |
children | d9a059484b22 |
line wrap: on
line diff
--- a/Gear.lua Mon Feb 12 19:34:09 2018 -0800 +++ b/Gear.lua Tue Jul 17 09:57:39 2018 -0700 @@ -7,9 +7,17 @@ -- Returns a number indicating how different two items are (0 means the same, higher means more different) local function countItemDifferences(item1, item2) - if item1 == nil and item2 == nil then return 0 end - - -- different items (id + bonus ids + suffix, constitutes a different physical drop) + -- both nil, the same + if not item1 and not item2 then + return 0 + end + + -- one nil and other not, or different id, totally different + if (not item1 and item2) or (item1 and not item2) or item1.id ~= item2.id then + return 10000 + end + + -- different versions of same item (id + bonus ids + suffix + drop level, constitutes a different physical drop) if Amr.GetItemUniqueId(item1, true) ~= Amr.GetItemUniqueId(item2, true) then return 1000 end @@ -17,7 +25,42 @@ -- different upgrade levels of the same item if item1.upgradeId ~= item2.upgradeId then return 100 - end + end + + -- different azerite powers + local aztDiffs = 0 + if item1.azerite or item2.azerite then + if item1.azerite and not item2.azerite then + aztDiffs = #item1.azerite * 10 + elseif item2.azerite and not item1.azerite then + aztDiffs = #item2.azerite * 10 + else + -- count up number in item1 but missing from item2 + for i = 1, #item1.azerite do + local missing = false + for j = 1, #item2.azerite do + if item2[j] == item1[i] then + missing = false + end + end + if missing then + aztDiffs = aztDiffs + 10 + end + end + -- count up number in item2 but missing from item1 + for i = 1, #item2.azerite do + local missing = false + for j = 1, #item1.azerite do + if item1[j] == item2[i] then + missing = false + end + end + if missing then + aztDiffs = aztDiffs + 10 + end + end + end + end -- different gems local gemDiffs = 0 @@ -33,26 +76,24 @@ enchantDiff = 1 end - return gemDiffs + enchantDiff + return aztDiffs + gemDiffs + enchantDiff end -- given a table of items (keyed or indexed doesn't matter) find closest match to item, or nil if none are a match -local function findMatchingItemFromTable(item, list, bestLink, bestItem, bestDiff, bestLoc, usedItems, tableType) +local function findMatchingItemFromTable(item, list, bestItem, bestDiff, bestLoc, usedItems, tableType) if not list then return nil end local found = false - for k,v in pairs(list) do - local listItem = Amr.ParseItemLink(v) + for k,listItem in pairs(list) do if listItem then local diff = countItemDifferences(item, listItem) if diff < bestDiff then -- each physical item can only be used once, the usedItems table has items we can't use in this search local key = string.format("%s_%s", tableType, k) if not usedItems[key] then - bestLink = v bestItem = listItem bestDiff = diff - bestLoc = string.format("%s_%s", tableType, k) + bestLoc = key found = true end end @@ -60,24 +101,27 @@ end end - return bestLink, bestItem, bestDiff, bestLoc + return bestItem, bestDiff, bestLoc end --- search the player's equipped gear, bag, bank, and void storage for an item that best matches the specified item +-- search the player's equipped gear, bag, and bank for an item that best matches the specified item function Amr:FindMatchingItem(item, player, usedItems) if not item then return nil end local equipped = player.Equipped and player.Equipped[player.ActiveSpec] or nil - local bestLink, bestItem, bestDiff, bestLoc = findMatchingItemFromTable(item, equipped, nil, nil, 1000, nil, usedItems, "equip") - bestLink, bestItem, bestDiff, bestLoc = findMatchingItemFromTable(item, player.BagItems, bestLink, bestItem, bestDiff, bestLoc, usedItems, "bag") - bestLink, bestItem, bestDiff, bestLoc = findMatchingItemFromTable(item, player.BankItems, bestLink, bestItem, bestDiff, bestLoc, usedItems, "bank") - bestLink, bestItem, bestDiff, bestLoc = findMatchingItemFromTable(item, player.VoidItems, bestLink, bestItem, bestDiff, bestLoc, usedItems, "void") + local bestItem, bestDiff, bestLoc = findMatchingItemFromTable(item, equipped, nil, 10000, nil, usedItems, "equip") + bestItem, bestDiff, bestLoc = findMatchingItemFromTable(item, player.BagItems, bestItem, bestDiff, bestLoc, usedItems, "bag") + if player.BankItems then + for bagId,bagList in pairs(player.BankItems) do + bestItem, bestDiff, bestLoc = findMatchingItemFromTable(item, bagList, bestItem, bestDiff, bestLoc, usedItems, "bank" .. bagId) + end + end - if bestDiff >= 1000 then - return nil, nil, 1000 + if bestDiff >= 10000 then + return nil, 10000 else usedItems[bestLoc] = true - return bestLink, bestItem, bestDiff + return bestItem, bestDiff end end @@ -86,25 +130,64 @@ local panelBlank = AceGUI:Create("AmrUiPanel") panelBlank:SetLayout("None") panelBlank:SetBackgroundColor(Amr.Colors.Black, 0.4) + container:AddChild(panelBlank) panelBlank:SetPoint("TOPLEFT", container.content, "TOPLEFT", 6, 0) panelBlank:SetPoint("BOTTOMRIGHT", container.content, "BOTTOMRIGHT") - container:AddChild(panelBlank) local lbl = AceGUI:Create("AmrUiLabel") + panelBlank:AddChild(lbl) lbl:SetText(L.GearBlank) lbl:SetWidth(700) lbl:SetJustifyH("MIDDLE") lbl:SetFont(Amr.CreateFont("Italic", 16, Amr.Colors.TextTan)) lbl:SetPoint("BOTTOM", panelBlank.content, "CENTER", 0, 20) - panelBlank:AddChild(lbl) local lbl2 = AceGUI:Create("AmrUiLabel") + panelBlank:AddChild(lbl2) lbl2:SetText(L.GearBlank2) lbl2:SetWidth(700) lbl2:SetJustifyH("MIDDLE") lbl2:SetFont(Amr.CreateFont("Italic", 16, Amr.Colors.TextTan)) lbl2:SetPoint("TOP", lbl.frame, "CENTER", 0, -20) - panelBlank:AddChild(lbl2) +end + +-- helper to create a widget for showing a socket or azerite power +local function createSocketWidget(panelMods, prevWidget, prevIsSocket, isEquipped) + + -- highlight for socket that doesn't match + local socketBorder = AceGUI:Create("AmrUiPanel") + panelMods:AddChild(socketBorder) + if not prevIsSocket then + socketBorder:SetPoint("LEFT", prevWidget.frame, "RIGHT", 30, 0) + else + socketBorder:SetPoint("LEFT", prevWidget.frame, "RIGHT", 2, 0) + end + socketBorder:SetLayout("None") + socketBorder:SetBackgroundColor(Amr.Colors.Black, isEquipped and 0 or 1) + socketBorder:SetWidth(26) + socketBorder:SetHeight(26) + if isEquipped then + socketBorder:SetAlpha(0.3) + end + + local socketBg = AceGUI:Create("AmrUiIcon") + socketBorder:AddChild(socketBg) + socketBg:SetPoint("TOPLEFT", socketBorder.content, "TOPLEFT", 1, -1) + socketBg:SetLayout("None") + socketBg:SetBorderWidth(2) + socketBg:SetIconBorderColor(Amr.Colors.Green, isEquipped and 0 or 1) + socketBg:SetWidth(24) + socketBg:SetHeight(24) + + local socketIcon = AceGUI:Create("AmrUiIcon") + socketBg:AddChild(socketIcon) + socketIcon:SetPoint("CENTER", socketBg.content, "CENTER") + socketIcon:SetBorderWidth(1) + socketIcon:SetIconBorderColor(Amr.Colors.White) + socketIcon:SetWidth(18) + socketIcon:SetHeight(18) + + return socketBorder, socketIcon end local function renderGear(spec, container) @@ -120,16 +203,16 @@ local panelGear = AceGUI:Create("AmrUiPanel") panelGear:SetLayout("None") panelGear:SetBackgroundColor(Amr.Colors.Black, 0.3) + container:AddChild(panelGear) panelGear:SetPoint("TOPLEFT", container.content, "TOPLEFT", 6, 0) panelGear:SetPoint("BOTTOMRIGHT", container.content, "BOTTOMRIGHT", -300, 0) - container:AddChild(panelGear) local panelMods = AceGUI:Create("AmrUiPanel") panelMods:SetLayout("None") + panelMods:SetBackgroundColor(Amr.Colors.Black, 0.3) + container:AddChild(panelMods) panelMods:SetPoint("TOPLEFT", panelGear.frame, "TOPRIGHT", 15, 0) panelMods:SetPoint("BOTTOMRIGHT", container.content, "BOTTOMRIGHT") - panelMods:SetBackgroundColor(Amr.Colors.Black, 0.3) - container:AddChild(panelMods) -- spec icon local icon = AceGUI:Create("AmrUiIcon") @@ -145,8 +228,8 @@ end icon:SetIcon("Interface\\Icons\\" .. Amr.SpecIcons[iconSpec]) + panelGear:AddChild(icon) icon:SetPoint("TOPLEFT", panelGear.content, "TOPLEFT", 10, -10) - panelGear:AddChild(icon) local btnEquip = AceGUI:Create("AmrUiButton") btnEquip:SetText(L.GearButtonEquip(L.SpecsShort[player.Specs[spec]])) @@ -154,12 +237,12 @@ btnEquip:SetFont(Amr.CreateFont("Regular", 14, Amr.Colors.White)) btnEquip:SetWidth(300) btnEquip:SetHeight(26) - btnEquip:SetPoint("LEFT", icon.frame, "RIGHT", 40, 0) - btnEquip:SetPoint("RIGHT", panelGear.content, "RIGHT", -40, 0) btnEquip:SetCallback("OnClick", function(widget) Amr:EquipGearSet(spec) end) panelGear:AddChild(btnEquip) + btnEquip:SetPoint("LEFT", icon.frame, "RIGHT", 40, 0) + btnEquip:SetPoint("RIGHT", panelGear.content, "RIGHT", -40, 0) -- each physical item can only be used once, this tracks ones we have already used local usedItems = {} @@ -169,8 +252,8 @@ for slotNum = 1, #Amr.SlotIds do local slotId = Amr.SlotIds[slotNum] - local equippedItemLink = equipped and equipped[slotId] or nil - local equippedItem = Amr.ParseItemLink(equippedItemLink) + local equippedItem = equipped and equipped[slotId] or nil + local equippedItemLink = equipped and equipped.link or nil local optimalItem = gear[slotId] local optimalItemLink = Amr.CreateItemLink(optimalItem) @@ -179,41 +262,43 @@ if equippedItem and optimalItem and Amr.GetItemUniqueId(equippedItem) == Amr.GetItemUniqueId(optimalItem) then isEquipped = true end + + local isAzerite = optimalItem and C_AzeriteEmpoweredItem.IsAzeriteEmpoweredItemByID(optimalItem.id) -- find the item in the player's inventory that best matches what the optimization wants to use - local matchItemLink, matchItem = Amr:FindMatchingItem(optimalItem, player, usedItems) + local matchItem = Amr:FindMatchingItem(optimalItem, player, usedItems) -- slot label local lbl = AceGUI:Create("AmrUiLabel") + panelGear:AddChild(lbl) + lbl:SetPoint("TOPLEFT", prevElem.frame, "BOTTOMLEFT", 0, -12) lbl:SetText(Amr.SlotDisplayText[slotId]) lbl:SetWidth(85) lbl:SetFont(Amr.CreateFont("Regular", 14, Amr.Colors.White)) - lbl:SetPoint("TOPLEFT", prevElem.frame, "BOTTOMLEFT", 0, -12) - panelGear:AddChild(lbl) prevElem = lbl -- ilvl label local lblIlvl = AceGUI:Create("AmrUiLabel") + panelGear:AddChild(lblIlvl) + lblIlvl:SetPoint("TOPLEFT", lbl.frame, "TOPRIGHT", 0, 0) lblIlvl:SetWidth(45) lblIlvl:SetFont(Amr.CreateFont("Italic", 14, Amr.Colors.TextTan)) - lblIlvl:SetPoint("TOPLEFT", lbl.frame, "TOPRIGHT", 0, 0) - panelGear:AddChild(lblIlvl) -- equipped label local lblEquipped = AceGUI:Create("AmrUiLabel") + panelGear:AddChild(lblEquipped) + lblEquipped:SetPoint("TOPLEFT", lblIlvl.frame, "TOPRIGHT", 0, 0) lblEquipped:SetWidth(20) lblEquipped:SetFont(Amr.CreateFont("Regular", 14, Amr.Colors.White)) - lblEquipped:SetPoint("TOPLEFT", lblIlvl.frame, "TOPRIGHT", 0, 0) lblEquipped:SetText(isEquipped and "E" or "") - panelGear:AddChild(lblEquipped) -- item name/link label local lblItem = AceGUI:Create("AmrUiLabel") + panelGear:AddChild(lblItem) + lblItem:SetPoint("TOPLEFT", lblEquipped.frame, "TOPRIGHT", 0, 0) lblItem:SetWordWrap(false) lblItem:SetWidth(345) lblItem:SetFont(Amr.CreateFont(isEquipped and "Regular" or "Bold", isEquipped and 14 or 15, Amr.Colors.White)) - lblItem:SetPoint("TOPLEFT", lblEquipped.frame, "TOPRIGHT", 0, 0) - panelGear:AddChild(lblItem) -- fill the name/ilvl labels, which may require asynchronous loading of item information if optimalItemLink then @@ -221,107 +306,105 @@ -- set item name, tooltip, and ilvl obj.nameLabel:SetText(link:gsub("%[", ""):gsub("%]", "")) - -- not quite right but whatever... close enough if quality == 6 then - local tmprel = optimalItem.relicBonusIds - optimalItem.relicBonusIds = nil - link = Amr.CreateItemLink(optimalItem) - optimalItem.relicBonusIds = tmprel + -- not quite right but whatever... close enough, artifacts are a thing of the past + local tmprel = obj.optimalItem.relicBonusIds + obj.optimalItem.relicBonusIds = nil + link = Amr.CreateItemLink(obj.optimalItem) + obj.optimalItem.relicBonusIds = tmprel + + -- for artifacts, we consider it equipped if the item id alone matches + if obj.equippedItem and obj.equippedItem.id == obj.optimalItem.id then + obj.isEquipped = true + end + obj.equipLabel:SetText(obj.isEquipped and "E" or "") end - Amr:SetItemTooltip(obj.nameLabel, link) + Amr:SetItemTooltip(obj.nameLabel, link, "ANCHOR_TOPRIGHT") - -- the game's info gives the wrong item level, so we have to scan for it - --iLevel = (quality ~= 6 or optimalItem.relicBonusIds) and Amr.GetItemLevel(nil, nil, link) or "" - obj.ilvlLabel:SetText(iLevel) - - end, { ilvlLabel = lblIlvl, nameLabel = lblItem }) + local itemObj = Item:CreateFromItemLink(link) + if itemObj then + -- game's GetItemInfo method returns the wrong ilvl sometimes, so use the new item api to get it + iLevel = itemObj:GetCurrentItemLevel() + end + obj.ilvlLabel:SetText(iLevel) + + end, { + ilvlLabel = lblIlvl, + nameLabel = lblItem, + equipLabel = lblEquipped, + optimalItem = optimalItem, + equippedItem = equippedItem, + isEquipped = isEquipped + }) end -- modifications if optimalItem then - local itemInfo = Amr.db.char.ExtraItemData[spec][optimalItem.id] - -- gems - if itemInfo and itemInfo.socketColors then - local prevSocket = nil - for i = 1, #itemInfo.socketColors do + -- gems or azerite powers + local prevSocket = nil + + if isAzerite then + local azt = optimalItem.azerite or {} + for i,spellId in ipairs(azt) do + if spellId and spellId ~= 0 then + local equippedAzt = equippedItem and equippedItem.azerite or {} + local isPowerActive = Amr.Contains(equippedAzt, spellId) + + local socketBorder, socketIcon = createSocketWidget(panelMods, prevSocket or lblItem, prevSocket, isPowerActive) + + -- set icon and tooltip + local spellName, _, spellIcon = GetSpellInfo(spellId) + socketIcon:SetIcon(spellIcon) + Amr:SetSpellTooltip(socketIcon, spellId, "ANCHOR_TOPRIGHT") + + prevSocket = socketBorder + end + end + else + for i = 1, #optimalItem.gemIds do + -- we rely on the fact that the gear sets coming back from the site will almost always have all sockets filled, + -- because it's a pain to get the actual number of sockets on an item from within the game local g = optimalItem.gemIds[i] - local isGemEquipped = g ~= 0 and matchItem and matchItem.gemIds and matchItem.gemIds[i] == g + if g == 0 then break end + + local isGemEquipped = matchItem and matchItem.gemIds and matchItem.gemIds[i] == g - -- highlight for gem that doesn't match - local socketBorder = AceGUI:Create("AmrUiPanel") - socketBorder:SetLayout("None") - socketBorder:SetBackgroundColor(Amr.Colors.Black, isGemEquipped and 0 or 1) - socketBorder:SetWidth(26) - socketBorder:SetHeight(26) - if not prevSocket then - socketBorder:SetPoint("LEFT", lblItem.frame, "RIGHT", 30, 0) - else - socketBorder:SetPoint("LEFT", prevSocket.frame, "RIGHT", 2, 0) - end - if isGemEquipped then - socketBorder:SetAlpha(0.3) - end - panelMods:AddChild(socketBorder) - - local socketBg = AceGUI:Create("AmrUiIcon") - socketBg:SetLayout("None") - socketBg:SetBorderWidth(2) - socketBg:SetIconBorderColor(Amr.Colors.Green, isGemEquipped and 0 or 1) - socketBg:SetWidth(24) - socketBg:SetHeight(24) - socketBg:SetPoint("TOPLEFT", socketBorder.content, "TOPLEFT", 1, -1) - socketBorder:AddChild(socketBg) - - local socketIcon = AceGUI:Create("AmrUiIcon") - socketIcon:SetBorderWidth(1) - socketIcon:SetIconBorderColor(Amr.Colors.White) - socketIcon:SetWidth(18) - socketIcon:SetHeight(18) - socketIcon:SetPoint("CENTER", socketBg.content, "CENTER") - socketBg:AddChild(socketIcon) + local socketBorder, socketIcon = createSocketWidget(panelMods, prevSocket or lblItem, prevSocket, isGemEquipped) -- get icon for optimized gem - if g ~= 0 then - local gemInfo = Amr.db.char.ExtraGemData[spec][g] - if gemInfo then - local gident = gemInfo.id - if optimalItem.relicBonusIds then - gident = Amr.CreateItemLink({ id = gemInfo.id, enchantId = 0, gemIds = {0,0,0,0}, suffixId = 0, bonusIds = optimalItem.relicBonusIds[i]}) - end - Amr.GetItemInfo(gident, function(obj, name, link, quality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, texture) - -- set icon and a tooltip - obj:SetIcon(texture) - Amr:SetItemTooltip(obj, link) - end, socketIcon) - end - end + Amr.GetItemInfo(g, function(obj, name, link, quality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, texture) + -- set icon and a tooltip + obj:SetIcon(texture) + Amr:SetItemTooltip(obj, link, "ANCHOR_TOPRIGHT") + end, socketIcon) prevSocket = socketBorder end end - + -- enchant if optimalItem.enchantId and optimalItem.enchantId ~= 0 then local isEnchantEquipped = matchItem and matchItem.enchantId and matchItem.enchantId == optimalItem.enchantId local lblEnchant = AceGUI:Create("AmrUiLabel") + panelMods:AddChild(lblEnchant) + lblEnchant:SetPoint("TOPLEFT", lblItem.frame, "TOPRIGHT", 130, 0) lblEnchant:SetWordWrap(false) lblEnchant:SetWidth(170) lblEnchant:SetFont(Amr.CreateFont(isEnchantEquipped and "Regular" or "Bold", 14, isEnchantEquipped and Amr.Colors.TextGray or Amr.Colors.White)) - lblEnchant:SetPoint("TOPLEFT", lblItem.frame, "TOPRIGHT", 130, 0) - local enchInfo = Amr.db.char.ExtraEnchantData[spec][optimalItem.enchantId] + local enchInfo = Amr.db.char.ExtraEnchantData[optimalItem.enchantId] if enchInfo then lblEnchant:SetText(enchInfo.text) Amr.GetItemInfo(enchInfo.itemId, function(obj, name, link) - Amr:SetItemTooltip(obj, link) + Amr:SetItemTooltip(obj, link, "ANCHOR_TOPRIGHT") end, lblEnchant) --Amr:SetSpellTooltip(lblEnchant, enchInfo.spellId) end - panelMods:AddChild(lblEnchant) + end end @@ -349,40 +432,40 @@ btnImport:SetFont(Amr.CreateFont("Bold", 16, Amr.Colors.White)) btnImport:SetWidth(120) btnImport:SetHeight(26) - btnImport:SetPoint("TOPLEFT", container.content, "TOPLEFT", 0, -81) btnImport:SetCallback("OnClick", onImportClick) container:AddChild(btnImport) + btnImport:SetPoint("TOPLEFT", container.content, "TOPLEFT", 0, -81) local lbl = AceGUI:Create("AmrUiLabel") + container:AddChild(lbl) lbl:SetText(L.GearImportNote) lbl:SetWidth(100) lbl:SetFont(Amr.CreateFont("Italic", 12, Amr.Colors.TextTan)) lbl:SetJustifyH("MIDDLE") lbl:SetPoint("TOP", btnImport.frame, "BOTTOM", 0, -5) - container:AddChild(lbl) local lbl2 = AceGUI:Create("AmrUiLabel") + container:AddChild(lbl2) lbl2:SetText(L.GearTipTitle) lbl2:SetWidth(140) lbl2:SetFont(Amr.CreateFont("Italic", 20, Amr.Colors.Text)) lbl2:SetJustifyH("MIDDLE") lbl2:SetPoint("TOP", lbl.frame, "BOTTOM", 0, -50) - container:AddChild(lbl2) lbl = AceGUI:Create("AmrUiLabel") + container:AddChild(lbl) lbl:SetText(L.GearTipText) lbl:SetWidth(140) lbl:SetFont(Amr.CreateFont("Italic", 12, Amr.Colors.Text)) lbl:SetJustifyH("MIDDLE") lbl:SetPoint("TOP", lbl2.frame, "BOTTOM", 0, -5) - container:AddChild(lbl) lbl2 = AceGUI:Create("AmrUiLabel") + container:AddChild(lbl2) lbl2:SetText(L.GearTipCommands) lbl2:SetWidth(130) lbl2:SetFont(Amr.CreateFont("Italic", 12, Amr.Colors.Text)) lbl2:SetPoint("TOP", lbl.frame, "BOTTOM", 10, -5) - container:AddChild(lbl2) local t = AceGUI:Create("AmrUiTabGroup") t:SetLayout("None") @@ -397,9 +480,9 @@ t:SetTabs(tabz) t:SetCallback("OnGroupSelected", onGearTabSelected) + container:AddChild(t) t:SetPoint("TOPLEFT", container.content, "TOPLEFT", 144, -30) t:SetPoint("BOTTOMRIGHT", container.content, "BOTTOMRIGHT") - container:AddChild(t) _gearTabs = t; local btnShop = AceGUI:Create("AmrUiButton") @@ -408,9 +491,9 @@ btnShop:SetFont(Amr.CreateFont("Regular", 14, Amr.Colors.White)) btnShop:SetWidth(245) btnShop:SetHeight(26) - btnShop:SetPoint("TOPRIGHT", container.content, "TOPRIGHT", -20, -25) btnShop:SetCallback("OnClick", function(widget) Amr:ShowShopWindow() end) container:AddChild(btnShop) + btnShop:SetPoint("TOPRIGHT", container.content, "TOPRIGHT", -20, -25) if not _activeTab then _activeTab = tostring(GetSpecialization()) @@ -443,56 +526,13 @@ -- Gear Set Management ------------------------------------------------------------------------------------------------ local _waitingForSpec = 0 -local _waitingForItemLock = nil -local _pendingEquip = nil -local _pendingRemove = nil +local _pendingGearOps = nil +local _currentGearOp = nil +local _itemLockAction = nil +local _gearOpPasses = 0 +local _gearOpWaiting = nil --- scan a bag for the best matching item -local function scanBagForItem(item, bagId, bestItem, bestDiff, bestLink) - local numSlots = GetContainerNumSlots(bagId) - for slotId = 1, numSlots do - local _, _, _, _, _, _, itemLink = GetContainerItemInfo(bagId, slotId) - -- we skip any stackable item, as far as we know, there is no equippable gear that can be stacked - if itemLink then - local bagItem = Amr.ParseItemLink(itemLink) - if bagItem ~= nil then - local diff = countItemDifferences(item, bagItem) - if diff < bestDiff then - bestItem = { bag = bagId, slot = slotId } - bestDiff = diff - bestLink = itemLink - end - end - end - end - return bestItem, bestDiff, bestLink -end - -local function onEquipGearSetComplete() - if Amr.db.profile.options.disableEm then return end - - -- create an equipment manager set - local specId, specName = GetSpecializationInfo(GetSpecialization()) - - local item = Amr.ParseItemLink(GetInventoryItemLink("player", INVSLOT_MAINHAND)) - if not item or not Amr.ArtifactIdToSpecNumber[item.id] then - item = Amr.ParseItemLink(GetInventoryItemLink("player", INVSLOT_OFFHAND)) - if item and not Amr.ArtifactIdToSpecNumber[item.id] then - item = nil - end - end - if item then - Amr.GetItemInfo(item.id, function(customArg, name, link, quality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, texture) - local setname = "AMR " .. specName - local setid = C_EquipmentSet.GetEquipmentSetID(setname) - if setid then - C_EquipmentSet.SaveEquipmentSet(setid, texture) - else - C_EquipmentSet.CreateEquipmentSet(setname, texture) - end - end) - end -end +local beginEquipGearSet, processCurrentGearOp, nextGearOp -- find the first empty slot in the player's backpack+bags local function findFirstEmptyBagSlot() @@ -516,30 +556,35 @@ return nil, nil end -local function finishEquipGearSet() - if not _pendingEquip then return end - - _pendingEquip.tries = _pendingEquip.tries + 1 - if _pendingEquip.tries > 16 then - -- too many tries, just give up (shouldn't happen but just to be safe) - _pendingEquip = nil - else - -- start over again, trying any items that could not be equipped in the previous pass (unique constraints) - Amr:EquipGearSet(_pendingEquip.spec) +-- scan a bag for the best matching item +local function scanBagForItem(item, bagId, bestItem, bestDiff, bestLink) + local numSlots = GetContainerNumSlots(bagId) + for slotId = 1, numSlots do + local _, _, _, _, _, _, itemLink = GetContainerItemInfo(bagId, slotId) + -- we skip any stackable item, as far as we know, there is no equippable gear that can be stacked + if itemLink then + local bagItem = Amr.ParseItemLink(itemLink) + if bagItem ~= nil then + local diff = countItemDifferences(item, bagItem) + if diff < bestDiff then + bestItem = { bag = bagId, slot = slotId } + bestDiff = diff + bestLink = itemLink + end + end + end end + return bestItem, bestDiff, bestLink end --- equip the next slot in a pending equip -local function tryEquipNextItem() - if not _pendingEquip then return end - - local item = _pendingEquip.itemsToEquip[_pendingEquip.nextSlot] - +-- find the item in the player's inventory that best matches the current gear op item, favoring stuff already equipped, then in bags, then in bank +local function findCurrentGearOpItem() + + local item = _currentGearOp.items[_currentGearOp.nextSlot] + local bestItem = nil local bestLink = nil - local bestDiff = 1000 - - -- find the best matching item + local bestDiff = 10000 -- inventory bestItem, bestDiff, bestLink = scanBagForItem(item, BACKPACK_CONTAINER, bestItem, bestDiff, bestLink) @@ -550,11 +595,11 @@ -- equipped items, but skip slots we have just equipped (to avoid e.g. just moving 2 of the same item back and forth between mh oh weapon slots) for slotNum = 1, #Amr.SlotIds do local slotId = Amr.SlotIds[slotNum] - if not _pendingEquip.doneSlots[slotId] then + if _currentGearOp.slotsRemaining[slotId] then local itemLink = GetInventoryItemLink("player", slotId) if itemLink then local invItem = Amr.ParseItemLink(itemLink) - if invItem ~= nil then + if invItem then local diff = countItemDifferences(item, invItem) if diff < bestDiff then bestItem = { slot = slotId } @@ -567,251 +612,429 @@ end -- bank - bestItem, bestDiff, bestLink = scanBagForItem(item, BANK_CONTAINER, bestItem, bestDiff, bestLink) - for bagId = NUM_BAG_SLOTS + 1, NUM_BAG_SLOTS + NUM_BANKBAGSLOTS do - bestItem, bestDiff, bestLink = scanBagForItem(item, bagId, bestItem, bestDiff, bestLink) + if bestDiff > 0 then + bestItem, bestDiff, bestLink = scanBagForItem(item, BANK_CONTAINER, bestItem, bestDiff, bestLink) + for bagId = NUM_BAG_SLOTS + 1, NUM_BAG_SLOTS + NUM_BANKBAGSLOTS do + bestItem, bestDiff, bestLink = scanBagForItem(item, bagId, bestItem, bestDiff, bestLink) + end end + + return bestItem, bestDiff, bestLink +end + +-- on completion, create an equipment manager set if desired +local function onEquipGearSetComplete() + if Amr.db.profile.options.disableEm then return end - ClearCursor() + -- create an equipment manager set + local specId, specName = GetSpecializationInfo(GetSpecialization()) - if not bestItem then - -- stop if we can't find an item - Amr:Print(L.GearEquipErrorNotFound) - Amr:Print(L.GearEquipErrorNotFound2) - _pendingEquip = nil - return - - elseif bestItem and bestItem.bag and (bestItem.bag == BANK_CONTAINER or bestItem.bag >= NUM_BAG_SLOTS + 1 and bestItem.bag <= NUM_BAG_SLOTS + NUM_BANKBAGSLOTS) then + local item = Amr.ParseItemLink(GetInventoryItemLink("player", INVSLOT_MAINHAND)) + if not item then + item = Amr.ParseItemLink(GetInventoryItemLink("player", INVSLOT_OFFHAND)) + end + if item then + Amr.GetItemInfo(item.id, function(customArg, name, link, quality, iLevel, reqLevel, class, subclass, maxStack, equipSlot, texture) + local setname = "AMR " .. specName + local setid = C_EquipmentSet.GetEquipmentSetID(setname) + if setid then + C_EquipmentSet.SaveEquipmentSet(setid, texture) + else + C_EquipmentSet.CreateEquipmentSet(setname, texture) + end + end) + end +end + +-- stop any currently in-progress gear swapping operation and clean up +local function disposeGearOp() + _pendingGearOps = nil + _currentGearOp = nil + _itemLockAction = nil + _gearOpPasses = 0 + _gearOpWaiting = nil + + -- make sure the gear tab is still in sync + Amr:RefreshGearTab() +end + +-- initialize a gear op to start running it +local function initializeGearOp(op, spec, pos) + op.pos = pos + op.spec = spec + + -- fill the remaining slot list and set the starting slot + op.nextSlot = nil + op.slotsRemaining = {} + op.isWaiting = false + for slotId, item in pairs(op.items) do + op.slotsRemaining[slotId] = true + if not op.nextSlot then + op.nextSlot = slotId + end + end +end + +function processCurrentGearOp() + if not _currentGearOp then return end + + if _currentGearOp.remove then + -- remove the next item + + -- check if the slot is already empty + local itemLink = GetInventoryItemLink("player", _currentGearOp.nextSlot) + if not itemLink then + nextGearOp() + return + end + -- find first empty bag slot local invBag, invSlot = findFirstEmptyBagSlot() if not invBag then -- stop if bags are too full Amr:Print(L.GearEquipErrorBagFull) - _pendingEquip = nil + disposeGearOp() return end - -- move from bank to bag - PickupContainerItem(bestItem.bag, bestItem.slot) + PickupInventoryItem(_currentGearOp.nextSlot) PickupContainerItem(invBag, invSlot) - -- set flag so that when we clear cursor and release the item lock, we can respond to the event and continue - _waitingForItemLock = { + -- set an action to happen on ITEM_UNLOCKED, triggered by ClearCursor + _itemLockAction = { bagId = invBag, - slotId = invSlot + slotId = invSlot, + isRemove = true } + + ClearCursor() + -- wait for remove to complete + else + -- equip the next item + local bestItem, bestDiff, bestLink = findCurrentGearOpItem() + + _itemLockAction = nil ClearCursor() - - -- now we need to wait for game event to continue and try this item again after it is in our bag - return - else - if not Amr:CanEquip(bestItem.bag, bestItem.slot) then + + if not bestItem then + -- stop if we can't find an item + Amr:Print(L.GearEquipErrorNotFound) + Amr:Print(L.GearEquipErrorNotFound2) + disposeGearOp() + + elseif bestItem and bestItem.bag and (bestItem.bag == BANK_CONTAINER or bestItem.bag >= NUM_BAG_SLOTS + 1 and bestItem.bag <= NUM_BAG_SLOTS + NUM_BANKBAGSLOTS) then + -- find first empty bag slot + local invBag, invSlot = findFirstEmptyBagSlot() + if not invBag then + -- stop if bags are too full + Amr:Print(L.GearEquipErrorBagFull) + disposeGearOp() + return + end + + -- move from bank to bag + PickupContainerItem(bestItem.bag, bestItem.slot) + PickupContainerItem(invBag, invSlot) + + -- set an action to happen on ITEM_UNLOCKED, triggered by ClearCursor + _itemLockAction = { + bagId = invBag, + slotId = invSlot, + isBank = true + } + + ClearCursor() + -- now we need to wait for game event to continue and try this item again after it is in our bag and unlocked + + elseif (bestItem.bag or bestItem.bag == 0) and not Amr:CanEquip(bestItem.bag, bestItem.slot) then -- if an item is not soulbound, then warn the user and quit Amr:Print(L.GearEquipErrorSoulbound(bestLink)) - _pendingEquip = nil - return + disposeGearOp() + else - local slotId = _pendingEquip.nextSlot - + -- an item in the player's bags or already equipped, equip it - _pendingEquip.bag = bestItem.bag - _pendingEquip.slot = bestItem.slot - _pendingEquip.destSlot = slotId - if bestItem.bag then PickupContainerItem(bestItem.bag, bestItem.slot) else + _gearOpWaiting.inventory[bestItem.slot] = true PickupInventoryItem(bestItem.slot) end - PickupInventoryItem(slotId) - ClearCursor() + _gearOpWaiting.inventory[_currentGearOp.nextSlot] = true + PickupInventoryItem(_currentGearOp.nextSlot) + + -- don't wait for now, do all equips at once + --[[ + -- set an action to happen on ITEM_UNLOCKED, triggered by ClearCursor + _itemLockAction = { + bagId = bestItem.bag, + slotId = bestItem.slot, + invSlot = _currentGearOp.nextSlot, + isEquip = true + } + ]] + + ClearCursor() + nextGearOp() + end + + end +end + +-- when a gear op completes successfully, this will advance to the next op or finish +function nextGearOp() + if not _currentGearOp then return end + + local spec = _currentGearOp.spec + local pos = _currentGearOp.pos + local passes = _gearOpPasses + + -- mark the slot as done and move to the next + if _currentGearOp.nextSlot then + _currentGearOp.slotsRemaining[_currentGearOp.nextSlot] = nil + _currentGearOp.nextSlot = nil + for slotId, item in pairs(_currentGearOp.items) do + if _currentGearOp.slotsRemaining[slotId] then + _currentGearOp.nextSlot = slotId + break + end + end + end + + if not _currentGearOp.nextSlot then + -- see if anything is still in progress and we want to wait for it before continuing + local inProgress = not Amr.IsEmpty(_gearOpWaiting.inventory) + + if (_currentGearOp.wait or _currentGearOp.remove) and inProgress then + -- this will cause the item unlock handler to call nextGearOp again when all in-progress swaps have unlocked related slots + _currentGearOp.isWaiting = true + else + _currentGearOp = _pendingGearOps[pos + 1] + if _currentGearOp then + -- we have another op, do it + initializeGearOp(_currentGearOp, spec, pos + 1) + processCurrentGearOp() + else + -- we are done + disposeGearOp() + + -- this will check if not all items were swapped, and either finish up, try again, or abort if have tried too many times + beginEquipGearSet(spec, passes + 1) + end + end + else + -- do the next item + processCurrentGearOp() + end + +end + +local function handleItemUnlocked(bagId, slotId) + + -- mark anything that is waiting as unlocked if it is no longer locked + if _currentGearOp and _gearOpWaiting then + for i,s in ipairs(Amr.SlotIds) do + if not IsInventoryItemLocked(s) then + _gearOpWaiting.inventory[s] = nil + end + end + end + + if _itemLockAction then + if _itemLockAction.isRemove then + -- waiting for a specific remove op to finish before continuing + if bagId == _itemLockAction.bagId and slotId == _itemLockAction.slotId then + _itemLockAction = nil + nextGearOp() + end + elseif _itemLockAction.isBank then + -- waiting for an item to move from bank into inventory, then reprocess the current op + if bagId == _itemLockAction.bagId and slotId == _itemLockAction.slotId then + _itemLockAction = nil + processCurrentGearOp() + end + + elseif _itemLockAction.isEquip then + -- this is not currently used... we do all equips at once usually, but could go back to this if it causes problems + + -- waiting for a specific equip op to finish - -- wait for game events to continue + -- inventory slot we're swapping to is still locked, can't continue yet + if IsInventoryItemLocked(_itemLockAction.invSlot) then return end + + if _itemLockAction.bagId then + local _, _, locked = GetContainerItemInfo(_itemLockAction.bagId, _itemLockAction.slotId) + -- the bag slot we're swapping from is still locked, can't continue yet + if locked then return end + else + -- inventory slot we're swapping from is still locked, can't continue yet + if IsInventoryItemLocked(_itemLockAction.slotId) then return end + end + + _itemLockAction = nil + nextGearOp() + else + -- unknown... shouldn't happen + _itemLockAction = nil end + else + + -- not waiting on a specific action, check if we are waiting for all locked slots to open up and they are done + if _currentGearOp and _gearOpWaiting and _currentGearOp.isWaiting and Amr.IsEmpty(_gearOpWaiting.inventory) then + nextGearOp() + end end end -local function removeNextItem() - if not _pendingRemove then return end - - local list = _pendingRemove.slotsToRemove - local slot = list[#list - _pendingRemove.remaining + 1] - - -- find first empty bag slot - local invBag, invSlot = findFirstEmptyBagSlot() - if not invBag then - -- stop if bags are too full - Amr:Print(L.GearEquipErrorBagFull) - _pendingRemove = nil - _pendingEquip = nil - return +local function shuffle(tbl) + local size = #tbl + for i = size, 1, -1 do + local rand = math.random(size) + tbl[i], tbl[rand] = tbl[rand], tbl[i] end - - PickupInventoryItem(slot) - PickupContainerItem(invBag, invSlot) - - -- set flag so that when we clear cursor and release the item lock, we can respond to the event and continue - _waitingForItemLock = { - bagId = invBag, - slotId = invSlot, - isRemove = true - } - - ClearCursor() + return tbl end -local function onItemUnlocked(bagId, slotId) - - if _waitingForItemLock then - -- waiting on a move from bank to bags to complete, or waiting on removing an item to complete, just continue as normal afterwards - if bagId == _waitingForItemLock.bagId and slotId == _waitingForItemLock.slotId then - local isremove = _waitingForItemLock.isRemove - _waitingForItemLock = nil - - if isremove then - _pendingRemove.remaining = _pendingRemove.remaining - 1 - if _pendingRemove.remaining > 0 then - removeNextItem() - else - -- we have removed all items that we want to remove, now do the equip - _pendingRemove = nil - tryEquipNextItem() - end - else - tryEquipNextItem() - end - end - - elseif _pendingEquip and _pendingEquip.destSlot then - -- waiting on an item swap to complete successfully so that we can go on to the next item - - -- inventory slot we're swapping to is still locked, can't continue yet - if IsInventoryItemLocked(_pendingEquip.destSlot) then return end - - if _pendingEquip.bag then - local _, _, locked = GetContainerItemInfo(_pendingEquip.bag, _pendingEquip.slot) - -- the bag slot we're swapping from is still locked, can't continue yet - if locked then return end - else - -- inventory slot we're swapping from is still locked, can't continue yet - if IsInventoryItemLocked(_pendingEquip.slot) then return end - end - - -- move on to the next item, this item is done or could not be swapped - - local item = _pendingEquip.itemsToEquip[_pendingEquip.destSlot] - local itemLink = GetInventoryItemLink("player", _pendingEquip.destSlot) - if itemLink then - local invItem = Amr.ParseItemLink(itemLink) - if invItem ~= nil then - local diff = countItemDifferences(item, invItem) - if diff == 0 then - _pendingEquip.doneSlots[_pendingEquip.destSlot] = true - end - end - end - - _pendingEquip.itemsToEquip[_pendingEquip.destSlot] = nil - _pendingEquip.destSlot = nil - _pendingEquip.bag = nil - _pendingEquip.slot = nil - - _pendingEquip.remaining = _pendingEquip.remaining - 1 - if _pendingEquip.remaining > 0 then - for slotId, item in pairs(_pendingEquip.itemsToEquip) do - _pendingEquip.nextSlot = slotId - break - end - tryEquipNextItem() - else - finishEquipGearSet() - end - - end -end - -local function startEquipGearSet(spec) +function beginEquipGearSet(spec, passes) local gear = Amr.db.char.GearSets[spec] if not gear then Amr:Print(L.GearEquipErrorEmpty) return end - + + -- ensure all our stored data is up to date local player = Amr:ExportCharacter() - local itemsToEquip = {} + local itemsToEquip = { + legendaries = {}, + weapons = {}, + rings = {}, + trinkets = {}, + others = {}, + blanks = {} + } local remaining = 0 - local usedItems = {} - local firstSlot = nil - - -- check for items that need to be equipped - for slotNum = 1, #Amr.SlotIds do - local slotId = Amr.SlotIds[slotNum] - + local usedItems = {} + + -- check for items that need to be equipped, do in a random order to try and defeat any unique constraint issues we might hit + local slots = {} + for i,s in ipairs(Amr.SlotIds) do + table.insert(slots, s) + end + shuffle(slots) + + for i,slotId in ipairs(slots) do + + -- we do stuff in batches that avoids most unique conflicts + local list = itemsToEquip.others + if slotId == 16 or slotId == 17 then + list = itemsToEquip.weapons + elseif slotId == 11 or slotId == 12 then + list = itemsToEquip.rings + elseif slotId == 13 or slotId == 14 then + list = itemsToEquip.trinkets + end + local old = player.Equipped[spec][slotId] - old = Amr.ParseItemLink(old) - local new = gear[slotId] + local prevRemaining = remaining if new then - local diff = countItemDifferences(old, new) - if diff < 1000 then - -- same item, see if inventory has one that is closer (e.g. a duplicate item with correct enchants/gems) - local bestLink, bestItem, bestDiff = Amr:FindMatchingItem(new, player, usedItems) - if bestDiff and bestDiff < diff then - itemsToEquip[slotId] = new + -- if the new thing is an artifact, only match the item id + local newItem = Item:CreateFromItemID(new.id) + local quality = newItem and newItem:GetItemQuality() or 0 + if quality == 6 then + if not old or new.id ~= old.id then + list[slotId] = new remaining = remaining + 1 end - else - itemsToEquip[slotId] = new - remaining = remaining + 1 + else + local diff = countItemDifferences(old, new) + if diff > 0 and diff < 1000 then + -- same item, see if inventory has one that is closer (e.g. a duplicate item with correct enchants/gems) + local bestItem, bestDiff = Amr:FindMatchingItem(new, player, usedItems) + if bestDiff and bestDiff < diff then + new = bestItem + diff = bestDiff + end + end + + if diff > 0 then + list[slotId] = new + remaining = remaining + 1 + end + end + else + -- need to remove this item + itemsToEquip.blanks[slotId] = {} + remaining = remaining + 1 + end + + if remaining > prevRemaining then + -- if we need to swap this slot, see if the old item is a legendary, add a step to remove those first to avoid conflicts + if old then + local oldItem = Item:CreateFromItemID(old.id) + if oldItem and oldItem:GetItemQuality() == 5 then + itemsToEquip.legendaries[slotId] = {} + end end end end + + if remaining > 0 then - if remaining > 0 then - -- if this is not our first try, then remove weapons before starting - local toRemove = {} - local removesRemaining = 0 - if _pendingEquip and _pendingEquip.tries > 0 then - for slotId, item in pairs(itemsToEquip) do - if slotId == 16 or slotId == 17 then - table.insert(toRemove, slotId) - removesRemaining = removesRemaining + 1 - end - end + if passes < 5 then + _pendingGearOps = {} + + if not Amr.IsEmpty(itemsToEquip.blanks) then + -- if gear set wants slots to be blank, do that first + table.insert(_pendingGearOps, { items = itemsToEquip.blanks, remove = true, label = "blanks" }) + end + if not Amr.IsEmpty(itemsToEquip.weapons) then + -- change weapons first: remove both, wait, then equip new ones + table.insert(_pendingGearOps, { items = itemsToEquip.weapons, remove = true, label = "remove weapons" }) + table.insert(_pendingGearOps, { items = itemsToEquip.weapons, wait = true, label = "equip weapons" }) + end + if not Amr.IsEmpty(itemsToEquip.legendaries) then + -- remove any legendaries, wait + table.insert(_pendingGearOps, { items = itemsToEquip.legendaries, remove = true, label = "remove legendaries" }) + end + if not Amr.IsEmpty(itemsToEquip.rings) then + -- remove both rings, wait, then equip new ones + table.insert(_pendingGearOps, { items = itemsToEquip.rings, remove = true, label = "remove rings" }) + table.insert(_pendingGearOps, { items = itemsToEquip.rings, wait = true, label = "equip rings" }) + end + if not Amr.IsEmpty(itemsToEquip.trinkets) then + -- remove both trinkets, wait, then equip new ones + table.insert(_pendingGearOps, { items = itemsToEquip.trinkets, remove = true, label = "remove trinkets" }) + table.insert(_pendingGearOps, { items = itemsToEquip.trinkets, wait = true, label = "equip trinkets" }) + end + if not Amr.IsEmpty(itemsToEquip.others) then + -- equip all other items, wait for completion + table.insert(_pendingGearOps, { items = itemsToEquip.others, wait = true, label = "equip others" }) + end + + -- make the last operation wait no matter what, before this gets called again to check if everything succeeded + _pendingGearOps[#_pendingGearOps].wait = true + + if not _gearOpWaiting then + _gearOpWaiting = { inventory = {} } + end + + _gearOpPasses = passes + _currentGearOp = _pendingGearOps[1] + initializeGearOp(_currentGearOp, spec, 1) + + processCurrentGearOp() + else + -- TODO: print message that gear set couldn't be equipped end - - _pendingEquip = { - tries = _pendingEquip and _pendingEquip.spec == spec and _pendingEquip.tries or 0, - spec = spec, - itemsToEquip = itemsToEquip, - remaining = remaining, - doneSlots = _pendingEquip and _pendingEquip.spec == spec and _pendingEquip.doneSlots or {}, - nextSlot = firstSlot - } - -- starting item - for slotId, item in pairs(_pendingEquip.itemsToEquip) do - _pendingEquip.nextSlot = slotId - break - end - - if removesRemaining > 0 then - _pendingRemove = { - slotsToRemove = toRemove, - remaining = removesRemaining - } - removeNextItem() - else - tryEquipNextItem() - end else - _pendingEquip = nil onEquipGearSetComplete() - end + end end local function onActiveTalentGroupChanged() @@ -821,7 +1044,7 @@ if currentSpec == _waitingForSpec or auto then -- spec is what we want, now equip the gear - startEquipGearSet(currentSpec) + beginEquipGearSet(currentSpec, 0) end _waitingForSpec = 0 @@ -856,7 +1079,7 @@ end end --- moves any gear in bags to the bank if not part of main or off spec gear set +-- moves any gear in bags to the bank if not part of a gear set function Amr:CleanBags() -- TODO: implement end @@ -874,8 +1097,12 @@ Amr:AddEventHandler("UNIT_INVENTORY_CHANGED", function(unitID) if unitID and unitID ~= "player" then return end + + -- don't update during a gear operation, wait until it is totally finished + if _pendingGearOps then return end + Amr:RefreshGearTab() end) - Amr:AddEventHandler("ITEM_UNLOCKED", onItemUnlocked) + Amr:AddEventHandler("ITEM_UNLOCKED", handleItemUnlocked) end