initial commit
This commit is contained in:
180
Modules/Bags/BagBar.lua
Normal file
180
Modules/Bags/BagBar.lua
Normal file
@@ -0,0 +1,180 @@
|
||||
local E, L, V, P, G = unpack(select(2, ...)); --Import: Engine, Locales, PrivateDB, ProfileDB, GlobalDB
|
||||
local B = E:GetModule('Bags')
|
||||
|
||||
local _G = _G
|
||||
local ipairs = ipairs
|
||||
local unpack = unpack
|
||||
local tinsert = tinsert
|
||||
local CreateFrame = CreateFrame
|
||||
local GetBagSlotFlag = GetBagSlotFlag
|
||||
local RegisterStateDriver = RegisterStateDriver
|
||||
local NUM_BAG_FRAMES = NUM_BAG_FRAMES
|
||||
local LE_BAG_FILTER_FLAG_EQUIPMENT = LE_BAG_FILTER_FLAG_EQUIPMENT
|
||||
local NUM_LE_BAG_FILTER_FLAGS = NUM_LE_BAG_FILTER_FLAGS
|
||||
|
||||
local function OnEnter()
|
||||
if not E.db.bags.bagBar.mouseover then return; end
|
||||
E:UIFrameFadeIn(B.BagBar, 0.2, B.BagBar:GetAlpha(), 1)
|
||||
end
|
||||
|
||||
local function OnLeave()
|
||||
if not E.db.bags.bagBar.mouseover then return; end
|
||||
E:UIFrameFadeOut(B.BagBar, 0.2, B.BagBar:GetAlpha(), 0)
|
||||
end
|
||||
|
||||
function B:SkinBag(bag)
|
||||
local icon = _G[bag:GetName()..'IconTexture']
|
||||
bag.oldTex = icon:GetTexture()
|
||||
|
||||
bag:StripTextures()
|
||||
bag:CreateBackdrop()
|
||||
bag:StyleButton(true)
|
||||
bag.IconBorder:Kill()
|
||||
bag.backdrop:SetAllPoints()
|
||||
|
||||
icon:SetInside()
|
||||
icon:SetTexture(bag.oldTex)
|
||||
icon:SetTexCoord(unpack(E.TexCoords))
|
||||
end
|
||||
|
||||
function B:SizeAndPositionBagBar()
|
||||
if not B.BagBar then return; end
|
||||
|
||||
local buttonSpacing = E.db.bags.bagBar.spacing
|
||||
local backdropSpacing = E.db.bags.bagBar.backdropSpacing
|
||||
local bagBarSize = E.db.bags.bagBar.size
|
||||
local showBackdrop = E.db.bags.bagBar.showBackdrop
|
||||
local growthDirection = E.db.bags.bagBar.growthDirection
|
||||
local sortDirection = E.db.bags.bagBar.sortDirection
|
||||
|
||||
local visibility = E.db.bags.bagBar.visibility
|
||||
if visibility and visibility:match('[\n\r]') then
|
||||
visibility = visibility:gsub('[\n\r]','')
|
||||
end
|
||||
|
||||
RegisterStateDriver(B.BagBar, 'visibility', visibility)
|
||||
B.BagBar:SetAlpha(E.db.bags.bagBar.mouseover and 0 or 1)
|
||||
B.BagBar.backdrop:SetShown(showBackdrop)
|
||||
|
||||
local bdpSpacing = (showBackdrop and backdropSpacing + E.Border) or 0
|
||||
local btnSpacing = (buttonSpacing + E.Border)
|
||||
|
||||
for i, button in ipairs(B.BagBar.buttons) do
|
||||
local prevButton = B.BagBar.buttons[i-1]
|
||||
button:Size(bagBarSize, bagBarSize)
|
||||
button:ClearAllPoints()
|
||||
|
||||
if growthDirection == 'HORIZONTAL' and sortDirection == 'ASCENDING' then
|
||||
if i == 1 then
|
||||
button:Point('LEFT', B.BagBar, 'LEFT', bdpSpacing, 0)
|
||||
elseif prevButton then
|
||||
button:Point('LEFT', prevButton, 'RIGHT', btnSpacing, 0)
|
||||
end
|
||||
elseif growthDirection == 'VERTICAL' and sortDirection == 'ASCENDING' then
|
||||
if i == 1 then
|
||||
button:Point('TOP', B.BagBar, 'TOP', 0, -bdpSpacing)
|
||||
elseif prevButton then
|
||||
button:Point('TOP', prevButton, 'BOTTOM', 0, -btnSpacing)
|
||||
end
|
||||
elseif growthDirection == 'HORIZONTAL' and sortDirection == 'DESCENDING' then
|
||||
if i == 1 then
|
||||
button:Point('RIGHT', B.BagBar, 'RIGHT', -bdpSpacing, 0)
|
||||
elseif prevButton then
|
||||
button:Point('RIGHT', prevButton, 'LEFT', -btnSpacing, 0)
|
||||
end
|
||||
else
|
||||
if i == 1 then
|
||||
button:Point('BOTTOM', B.BagBar, 'BOTTOM', 0, bdpSpacing)
|
||||
elseif prevButton then
|
||||
button:Point('BOTTOM', prevButton, 'TOP', 0, btnSpacing)
|
||||
end
|
||||
end
|
||||
for j = LE_BAG_FILTER_FLAG_EQUIPMENT, NUM_LE_BAG_FILTER_FLAGS do
|
||||
local active = GetBagSlotFlag(i - 1, j)
|
||||
if active then
|
||||
button.ElvUIFilterIcon:SetTexture(B.BAG_FILTER_ICONS[j])
|
||||
button.ElvUIFilterIcon:SetShown(E.db.bags.showAssignedIcon)
|
||||
|
||||
local r, g, b, a = unpack(B.AssignmentColors[j])
|
||||
|
||||
button.forcedBorderColors = {r, g, b, a}
|
||||
button.backdrop:SetBackdropBorderColor(r, g, b, a)
|
||||
break -- this loop
|
||||
else
|
||||
button.ElvUIFilterIcon:SetShown(false)
|
||||
|
||||
button.forcedBorderColors = nil
|
||||
button.backdrop:SetBackdropBorderColor(unpack(E.media.bordercolor))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local btnSize = bagBarSize * (NUM_BAG_FRAMES + 1)
|
||||
local btnSpace = btnSpacing * NUM_BAG_FRAMES
|
||||
local bdpDoubled = bdpSpacing * 2
|
||||
|
||||
if growthDirection == 'HORIZONTAL' then
|
||||
B.BagBar:Width(btnSize + btnSpace + bdpDoubled)
|
||||
B.BagBar:Height(bagBarSize + bdpDoubled)
|
||||
else
|
||||
B.BagBar:Height(btnSize + btnSpace + bdpDoubled)
|
||||
B.BagBar:Width(bagBarSize + bdpDoubled)
|
||||
end
|
||||
end
|
||||
|
||||
function B:LoadBagBar()
|
||||
if not E.private.bags.bagBar then return end
|
||||
|
||||
B.BagBar = CreateFrame('Frame', 'ElvUIBags', E.UIParent)
|
||||
B.BagBar:Point('TOPRIGHT', _G.RightChatPanel, 'TOPLEFT', -4, 0)
|
||||
B.BagBar.buttons = {}
|
||||
B.BagBar:CreateBackdrop(E.db.bags.transparent and 'Transparent')
|
||||
B.BagBar.backdrop:SetAllPoints()
|
||||
B.BagBar:EnableMouse(true)
|
||||
B.BagBar:SetScript('OnEnter', OnEnter)
|
||||
B.BagBar:SetScript('OnLeave', OnLeave)
|
||||
|
||||
_G.MainMenuBarBackpackButton:SetParent(B.BagBar)
|
||||
_G.MainMenuBarBackpackButton:ClearAllPoints()
|
||||
_G.MainMenuBarBackpackButtonCount:FontTemplate(nil, 10)
|
||||
_G.MainMenuBarBackpackButtonCount:ClearAllPoints()
|
||||
_G.MainMenuBarBackpackButtonCount:Point('BOTTOMRIGHT', _G.MainMenuBarBackpackButton, 'BOTTOMRIGHT', -1, 4)
|
||||
_G.MainMenuBarBackpackButton:HookScript('OnEnter', OnEnter)
|
||||
_G.MainMenuBarBackpackButton:HookScript('OnLeave', OnLeave)
|
||||
|
||||
tinsert(B.BagBar.buttons, _G.MainMenuBarBackpackButton)
|
||||
B:SkinBag(_G.MainMenuBarBackpackButton)
|
||||
|
||||
for i = 0, NUM_BAG_FRAMES-1 do
|
||||
local b = _G['CharacterBag'..i..'Slot']
|
||||
b:SetParent(B.BagBar)
|
||||
b:HookScript('OnEnter', OnEnter)
|
||||
b:HookScript('OnLeave', OnLeave)
|
||||
|
||||
B:SkinBag(b)
|
||||
tinsert(B.BagBar.buttons, b)
|
||||
end
|
||||
|
||||
--Item assignment
|
||||
for i, bagButton in ipairs(B.BagBar.buttons) do
|
||||
B:CreateFilterIcon(bagButton)
|
||||
bagButton.id = (i - 1)
|
||||
|
||||
bagButton:SetScript('OnClick', function(holder, button)
|
||||
if button == 'RightButton' then
|
||||
B.AssignBagDropdown.holder = holder
|
||||
_G.ToggleDropDownMenu(1, nil, B.AssignBagDropdown, 'cursor')
|
||||
else
|
||||
if holder.id == 0 then
|
||||
_G.MainMenuBarBackpackButton_OnClick(holder)
|
||||
else
|
||||
_G.BagSlotButton_OnClick(holder)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
B:SizeAndPositionBagBar()
|
||||
E:CreateMover(B.BagBar, 'BagsMover', L["Bags"], nil, nil, nil, nil, nil, 'bags,general')
|
||||
B:RegisterEvent('BAG_SLOT_FLAGS_UPDATED', 'SizeAndPositionBagBar')
|
||||
end
|
||||
2461
Modules/Bags/Bags.lua
Normal file
2461
Modules/Bags/Bags.lua
Normal file
File diff suppressed because it is too large
Load Diff
5
Modules/Bags/Load_Bags.xml
Normal file
5
Modules/Bags/Load_Bags.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<Ui xmlns='http://www.blizzard.com/wow/ui/'>
|
||||
<Script file='Bags.lua'/>
|
||||
<Script file='BagBar.lua'/>
|
||||
<Script file='Sort.lua'/>
|
||||
</Ui>
|
||||
949
Modules/Bags/Sort.lua
Normal file
949
Modules/Bags/Sort.lua
Normal file
@@ -0,0 +1,949 @@
|
||||
local E, L, V, P, G = unpack(select(2, ...)); --Import: Engine, Locales, PrivateDB, ProfileDB, GlobalDB
|
||||
local B = E:GetModule('Bags')
|
||||
local Search = E.Libs.ItemSearch
|
||||
|
||||
local ipairs, pairs, select, unpack, pcall = ipairs, pairs, select, unpack, pcall
|
||||
local strmatch, gmatch, strfind = strmatch, gmatch, strfind
|
||||
local tinsert, tremove, sort, wipe = tinsert, tremove, sort, wipe
|
||||
local tonumber, floor, band = tonumber, floor, bit.band
|
||||
|
||||
local ContainerIDToInventoryID = ContainerIDToInventoryID
|
||||
local GetContainerItemID = GetContainerItemID
|
||||
local GetContainerItemInfo = GetContainerItemInfo
|
||||
local GetContainerItemLink = GetContainerItemLink
|
||||
local GetContainerNumFreeSlots = GetContainerNumFreeSlots
|
||||
local GetContainerNumSlots = GetContainerNumSlots
|
||||
local GetCurrentGuildBankTab = GetCurrentGuildBankTab
|
||||
local GetCursorInfo = GetCursorInfo
|
||||
local GetGuildBankItemInfo = GetGuildBankItemInfo
|
||||
local GetGuildBankItemLink = GetGuildBankItemLink
|
||||
local GetGuildBankTabInfo = GetGuildBankTabInfo
|
||||
local GetInventoryItemLink = GetInventoryItemLink
|
||||
local GetItemFamily = GetItemFamily
|
||||
local GetItemInfo = GetItemInfo
|
||||
local GetTime = GetTime
|
||||
local InCombatLockdown = InCombatLockdown
|
||||
local PickupContainerItem = PickupContainerItem
|
||||
local PickupGuildBankItem = PickupGuildBankItem
|
||||
local QueryGuildBankTab = QueryGuildBankTab
|
||||
local SplitContainerItem = SplitContainerItem
|
||||
local SplitGuildBankItem = SplitGuildBankItem
|
||||
|
||||
local C_PetJournalGetPetInfoBySpeciesID = C_PetJournal.GetPetInfoBySpeciesID
|
||||
local LE_ITEM_CLASS_ARMOR = LE_ITEM_CLASS_ARMOR
|
||||
local LE_ITEM_CLASS_WEAPON = LE_ITEM_CLASS_WEAPON
|
||||
|
||||
local guildBags = {51,52,53,54,55,56,57,58}
|
||||
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
|
||||
|
||||
for _,i in ipairs(guildBags) do
|
||||
tinsert(allBags, i)
|
||||
end
|
||||
|
||||
local coreGroups = {
|
||||
guild = guildBags,
|
||||
bank = bankBags,
|
||||
bags = playerBags,
|
||||
all = allBags,
|
||||
}
|
||||
|
||||
local bagCache = {}
|
||||
local bagIDs = {}
|
||||
local bagQualities = {}
|
||||
local bagPetIDs = {}
|
||||
local bagStacks = {}
|
||||
local bagMaxStacks = {}
|
||||
local bagGroups = {}
|
||||
local initialOrder = {}
|
||||
local bagSorted, bagLocked = {}, {}
|
||||
local bagRole
|
||||
local moves = {}
|
||||
local targetItems = {}
|
||||
local sourceUsed = {}
|
||||
local targetSlots = {}
|
||||
local specialtyBags = {}
|
||||
local emptySlots = {}
|
||||
|
||||
local moveRetries = 0
|
||||
local lastItemID, 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 conjured_items = {
|
||||
[5512] = true, -- Healthstone
|
||||
[162518] = true, -- Mystical Flask
|
||||
[113509] = true, -- Conjured Mana Bun
|
||||
}
|
||||
|
||||
local safe = {
|
||||
[BANK_CONTAINER] = true,
|
||||
[0] = true
|
||||
}
|
||||
|
||||
local WAIT_TIME = 0.05
|
||||
do
|
||||
local t = 0
|
||||
local frame = CreateFrame('Frame')
|
||||
frame:SetScript('OnUpdate', function(_, elapsed)
|
||||
t = t + (elapsed or 0.01)
|
||||
if t > WAIT_TIME then
|
||||
t = 0
|
||||
B:DoMoves()
|
||||
end
|
||||
end)
|
||||
frame:Hide()
|
||||
B.SortUpdateTimer = frame
|
||||
end
|
||||
|
||||
local function IsGuildBankBag(bagid)
|
||||
return bagid > 50 and bagid <= 58
|
||||
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, _, _, aLvl, _, _, _, _, _, _, aPrice = GetItemInfo(bagIDs[a])
|
||||
local bName, _, _, bLvl, _, _, _, _, _, _, bPrice = GetItemInfo(bagIDs[b])
|
||||
|
||||
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
|
||||
|
||||
if bagPetIDs[a] and bagPetIDs[b] then
|
||||
local aName, _, aType = C_PetJournalGetPetInfoBySpeciesID(aID)
|
||||
local bName, _, bType = C_PetJournalGetPetInfoBySpeciesID(bID)
|
||||
|
||||
if aType and bType and aType ~= bType then
|
||||
return aType > bType
|
||||
end
|
||||
|
||||
if aName and bName and aName ~= bName then
|
||||
return aName < bName
|
||||
end
|
||||
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 _, _, _, _, _, _, _, _, aEquipLoc, _, _, aItemClassId, aItemSubClassId = GetItemInfo(aID)
|
||||
local _, _, _, _, _, _, _, _, bEquipLoc, _, _, bItemClassId, bItemSubClassId = GetItemInfo(bID)
|
||||
|
||||
local aRarity, bRarity = bagQualities[a], bagQualities[b]
|
||||
|
||||
if bagPetIDs[a] then
|
||||
aRarity = 1
|
||||
end
|
||||
|
||||
if bagPetIDs[b] then
|
||||
bRarity = 1
|
||||
end
|
||||
|
||||
if conjured_items[aID] then
|
||||
aRarity = -99
|
||||
end
|
||||
|
||||
if conjured_items[bID] then
|
||||
bRarity = -99
|
||||
end
|
||||
|
||||
if aRarity ~= bRarity and aRarity and bRarity then
|
||||
return aRarity > bRarity
|
||||
end
|
||||
|
||||
if aItemClassId ~= bItemClassId then
|
||||
return (aItemClassId or 99) < (bItemClassId or 99)
|
||||
end
|
||||
|
||||
if aItemClassId == LE_ITEM_CLASS_ARMOR or aItemClassId == LE_ITEM_CLASS_WEAPON then
|
||||
aEquipLoc = inventorySlots[aEquipLoc] or -1
|
||||
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, bagRole)
|
||||
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
|
||||
bagRole = nil
|
||||
end
|
||||
|
||||
local function IterateBackwards(bagList, i)
|
||||
i = i + 1
|
||||
local step = 1
|
||||
for ii = #bagList, 1, -1 do
|
||||
local bag = bagList[ii]
|
||||
local slots = B:GetNumSlots(bag, bagRole)
|
||||
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
|
||||
bagRole = nil
|
||||
end
|
||||
|
||||
function B:IterateBags(bagList, reverse, role)
|
||||
bagRole = role
|
||||
return (reverse and IterateBackwards or IterateForwards), bagList, 0
|
||||
end
|
||||
|
||||
function B:GetItemLink(bag, slot)
|
||||
if IsGuildBankBag(bag) then
|
||||
return GetGuildBankItemLink(bag - 50, slot)
|
||||
else
|
||||
return GetContainerItemLink(bag, slot)
|
||||
end
|
||||
end
|
||||
|
||||
function B:GetItemID(bag, slot)
|
||||
if IsGuildBankBag(bag) then
|
||||
local link = B:GetItemLink(bag, slot)
|
||||
return link and tonumber(strmatch(link, 'item:(%d+)'))
|
||||
else
|
||||
return GetContainerItemID(bag, slot)
|
||||
end
|
||||
end
|
||||
|
||||
function B:GetItemInfo(bag, slot)
|
||||
if IsGuildBankBag(bag) then
|
||||
return GetGuildBankItemInfo(bag - 50, slot)
|
||||
else
|
||||
return GetContainerItemInfo(bag, slot)
|
||||
end
|
||||
end
|
||||
|
||||
function B:PickupItem(bag, slot)
|
||||
if IsGuildBankBag(bag) then
|
||||
return PickupGuildBankItem(bag - 50, slot)
|
||||
else
|
||||
return PickupContainerItem(bag, slot)
|
||||
end
|
||||
end
|
||||
|
||||
function B:SplitItem(bag, slot, amount)
|
||||
if IsGuildBankBag(bag) then
|
||||
return SplitGuildBankItem(bag - 50, slot, amount)
|
||||
else
|
||||
return SplitContainerItem(bag, slot, amount)
|
||||
end
|
||||
end
|
||||
|
||||
function B:GetNumSlots(bag)
|
||||
if IsGuildBankBag(bag) then
|
||||
local name, _, canView = GetGuildBankTabInfo(bag - 50)
|
||||
if name and canView then
|
||||
return 98
|
||||
end
|
||||
else
|
||||
return GetContainerNumSlots(bag)
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
function B:ConvertLinkToID(link)
|
||||
if not link then return end
|
||||
|
||||
local item = strmatch(link, 'item:(%d+)')
|
||||
if item then return tonumber(item) end
|
||||
|
||||
local ks = strmatch(link, 'keystone:(%d+)')
|
||||
if ks then return tonumber(ks), nil, true end
|
||||
|
||||
local bp = strmatch(link, 'battlepet:(%d+)')
|
||||
if bp then return tonumber(bp), true 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), 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 = 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 itemLink = B:GetItemLink(bag, slot)
|
||||
local itemID, isBattlePet, isKeystone = B:ConvertLinkToID(itemLink)
|
||||
if itemID then
|
||||
if isBattlePet then
|
||||
bagPetIDs[bagSlot] = itemID
|
||||
bagMaxStacks[bagSlot] = 1
|
||||
elseif isKeystone then
|
||||
bagMaxStacks[bagSlot] = 1
|
||||
bagQualities[bagSlot] = 4
|
||||
bagStacks[bagSlot] = 1
|
||||
else
|
||||
bagMaxStacks[bagSlot] = select(8, GetItemInfo(itemID))
|
||||
end
|
||||
|
||||
bagIDs[bagSlot] = itemID
|
||||
if not isKeystone then
|
||||
bagQualities[bagSlot] = select(3, GetItemInfo(itemLink))
|
||||
bagStacks[bagSlot] = select(2, B:GetItemInfo(bag, slot))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function B:IsSpecialtyBag(bagID)
|
||||
if safe[bagID] or IsGuildBankBag(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)
|
||||
if family == 0 or family == nil then return false end
|
||||
|
||||
return family
|
||||
end
|
||||
|
||||
function B:CanItemGoInBag(bag, slot, targetBag)
|
||||
if IsGuildBankBag(targetBag) then return true end
|
||||
|
||||
local item = bagIDs[B:Encode_BagSlot(bag, slot)]
|
||||
local itemFamily = GetItemFamily(item)
|
||||
if itemFamily and itemFamily > 0 then
|
||||
local equipSlot = select(9, GetItemInfo(item))
|
||||
if equipSlot == 'INVTYPE_BAG' then
|
||||
itemFamily = 1
|
||||
end
|
||||
end
|
||||
local bagFamily = select(2, GetContainerNumFreeSlots(targetBag))
|
||||
if itemFamily then
|
||||
return (bagFamily == 0) or band(itemFamily, bagFamily) > 0
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function B.Compress(...)
|
||||
for i=1, select('#', ...) do
|
||||
local bags = select(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 = #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
|
||||
|
||||
wipe(targetItems)
|
||||
wipe(targetSlots)
|
||||
wipe(sourceUsed)
|
||||
end
|
||||
|
||||
local blackListedSlots = {}
|
||||
local blackList = {}
|
||||
local blackListQueries = {}
|
||||
function B:BuildBlacklist(...)
|
||||
for entry in pairs(...) do
|
||||
local itemName = GetItemInfo(entry)
|
||||
|
||||
if itemName then
|
||||
blackList[itemName] = true
|
||||
elseif entry ~= '' then
|
||||
if strfind(entry, '%[') and strfind(entry, '%]') then
|
||||
--For some reason the entry was not treated as a valid item. Extract the item name.
|
||||
entry = strmatch(entry, '%[(.*)%]')
|
||||
end
|
||||
blackListQueries[#blackListQueries+1] = entry
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function B.Sort(bags, sorter, invertDirection)
|
||||
if not sorter then sorter = invertDirection and ReverseSort or DefaultSort end
|
||||
|
||||
--Wipe tables before we begin
|
||||
wipe(blackList)
|
||||
wipe(blackListQueries)
|
||||
wipe(blackListedSlots)
|
||||
|
||||
--Build blacklist of items based on the profile and global list
|
||||
B:BuildBlacklist(B.db.ignoredItems)
|
||||
B: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)
|
||||
|
||||
if link then
|
||||
if blackList[GetItemInfo(link)] then
|
||||
blackListedSlots[bagSlot] = true
|
||||
end
|
||||
|
||||
if not blackListedSlots[bagSlot] then
|
||||
local method
|
||||
for _,itemsearchquery in pairs(blackListQueries) do
|
||||
method = Search.Matches
|
||||
if Search.Filters.tipPhrases.keywords[itemsearchquery] then
|
||||
method = Search.TooltipPhrase
|
||||
itemsearchquery = Search.Filters.tipPhrases.keywords[itemsearchquery]
|
||||
end
|
||||
local success, result = pcall(method, Search, link, itemsearchquery)
|
||||
if success and result then
|
||||
blackListedSlots[bagSlot] = result
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not blackListedSlots[bagSlot] then
|
||||
initialOrder[bagSlot] = i
|
||||
tinsert(bagSorted, bagSlot)
|
||||
end
|
||||
end
|
||||
|
||||
sort(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
|
||||
wipe(bagLocked)
|
||||
end
|
||||
|
||||
wipe(bagSorted)
|
||||
wipe(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 #specialtyBags > 0 then
|
||||
B:Fill(from, specialtyBags)
|
||||
end
|
||||
|
||||
B.Fill(from, to)
|
||||
wipe(specialtyBags)
|
||||
end
|
||||
|
||||
function B.Fill(sourceBags, targetBags, reverse, canMove)
|
||||
if not canMove then canMove = DefaultCanMove end
|
||||
|
||||
--Wipe tables before we begin
|
||||
wipe(blackList)
|
||||
wipe(blackListedSlots)
|
||||
|
||||
--Build blacklist of items based on the profile and global list
|
||||
B:BuildBlacklist(B.db.ignoredItems)
|
||||
B: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 #emptySlots == 0 then break end
|
||||
local bagSlot = B:Encode_BagSlot(bag, slot)
|
||||
local targetBag = B:Decode_BagSlot(emptySlots[1])
|
||||
local link = B:GetItemLink(bag, slot)
|
||||
|
||||
if link and blackList[GetItemInfo(link)] 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
|
||||
wipe(emptySlots)
|
||||
end
|
||||
|
||||
function B.SortBags(...)
|
||||
for i=1, select('#', ...) do
|
||||
local bags = select(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)
|
||||
wipe(sortedBags)
|
||||
end
|
||||
end
|
||||
|
||||
if bagCache.Normal then
|
||||
B.Stack(bagCache.Normal, bagCache.Normal, B.IsPartial)
|
||||
B.Sort(bagCache.Normal, nil, B.db.sortInverted)
|
||||
wipe(bagCache.Normal)
|
||||
end
|
||||
wipe(bagCache)
|
||||
wipe(bagGroups)
|
||||
end
|
||||
end
|
||||
|
||||
function B:StartStacking()
|
||||
wipe(bagMaxStacks)
|
||||
wipe(bagStacks)
|
||||
wipe(bagIDs)
|
||||
wipe(bagQualities)
|
||||
wipe(bagPetIDs)
|
||||
wipe(moveTracker)
|
||||
|
||||
if #moves > 0 then
|
||||
B.SortUpdateTimer:Show()
|
||||
else
|
||||
B:StopStacking()
|
||||
end
|
||||
end
|
||||
|
||||
function B:RegisterUpdateDelayed()
|
||||
local shouldUpdateFade
|
||||
|
||||
for _, bagFrame in pairs(B.BagFrames) do
|
||||
if bagFrame.registerUpdate then
|
||||
B:UpdateAllSlots(bagFrame)
|
||||
|
||||
bagFrame:RegisterEvent('BAG_UPDATE')
|
||||
bagFrame:RegisterEvent('BAG_UPDATE_COOLDOWN')
|
||||
|
||||
for _, event in pairs(bagFrame.events) do
|
||||
bagFrame:RegisterEvent(event)
|
||||
end
|
||||
|
||||
bagFrame.registerUpdate = nil
|
||||
shouldUpdateFade = true -- we should refresh the bag search after sorting
|
||||
end
|
||||
end
|
||||
|
||||
if shouldUpdateFade then
|
||||
B:RefreshSearch() -- this will clear the bag lock look during a sort
|
||||
end
|
||||
end
|
||||
|
||||
function B:StopStacking(message, noUpdate)
|
||||
wipe(moves)
|
||||
wipe(moveTracker)
|
||||
moveRetries, lastItemID, lockStop, lastDestination, lastMove = 0, nil, nil, nil, nil
|
||||
|
||||
B.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, B.RegisterUpdateDelayed)
|
||||
end
|
||||
|
||||
if message then
|
||||
E:Print(message)
|
||||
end
|
||||
end
|
||||
|
||||
function B:DoMove(move)
|
||||
if GetCursorInfo() == 'item' 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 = B:GetItemID(sourceBag, sourceSlot)
|
||||
local targetItemID = B: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 = select(8, 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 GetCursorInfo() == 'item' then
|
||||
B:PickupItem(targetBag, targetSlot)
|
||||
end
|
||||
|
||||
local sourceGuild = IsGuildBankBag(sourceBag)
|
||||
local targetGuild = IsGuildBankBag(targetBag)
|
||||
|
||||
if sourceGuild then
|
||||
QueryGuildBankTab(sourceBag - 50)
|
||||
end
|
||||
if targetGuild then
|
||||
QueryGuildBankTab(targetBag - 50)
|
||||
end
|
||||
|
||||
return true, sourceItemID, source, targetItemID, target, sourceGuild or targetGuild
|
||||
end
|
||||
|
||||
function B:DoMoves()
|
||||
if InCombatLockdown() then
|
||||
return B:StopStacking(L["Confused.. Try Again!"])
|
||||
end
|
||||
|
||||
local cursorType, cursorItemID = GetCursorInfo()
|
||||
if cursorType == 'item' and cursorItemID then
|
||||
if lastItemID ~= cursorItemID then
|
||||
return B:StopStacking(L["Confused.. Try Again!"])
|
||||
end
|
||||
|
||||
if moveRetries < 100 then
|
||||
local targetBag, targetSlot = B:Decode_BagSlot(lastDestination)
|
||||
local _, _, targetLocked = B:GetItemInfo(targetBag, targetSlot)
|
||||
if not targetLocked then
|
||||
B: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 = B:GetItemID(B: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, wasGuild = B:DoMove(lastMove)
|
||||
WAIT_TIME = wasGuild and 0.5 or 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
|
||||
wipe(moveTracker)
|
||||
|
||||
local success, moveID, targetID, moveSource, moveTarget, wasGuild
|
||||
if #moves > 0 then
|
||||
for i = #moves, 1, -1 do
|
||||
success, moveID, moveSource, targetID, moveTarget, wasGuild = B:DoMove(moves[i])
|
||||
if not success then
|
||||
WAIT_TIME = wasGuild and 0.3 or 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 = wasGuild and 0.3 or 0
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
B:StopStacking()
|
||||
end
|
||||
|
||||
function B:GetGroup(id)
|
||||
if strmatch(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 B.SortUpdateTimer:IsShown() then
|
||||
B:StopStacking(L["Already Running.. Bailing Out!"], true)
|
||||
return
|
||||
end
|
||||
|
||||
wipe(bagGroups)
|
||||
if not groups or #groups == 0 then
|
||||
groups = groupsDefaults
|
||||
end
|
||||
for bags in (groups or ''):gmatch('%S+') do
|
||||
if bags == 'guild' then
|
||||
bags = B:GetGroup(bags)
|
||||
if bags then
|
||||
tinsert(bagGroups, {bags[GetCurrentGuildBankTab()]})
|
||||
end
|
||||
else
|
||||
bags = B:GetGroup(bags)
|
||||
if bags then
|
||||
tinsert(bagGroups, bags)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
B:ScanBags()
|
||||
if func(unpack(bagGroups)) == false then
|
||||
return
|
||||
end
|
||||
wipe(bagGroups)
|
||||
B:StartStacking()
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user