Files
ElvUIModernized/ElvUI/Modules/Bags/Sort.lua
T
2018-12-06 19:55:03 -06:00

837 lines
21 KiB
Lua

local E, L, V, P, G = unpack(ElvUI); --Import: Engine, Locales, PrivateDB, ProfileDB, GlobalDB
local B = E:GetModule("Bags");
local Search = LibStub("LibItemSearch-1.2");
local LIP = LibStub("ItemPrice-1.1");
--Cache global variables
--Lua functions
local ipairs, pairs, tonumber, select, unpack, pcall = ipairs, pairs, tonumber, select, unpack, pcall
local getn, tinsert, tremove, tsort, twipe = table.getn, table.insert, table.remove, table.sort, table.wipe
local floor, mod = math.floor, math.mod
local band = bit.band
local match, gmatch, find = string.match, string.gmatch, string.find
--WoW API / Variables
local ContainerIDToInventoryID = ContainerIDToInventoryID
local CursorHasItem = CursorHasItem
local GetAuctionItemClasses = GetAuctionItemClasses
local GetAuctionItemSubClasses = GetAuctionItemSubClasses
local GetContainerItemInfo = GetContainerItemInfo
local GetContainerItemLink = GetContainerItemLink
local GetContainerNumSlots = GetContainerNumSlots
local GetInventoryItemLink = GetInventoryItemLink
local GetItemInfo = GetItemInfo
local GetTime = GetTime
local PickupContainerItem = PickupContainerItem
local SplitContainerItem = SplitContainerItem
local ARMOR, EN = ARMOR
local bankBags = {BANK_CONTAINER}
local MAX_MOVE_TIME = 1.25
for i = NUM_BAG_SLOTS + 1, NUM_BAG_SLOTS + NUM_BANKBAGSLOTS do
tinsert(bankBags, i)
end
local playerBags = {}
for i = 0, NUM_BAG_SLOTS do
tinsert(playerBags, i)
end
local allBags = {}
for _, i in ipairs(playerBags) do
tinsert(allBags, i)
end
for _, i in ipairs(bankBags) do
tinsert(allBags, i)
end
local coreGroups = {
bank = bankBags,
bags = playerBags,
all = allBags,
}
local bagCache = {}
local bagIDs = {}
local bagQualities = {}
local bagStacks = {}
local bagMaxStacks = {}
local bagGroups = {}
local initialOrder = {}
local itemTypes, itemSubTypes = {}, {}
local bagSorted, bagLocked = {}, {}
local moves = {}
local targetItems = {}
local sourceUsed = {}
local targetSlots = {}
local specialtyBags = {}
local emptySlots = {}
local moveRetries = 0
local lastItemID, currentItemID, lockStop, lastDestination, lastMove
local moveTracker = {}
local inventorySlots = {
INVTYPE_AMMO = 0,
INVTYPE_HEAD = 1,
INVTYPE_NECK = 2,
INVTYPE_SHOULDER = 3,
INVTYPE_BODY = 4,
INVTYPE_CHEST = 5,
INVTYPE_ROBE = 5,
INVTYPE_WAIST = 6,
INVTYPE_LEGS = 7,
INVTYPE_FEET = 8,
INVTYPE_WRIST = 9,
INVTYPE_HAND = 10,
INVTYPE_FINGER = 11,
INVTYPE_TRINKET = 12,
INVTYPE_CLOAK = 13,
INVTYPE_WEAPON = 14,
INVTYPE_SHIELD = 15,
INVTYPE_2HWEAPON = 16,
INVTYPE_WEAPONMAINHAND = 18,
INVTYPE_WEAPONOFFHAND = 19,
INVTYPE_HOLDABLE = 20,
INVTYPE_RANGED = 21,
INVTYPE_THROWN = 22,
INVTYPE_RANGEDRIGHT = 23,
INVTYPE_RELIC = 24,
INVTYPE_TABARD = 25,
}
local safe = {
[BANK_CONTAINER] = true,
[0] = true
}
local frame = CreateFrame("Frame")
local t, WAIT_TIME = 0, 0.05
frame:SetScript("OnUpdate", function()
t = t + (arg1 or 0.01)
if t > WAIT_TIME then
t = 0
B:DoMoves()
end
end)
frame:Hide()
B.SortUpdateTimer = frame
local function BuildSortOrder()
for i, iType in ipairs({GetAuctionItemClasses()}) do
itemTypes[iType] = i
itemSubTypes[iType] = {}
for ii, isType in ipairs({GetAuctionItemSubClasses(i)}) do
itemSubTypes[iType][isType] = ii
end
end
end
local function UpdateLocation(from, to)
if (bagIDs[from] == bagIDs[to]) and (bagStacks[to] < bagMaxStacks[to]) then
local stackSize = bagMaxStacks[to]
if (bagStacks[to] + bagStacks[from]) > stackSize then
bagStacks[from] = bagStacks[from] - (stackSize - bagStacks[to])
bagStacks[to] = stackSize
else
bagStacks[to] = bagStacks[to] + bagStacks[from]
bagStacks[from] = nil
bagIDs[from] = nil
bagQualities[from] = nil
bagMaxStacks[from] = nil
end
else
bagIDs[from], bagIDs[to] = bagIDs[to], bagIDs[from]
bagQualities[from], bagQualities[to] = bagQualities[to], bagQualities[from]
bagStacks[from], bagStacks[to] = bagStacks[to], bagStacks[from]
bagMaxStacks[from], bagMaxStacks[to] = bagMaxStacks[to], bagMaxStacks[from]
end
end
local function PrimarySort(a, b)
local aName, aLink, _, aLvl = GetItemInfo(bagIDs[a])
local bName, bLink, _, bLvl = GetItemInfo(bagIDs[b])
local aPrice = LIP:GetSellValue(aLink)
local bPrice = LIP:GetSellValue(bLink)
if aLvl ~= bLvl and aLvl and bLvl then
return aLvl > bLvl
end
if aPrice ~= bPrice and aPrice and bPrice then
return aPrice > bPrice
end
if aName and bName then
return aName < bName
end
end
local function DefaultSort(a, b)
local aID = bagIDs[a]
local bID = bagIDs[b]
if (not aID) or (not bID) then return aID end
local aOrder, bOrder = initialOrder[a], initialOrder[b]
if aID == bID then
local aCount = bagStacks[a]
local bCount = bagStacks[b]
if aCount and bCount and aCount == bCount then
return aOrder < bOrder
elseif aCount and bCount then
return aCount < bCount
end
end
local _, _, _, _, aType, aSubType, _, aEquipLoc = GetItemInfo(aID)
local _, _, _, _, bType, bSubType, _, bEquipLoc = GetItemInfo(bID)
local aRarity, bRarity = bagQualities[a], bagQualities[b]
if aRarity ~= bRarity and aRarity and bRarity then
return aRarity > bRarity
end
local aItemClassId, aItemSubClassId = itemTypes[aType] or 99, itemSubTypes[aType] and itemSubTypes[aType][aSubType] or 99
local bItemClassId, bItemSubClassId = itemTypes[bType] or 99, itemSubTypes[bType] and itemSubTypes[bType][bSubType] or 99
if aItemClassId ~= bItemClassId then
return aItemClassId < bItemClassId
end
if aItemClassId == 1 or aItemClassId == 2 then
local aEquipLoc = inventorySlots[aEquipLoc] or -1
local bEquipLoc = inventorySlots[bEquipLoc] or -1
if aEquipLoc == bEquipLoc then
return PrimarySort(a, b)
end
if aEquipLoc and bEquipLoc then
return aEquipLoc < bEquipLoc
end
end
if (aItemClassId == bItemClassId) and (aItemSubClassId == bItemSubClassId) then
return PrimarySort(a, b)
end
return (aItemSubClassId or 99) < (bItemSubClassId or 99)
end
local function ReverseSort(a, b)
return DefaultSort(b, a)
end
local function UpdateSorted(source, destination)
for i, bs in pairs(bagSorted) do
if bs == source then
bagSorted[i] = destination
elseif bs == destination then
bagSorted[i] = source
end
end
end
local function ShouldMove(source, destination)
if destination == source then return end
if not bagIDs[source] then return end
if bagIDs[source] == bagIDs[destination] and bagStacks[source] == bagStacks[destination] then return end
return true
end
local function IterateForwards(bagList, i)
i = i + 1
local step = 1
for _, bag in ipairs(bagList) do
local slots = B:GetNumSlots(bag)
if i > slots + step then
step = step + slots
else
for slot = 1, slots do
if step == i then
return i, bag, slot
end
step = step + 1
end
end
end
end
local function IterateBackwards(bagList, i)
i = i + 1
local step = 1
for ii = getn(bagList), 1, -1 do
local bag = bagList[ii]
local slots = B:GetNumSlots(bag)
if i > slots + step then
step = step + slots
else
for slot = slots, 1, -1 do
if step == i then
return i, bag, slot
end
step = step + 1
end
end
end
end
function B.IterateBags(bagList, reverse)
return (reverse and IterateBackwards or IterateForwards), bagList, 0
end
function B:GetItemID(bag, slot)
local link = self:GetItemLink(bag, slot)
return link and tonumber(match(link, "item:(%d+)"))
end
function B:GetItemInfo(bag, slot)
return GetContainerItemInfo(bag, slot)
end
function B:GetItemLink(bag, slot)
return GetContainerItemLink(bag, slot)
end
function B:PickupItem(bag, slot)
currentItemID = self:GetItemID(bag, slot)
return PickupContainerItem(bag, slot)
end
function B:SplitItem(bag, slot, amount)
return SplitContainerItem(bag, slot, amount)
end
function B:GetNumSlots(bag)
if bag then
return GetContainerNumSlots(bag)
end
return 0
end
local function ConvertLinkToID(link)
if not link then return end
if tonumber(match(link, "item:(%d+)")) then
return tonumber(match(link, "item:(%d+)"))
end
end
local function DefaultCanMove()
return true
end
function B:Encode_BagSlot(bag, slot)
return (bag * 100) + slot
end
function B:Decode_BagSlot(int)
return floor(int / 100), mod(int, 100)
end
function B:IsPartial(bag, slot)
local bagSlot = B:Encode_BagSlot(bag, slot)
return ((bagMaxStacks[bagSlot] or 0) - (bagStacks[bagSlot] or 0)) > 0
end
function B:EncodeMove(source, target)
return (source * 10000) + target
end
function B:DecodeMove(move)
local s = floor(move / 10000)
local t = mod(move, 10000)
s = (t > 9000) and (s + 1) or s
t = (t > 9000) and (t - 10000) or t
return s, t
end
function B:AddMove(source, destination)
UpdateLocation(source, destination)
tinsert(moves, 1, B:EncodeMove(source, destination))
end
function B:ScanBags()
for _, bag, slot in B.IterateBags(allBags) do
local bagSlot = B:Encode_BagSlot(bag, slot)
local itemID = ConvertLinkToID(B:GetItemLink(bag, slot))
if itemID then
local _, _, rarity, _, _, _, stackCount = GetItemInfo(itemID)
local _, itemCount = B:GetItemInfo(bag, slot)
bagMaxStacks[bagSlot] = stackCount
bagIDs[bagSlot] = itemID
bagQualities[bagSlot] = rarity
bagStacks[bagSlot] = itemCount
end
end
end
function B:IsSpecialtyBag(bagID)
if safe[bagID] then return false end
local inventorySlot = ContainerIDToInventoryID(bagID)
if not inventorySlot then return false end
local bag = GetInventoryItemLink("player", inventorySlot)
if not bag then return false end
local family = GetItemFamily(bag, true)
if family == 0 or family == nil then return false end
return family
end
function B:CanItemGoInBag(bag, slot, targetBag)
local item = bagIDs[B:Encode_BagSlot(bag, slot)]
local itemFamily = GetItemFamily(item)
if itemFamily and itemFamily > 0 then
local equipSlot = select(8, GetItemInfo(item))
if equipSlot == "INVTYPE_QUIVER" then
itemFamily = 1
end
end
local bagFamily = GetItemFamily(GetInventoryItemLink("player", ContainerIDToInventoryID(targetBag)), true)
if itemFamily then
return (bagFamily == 0) or band(itemFamily, bagFamily) > 0
else
return false
end
end
function B.Compress(...)
for i = 1, arg.n do
local bags = arg[i]
B.Stack(bags, bags, B.IsPartial)
end
end
function B.Stack(sourceBags, targetBags, canMove)
if not canMove then canMove = DefaultCanMove end
for _, bag, slot in B.IterateBags(targetBags, nil, "deposit") do
local bagSlot = B:Encode_BagSlot(bag, slot)
local itemID = bagIDs[bagSlot]
if itemID and (bagStacks[bagSlot] ~= bagMaxStacks[bagSlot]) then
targetItems[itemID] = (targetItems[itemID] or 0) + 1
tinsert(targetSlots, bagSlot)
end
end
for _, bag, slot in B.IterateBags(sourceBags, true, "withdraw") do
local sourceSlot = B:Encode_BagSlot(bag, slot)
local itemID = bagIDs[sourceSlot]
if itemID and targetItems[itemID] and canMove(itemID, bag, slot) then
for i = getn(targetSlots), 1, -1 do
local targetedSlot = targetSlots[i]
if bagIDs[sourceSlot] and bagIDs[targetedSlot] == itemID and targetedSlot ~= sourceSlot and not (bagStacks[targetedSlot] == bagMaxStacks[targetedSlot]) and not sourceUsed[targetedSlot] then
B:AddMove(sourceSlot, targetedSlot)
sourceUsed[sourceSlot] = true
if bagStacks[targetedSlot] == bagMaxStacks[targetedSlot] then
targetItems[itemID] = (targetItems[itemID] > 1) and (targetItems[itemID] - 1) or nil
end
if bagStacks[sourceSlot] == 0 then
targetItems[itemID] = (targetItems[itemID] > 1) and (targetItems[itemID] - 1) or nil
break
end
if not targetItems[itemID] then break end
end
end
end
end
twipe(targetItems)
twipe(targetSlots)
twipe(sourceUsed)
end
local blackListedSlots = {}
local blackList = {}
local blackListQueries = {}
local function buildBlacklist(arg)
for entry in pairs(arg) do
local itemName = GetItemInfo(entry)
if itemName then
blackList[itemName] = true
elseif entry ~= "" then
if find(entry, "%[") and find(entry, "%]") then
--For some reason the entry was not treated as a valid item. Extract the item name.
entry = match(entry, "%[(.*)%]")
end
tinsert(blackListQueries, entry)
end
end
end
function B.Sort(bags, sorter, invertDirection)
if not sorter then sorter = invertDirection and ReverseSort or DefaultSort end
if not itemTypes then BuildSortOrder() end
--Wipe tables before we begin
twipe(blackList)
twipe(blackListQueries)
twipe(blackListedSlots)
--Build blacklist of items based on the profile and global list
buildBlacklist(B.db.ignoredItems)
buildBlacklist(E.global.bags.ignoredItems)
for i, bag, slot in B.IterateBags(bags, nil, "both") do
local bagSlot = B:Encode_BagSlot(bag, slot)
local link = B:GetItemLink(bag, slot)
local id = B:GetItemID(bag, slot)
if link then
if blackList[GetItemInfo(id)] then
blackListedSlots[bagSlot] = true
end
if not blackListedSlots[bagSlot] then
for _, itemsearchquery in pairs(blackListQueries) do
local success, result = pcall(Search.Matches, Search, link, itemsearchquery)
if success and result then
blackListedSlots[bagSlot] = blackListedSlots[bagSlot] or result
break
end
end
end
end
if not blackListedSlots[bagSlot] then
initialOrder[bagSlot] = i
tinsert(bagSorted, bagSlot)
end
end
tsort(bagSorted, sorter)
local passNeeded = true
while passNeeded do
passNeeded = false
local i = 1
for _, bag, slot in B.IterateBags(bags, nil, "both") do
local destination = B:Encode_BagSlot(bag, slot)
local source = bagSorted[i]
if not blackListedSlots[destination] then
if ShouldMove(source, destination) then
if not (bagLocked[source] or bagLocked[destination]) then
B:AddMove(source, destination)
UpdateSorted(source, destination)
bagLocked[source] = true
bagLocked[destination] = true
else
passNeeded = true
end
end
i = i + 1
end
end
twipe(bagLocked)
end
twipe(bagSorted)
twipe(initialOrder)
end
function B.FillBags(from, to)
B.Stack(from, to)
for _, bag in ipairs(to) do
if B:IsSpecialtyBag(bag) then
tinsert(specialtyBags, bag)
end
end
if getn(specialtyBags) > 0 then
B:Fill(from, specialtyBags)
end
B.Fill(from, to)
twipe(specialtyBags)
end
function B.Fill(sourceBags, targetBags, reverse, canMove)
if not canMove then canMove = DefaultCanMove end
--Wipe tables before we begin
twipe(blackList)
twipe(blackListedSlots)
--Build blacklist of items based on the profile and global list
buildBlacklist(B.db.ignoredItems)
buildBlacklist(E.global.bags.ignoredItems)
for _, bag, slot in B.IterateBags(targetBags, reverse, "deposit") do
local bagSlot = B:Encode_BagSlot(bag, slot)
if not bagIDs[bagSlot] then
tinsert(emptySlots, bagSlot)
end
end
for _, bag, slot in B.IterateBags(sourceBags, not reverse, "withdraw") do
if getn(emptySlots) == 0 then break end
local bagSlot = B:Encode_BagSlot(bag, slot)
local targetBag = B:Decode_BagSlot(emptySlots[1])
local id = B:GetItemID(bag, slot)
if id and blackList[GetItemInfo(id)] then
blackListedSlots[bagSlot] = true
end
if bagIDs[bagSlot] and B:CanItemGoInBag(bag, slot, targetBag) and canMove(bagIDs[bagSlot], bag, slot) and not blackListedSlots[bagSlot] then
B:AddMove(bagSlot, tremove(emptySlots, 1))
end
end
twipe(emptySlots)
end
function B.SortBags(...)
for i = 1, arg.n do
local bags = arg[i]
for _, slotNum in ipairs(bags) do
local bagType = B:IsSpecialtyBag(slotNum)
if bagType == false then bagType = "Normal" end
if not bagCache[bagType] then bagCache[bagType] = {} end
tinsert(bagCache[bagType], slotNum)
end
for bagType, sortedBags in pairs(bagCache) do
if bagType ~= "Normal" then
B.Stack(sortedBags, sortedBags, B.IsPartial)
B.Stack(bagCache.Normal, sortedBags)
B.Fill(bagCache.Normal, sortedBags, B.db.sortInverted)
B.Sort(sortedBags, nil, B.db.sortInverted)
twipe(sortedBags)
end
end
if bagCache.Normal then
B.Stack(bagCache.Normal, bagCache.Normal, B.IsPartial)
B.Sort(bagCache.Normal, nil, B.db.sortInverted)
twipe(bagCache.Normal)
end
twipe(bagCache)
twipe(bagGroups)
end
end
function B:StartStacking()
twipe(bagMaxStacks)
twipe(bagStacks)
twipe(bagIDs)
twipe(bagQualities)
twipe(moveTracker)
if getn(moves) > 0 then
self.SortUpdateTimer:Show()
else
B:StopStacking()
end
end
local function RegisterUpdateDelayed()
local shouldUpdateFade
for _, bagFrame in pairs(B.BagFrames) do
if bagFrame.registerUpdate then
bagFrame:UpdateAllSlots()
bagFrame.registerUpdate = nil -- call update and re-register events, keep this after UpdateAllSlots
shouldUpdateFade = true -- we should refresh the bag search after sorting
bagFrame:RegisterEvent("BAG_UPDATE")
bagFrame:RegisterEvent("BAG_UPDATE_COOLDOWN")
for _, event in pairs(bagFrame.events) do
bagFrame:RegisterEvent(event)
end
end
end
if shouldUpdateFade then
B:RefreshSearch() -- this will clear the bag lock look during a sort
end
end
function B:StopStacking(message, noUpdate)
twipe(moves)
twipe(moveTracker)
moveRetries, lastItemID, lockStop, lastDestination, lastMove = 0, nil, nil, nil, nil, nil
self.SortUpdateTimer:Hide()
if not noUpdate then
--Add a delayed update call, as BAG_UPDATE fires slightly delayed
-- and we don't want the last few unneeded updates to be catched
E:Delay(0.6, RegisterUpdateDelayed)
end
if message then
E:Print(message)
end
end
function B:DoMove(move)
if CursorHasItem() then
return false, "cursorhasitem"
end
local source, target = B:DecodeMove(move)
local sourceBag, sourceSlot = B:Decode_BagSlot(source)
local targetBag, targetSlot = B:Decode_BagSlot(target)
local _, sourceCount, sourceLocked = B:GetItemInfo(sourceBag, sourceSlot)
local _, targetCount, targetLocked = B:GetItemInfo(targetBag, targetSlot)
if sourceLocked or targetLocked then
return false, "source/target_locked"
end
local sourceItemID = self:GetItemID(sourceBag, sourceSlot)
local targetItemID = self:GetItemID(targetBag, targetSlot)
if not sourceItemID then
if moveTracker[source] then
return false, "move incomplete"
else
return B:StopStacking(L["Confused.. Try Again!"])
end
end
local _, _, _, _, _, _, stackSize = GetItemInfo(sourceItemID)
if (sourceItemID == targetItemID) and (targetCount ~= stackSize) and ((targetCount + sourceCount) > stackSize) then
B:SplitItem(sourceBag, sourceSlot, stackSize - targetCount)
else
B:PickupItem(sourceBag, sourceSlot)
end
if CursorHasItem() then
B:PickupItem(targetBag, targetSlot)
end
return true, sourceItemID, source, targetItemID, target
end
function B:DoMoves()
if CursorHasItem() and currentItemID then
if lastItemID ~= currentItemID then
return B:StopStacking(L["Confused.. Try Again!"])
end
if moveRetries < 100 then
local targetBag, targetSlot = self:Decode_BagSlot(lastDestination)
local _, _, targetLocked = self:GetItemInfo(targetBag, targetSlot)
if not targetLocked then
self:PickupItem(targetBag, targetSlot)
WAIT_TIME = 0.1
lockStop = GetTime()
moveRetries = moveRetries + 1
return
end
end
end
if lockStop then
for slot, itemID in pairs(moveTracker) do
local actualItemID = self:GetItemID(self:Decode_BagSlot(slot))
if actualItemID ~= itemID then
WAIT_TIME = 0.1
if (GetTime() - lockStop) > MAX_MOVE_TIME then
if lastMove and moveRetries < 100 then
local success, moveID, moveSource, targetID, moveTarget = self:DoMove(lastMove)
WAIT_TIME = 0.1
if not success then
lockStop = GetTime()
moveRetries = moveRetries + 1
return
end
moveTracker[moveSource] = targetID
moveTracker[moveTarget] = moveID
lastDestination = moveTarget
-- lastMove = moves[i] --Where does "i" come from???
lastItemID = moveID
-- tremove(moves, i) --Where does "i" come from???
return
end
B:StopStacking()
return
end
return --give processing time to happen
end
moveTracker[slot] = nil
end
end
lastItemID, lockStop, lastDestination, lastMove = nil, nil, nil, nil
twipe(moveTracker)
local success, moveID, targetID, moveSource, moveTarget
if getn(moves) > 0 then
for i = getn(moves), 1, -1 do
success, moveID, moveSource, targetID, moveTarget = B:DoMove(moves[i])
if not success then
WAIT_TIME = 0.1
lockStop = GetTime()
return
end
moveTracker[moveSource] = targetID
moveTracker[moveTarget] = moveID
lastDestination = moveTarget
lastMove = moves[i]
lastItemID = moveID
tremove(moves, i)
if moves[i - 1] then
WAIT_TIME = 0
return
end
end
end
B:StopStacking()
end
function B:GetGroup(id)
if match(id, "^[-%d,]+$") then
local bags = {}
for b in gmatch(id, "-?%d+") do
tinsert(bags, tonumber(b))
end
return bags
end
return coreGroups[id]
end
function B:CommandDecorator(func, groupsDefaults)
return function(groups)
if self.SortUpdateTimer:IsShown() then
B:StopStacking(L["Already Running.. Bailing Out!"], true)
return
end
twipe(bagGroups)
if not groups or getn(groups) == 0 then
groups = groupsDefaults
end
for bags in gmatch(groups or "", "%S+") do
bags = B:GetGroup(bags)
if bags then
tinsert(bagGroups, bags)
end
end
B:ScanBags()
if func(unpack(bagGroups)) == false then
return
end
twipe(bagGroups)
B:StartStacking()
end
end