This commit is contained in:
rucky
2026-03-16 13:48:46 +08:00
commit 2a55dd6dad
93 changed files with 75075 additions and 0 deletions

2087
Bags/Bank.lua Normal file

File diff suppressed because it is too large Load Diff

1619
Bags/Container.lua Normal file

File diff suppressed because it is too large Load Diff

113
Bags/Core.lua Normal file
View File

@@ -0,0 +1,113 @@
--------------------------------------------------------------------------------
-- S-Frames: Bag Module Core (Bags/Core.lua)
-- Entry point, registered on PLAYER_LOGIN
--------------------------------------------------------------------------------
SFrames.Bags.Core = {}
-- Ensure default config exists
if not SFrames.Config.Bags then
SFrames.Config.Bags = {
enable = true,
columns = 10,
bagSpacing = 0,
scale = 1,
sellGrey = true,
bankColumns = 12,
bankSpacing = 0,
bankScale = 1,
}
end
local function EnsureDB()
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Bags then
SFramesDB.Bags = {}
for k, v in pairs(SFrames.Config.Bags) do
SFramesDB.Bags[k] = v
end
end
-- Fill missing keys with defaults
for k, v in pairs(SFrames.Config.Bags) do
if SFramesDB.Bags[k] == nil then
SFramesDB.Bags[k] = v
end
end
end
local function HookBagFunctions()
-- Save original functions
local _OpenAllBags = OpenAllBags
local _CloseAllBags = CloseAllBags
local _ToggleBag = ToggleBag
local _ToggleBackpack = ToggleBackpack
local _ToggleKeyRing = ToggleKeyRing
SFrames.Bags._origToggleKeyRing = _ToggleKeyRing
OpenAllBags = function()
if SFramesDB.Bags.enable then
SFrames.Bags.Container:Open()
else
_OpenAllBags()
end
end
CloseAllBags = function()
if SFramesDB.Bags.enable then
SFrames.Bags.Container:Close()
else
_CloseAllBags()
end
end
ToggleBag = function(id)
if id == -2 or id == (KEYRING_CONTAINER or -2) then
_ToggleBag(id)
return
end
if SFramesDB.Bags.enable then
SFrames.Bags.Container:Toggle()
else
_ToggleBag(id)
end
end
ToggleBackpack = function()
if SFramesDB.Bags.enable then
SFrames.Bags.Container:Toggle()
else
_ToggleBackpack()
end
end
-- Keyring: always use original keyring window
ToggleKeyRing = function()
_ToggleKeyRing()
end
end
function SFrames.Bags.Core:Initialize()
SFrames:Print("Debug: Bags Core Initializing...")
EnsureDB()
if not SFramesDB.Bags.enable then
SFrames:Print("Debug: Bags are disabled in config.")
return
end
SFrames:Print("Debug: Hooking functions...")
HookBagFunctions()
SFrames:Print("Debug: Init Container...")
SFrames.Bags.Container:Initialize()
SFrames:Print("Debug: Init Bank...")
if SFrames.Bags.Bank then SFrames.Bags.Bank:Initialize() end
SFrames:Print("Debug: Init Features...")
SFrames.Bags.Features:Initialize()
SFrames:Print("Debug: Bags Init Complete.")
end
-- NOTE: Initialize is called from Core.lua SFrames:Initialize() on PLAYER_LOGIN.
-- Do NOT register PLAYER_LOGIN here to avoid double initialization.

139
Bags/Features.lua Normal file
View File

@@ -0,0 +1,139 @@
--------------------------------------------------------------------------------
-- S-Frames: Bag Module Features (Bags/Features.lua)
-- Auto-sell grey + search apply (buttons are now built in Container.lua)
--------------------------------------------------------------------------------
SFrames.Bags.Features = {}
function SFrames.Bags.Features:Initialize()
self:SetupAutoSell()
self:SetupAutoOpenBags()
end
-- Auto Sell Grey Items when merchant opens
function SFrames.Bags.Features:SetupAutoSell()
local f = CreateFrame("Frame")
f:RegisterEvent("MERCHANT_SHOW")
f:SetScript("OnEvent", function()
if not (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.sellGrey) then return end
for bag = 0, 4 do
for slot = 1, GetContainerNumSlots(bag) do
local link = GetContainerItemLink(bag, slot)
if link then
-- Extract item ID from link (format: item:XXXX:...)
local _, _, itemString = string.find(link, "item:(%d+)")
if itemString then
local _, _, _, _, _, _, _, _, _, _, itemSellPrice = GetItemInfo("item:" .. itemString)
-- GetItemInfo returns quality as 5th return
local _, _, itemRarity = GetItemInfo("item:" .. itemString)
-- safer: use a scan tooltip to get quality
local scanName, _, scanRarity = GetItemInfo("item:" .. itemString)
if scanRarity and scanRarity == 0 then
local _, itemCount = GetContainerItemInfo(bag, slot)
UseContainerItem(bag, slot)
if scanName then
SFrames:Print("售出: " .. scanName .. " x" .. (itemCount or 1))
end
end
end
end
end
end
end)
end
-- Auto open/close bags at Bank, Merchant, Mail, AH, Trade
function SFrames.Bags.Features:SetupAutoOpenBags()
local f = CreateFrame("Frame")
f:RegisterEvent("MERCHANT_SHOW")
f:RegisterEvent("BANKFRAME_OPENED")
f:RegisterEvent("MAIL_SHOW")
f:RegisterEvent("AUCTION_HOUSE_SHOW")
f:RegisterEvent("TRADE_SHOW")
f:RegisterEvent("MERCHANT_CLOSED")
f:RegisterEvent("BANKFRAME_CLOSED")
f:RegisterEvent("MAIL_CLOSED")
f:RegisterEvent("AUCTION_HOUSE_CLOSED")
f:RegisterEvent("TRADE_CLOSED")
local autoOpened = false
f:SetScript("OnEvent", function()
if not (SFramesDB and SFramesDB.Bags and SFramesDB.Bags.enable) then return end
-- Shows
if event == "MERCHANT_SHOW" or event == "BANKFRAME_OPENED" or event == "MAIL_SHOW" or event == "AUCTION_HOUSE_SHOW" or event == "TRADE_SHOW" then
if SFramesBagFrame and not SFramesBagFrame:IsVisible() then
autoOpened = true
SFrames.Bags.Container:Open()
end
-- Closes
elseif event == "MERCHANT_CLOSED" or event == "BANKFRAME_CLOSED" or event == "MAIL_CLOSED" or event == "AUCTION_HOUSE_CLOSED" or event == "TRADE_CLOSED" then
if autoOpened then
SFrames.Bags.Container:Close()
autoOpened = false
end
end
end)
end
-- Search filter - called from the search EditBox OnTextChanged
function SFrames.Bags.Features:ApplySearch(query)
-- In WoW 1.12 Chinese client, text is GBK encoded.
-- using string.lower on GBK strings corrupts Chinese characters because the 2nd byte
-- of Chinese characters can hit the ASCII uppercase range (A-Z).
local safeQuery = query or ""
safeQuery = string.gsub(safeQuery, "^%s+", "")
safeQuery = string.gsub(safeQuery, "%s+$", "")
if not string.find(safeQuery, "%S") then
safeQuery = ""
end
local function ProcessSlots(prefix)
for i = 1, 250 do
local btn = _G[prefix .. i]
if not btn then break end
local icon = _G[btn:GetName() .. "IconTexture"]
if btn:IsShown() and icon then
if safeQuery == "" then
icon:SetVertexColor(1, 1, 1)
if btn.border then btn.border:SetAlpha(1) end
if btn.junkIcon then btn.junkIcon:SetAlpha(1) end
if btn.qualGlow then btn.qualGlow:SetAlpha(0.8) end
elseif btn.bagID ~= nil and btn.slotID ~= nil then
local link = GetContainerItemLink(btn.bagID, btn.slotID)
if link then
local name = GetItemInfo(link)
if not name then
local _, _, parsedName = string.find(link, "%[(.+)%]")
name = parsedName
end
-- Exact substring match (safe for GBK)
if name and string.find(name, safeQuery, 1, true) then
icon:SetVertexColor(1, 1, 1)
if btn.border then btn.border:SetAlpha(1) end
if btn.junkIcon then btn.junkIcon:SetAlpha(1) end
if btn.qualGlow then btn.qualGlow:SetAlpha(0.8) end
else
icon:SetVertexColor(0.45, 0.45, 0.45)
if btn.border then btn.border:SetAlpha(0.4) end
if btn.junkIcon then btn.junkIcon:SetAlpha(0.4) end
if btn.qualGlow then btn.qualGlow:SetAlpha(0.15) end
end
else
icon:SetVertexColor(0.45, 0.45, 0.45)
if btn.border then btn.border:SetAlpha(0.4) end
if btn.junkIcon then btn.junkIcon:SetAlpha(0.4) end
if btn.qualGlow then btn.qualGlow:SetAlpha(0.15) end
end
end
end
end
end
ProcessSlots("SFramesBagSlot")
end

337
Bags/Offline.lua Normal file
View File

@@ -0,0 +1,337 @@
--------------------------------------------------------------------------------
-- S-Frames: Offline DB for Bag Module (Bags/Offline.lua)
-- Tracks inventory across characters and realms
-- NOTE: This file defines SFrames.Bags first - must be loaded before other Bag files
--------------------------------------------------------------------------------
SFrames.Bags = SFrames.Bags or {}
SFrames.Bags.Offline = {}
local offlineFrame = CreateFrame("Frame")
offlineFrame:RegisterEvent("PLAYER_LOGIN")
offlineFrame:RegisterEvent("BAG_UPDATE")
offlineFrame:RegisterEvent("BANKFRAME_OPENED")
offlineFrame:RegisterEvent("BANKFRAME_CLOSED")
offlineFrame:RegisterEvent("PLAYERBANKSLOTS_CHANGED")
offlineFrame:RegisterEvent("PLAYERBANKBAGSLOTS_CHANGED")
local realmName = ""
local playerName = ""
local isBankOpen = false
local function QueueBankRefreshScans()
local elapsed = 0
local index = 1
local points = { 0.08, 0.25, 0.55, 0.95 }
local t = CreateFrame("Frame")
t:SetScript("OnUpdate", function()
elapsed = elapsed + (arg1 or 0)
if index <= table.getn(points) and elapsed >= points[index] then
if isBankOpen then
SFrames.Bags.Offline:ScanBags()
end
index = index + 1
end
if index > table.getn(points) then
this:SetScript("OnUpdate", nil)
end
end)
end
local function InitDB()
if not SFramesGlobalDB then SFramesGlobalDB = {} end
if not SFramesGlobalDB[realmName] then SFramesGlobalDB[realmName] = {} end
if not SFramesGlobalDB[realmName][playerName] then
SFramesGlobalDB[realmName][playerName] = {
bags = {},
bank = {},
bankSlots = 0,
bankBags = {},
equippedBags = {},
money = 0
}
end
end
local function IsBankBagInvSlot(slotID)
return type(slotID) == "number" and slotID > 23
end
local function IsBagItemLink(link)
if type(link) ~= "string" or link == "" then
return false
end
local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link)
return equipLoc == "INVTYPE_BAG"
end
local function GetBagLinkState(link)
if type(link) ~= "string" or link == "" then
return "invalid"
end
local _, _, _, _, _, _, _, equipLoc = GetItemInfo(link)
if equipLoc == "INVTYPE_BAG" then
return "bag"
end
if equipLoc == nil or equipLoc == "" then
return "unknown"
end
return "invalid"
end
local function SafeGetInventorySlotByName(slotName)
if not GetInventorySlotInfo then return nil end
local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end)
if ok and IsBankBagInvSlot(slotID) then
return slotID
end
return nil
end
local function ResolvePlayerBagInvSlot(index)
if type(index) ~= "number" or index < 1 or index > 4 then
return nil
end
local liveBtn = _G["CharacterBag" .. (index - 1) .. "Slot"]
if liveBtn and liveBtn.GetID then
local id = liveBtn:GetID()
if type(id) == "number" and id > 0 then
return id
end
end
if ContainerIDToInventoryID then
local ok, slotID = pcall(function() return ContainerIDToInventoryID(index) end)
if ok and type(slotID) == "number" and slotID > 0 then
return slotID
end
end
local slotNames = {
"Bag" .. (index - 1) .. "Slot",
"Bag" .. index .. "Slot",
"CharacterBag" .. (index - 1) .. "Slot",
}
for _, slotName in ipairs(slotNames) do
local ok, slotID = pcall(function() return GetInventorySlotInfo(slotName) end)
if ok and type(slotID) == "number" and slotID > 0 then
return slotID
end
end
-- Vanilla fallback.
local fallback = { [1] = 20, [2] = 21, [3] = 22, [4] = 23 }
return fallback[index]
end
local function CaptureEquippedBagMeta(db)
if not db then return end
db.equippedBags = {}
for index = 1, 4 do
local invSlot = ResolvePlayerBagInvSlot(index)
local link = nil
local texture = nil
if invSlot then
link = GetInventoryItemLink("player", invSlot)
texture = GetInventoryItemTexture("player", invSlot)
end
local size = GetContainerNumSlots(index) or 0
db.equippedBags[index] = {
link = link,
texture = texture,
size = tonumber(size) or 0,
}
end
end
local function ResolveBankBagInvSlot(index)
if type(index) ~= "number" or index <= 0 then return nil end
local bagID = index + 4
local function TryBankButtonID(buttonID, isBank)
if not BankButtonIDToInvSlotID then return nil end
local ok, slotID = pcall(function() return BankButtonIDToInvSlotID(buttonID, isBank) end)
if ok and IsBankBagInvSlot(slotID) then
return slotID
end
return nil
end
local function AcceptIfBag(slotID)
if not IsBankBagInvSlot(slotID) then return nil end
local link = GetInventoryItemLink("player", slotID)
if link and IsBagItemLink(link) then
return slotID
end
return nil
end
-- Guda-style primary mapping
local slot = TryBankButtonID(bagID, 1)
if slot then return slot end
slot = TryBankButtonID(index, 1)
if slot then return slot end
-- Fallbacks: only when they are confirmed bag items
slot = TryBankButtonID(bagID, nil)
slot = AcceptIfBag(slot)
if slot then return slot end
slot = TryBankButtonID(index, nil)
slot = AcceptIfBag(slot)
if slot then return slot end
if ContainerIDToInventoryID then
local ok, s = pcall(function() return ContainerIDToInventoryID(bagID) end)
if ok then
slot = AcceptIfBag(s)
if slot then return slot end
end
end
slot = AcceptIfBag(SafeGetInventorySlotByName("BankBagSlot" .. index))
if slot then return slot end
slot = AcceptIfBag(SafeGetInventorySlotByName("BankSlot" .. index))
if slot then return slot end
local liveBtn = _G["BankFrameBag" .. index] or _G["BankFrameBag" .. index .. "Slot"]
if liveBtn and liveBtn.GetID then
slot = AcceptIfBag(liveBtn:GetID())
if slot then return slot end
end
return nil
end
local function CaptureBankBagMeta(db)
if not db then return end
local purchased = (GetNumBankSlots and GetNumBankSlots()) or 0
db.bankSlots = tonumber(purchased) or 0
db.bankBags = {}
for index = 1, 7 do
local bagID = index + 4
local size = GetContainerNumSlots(bagID) or 0
local info = {
unlocked = (index <= db.bankSlots),
size = tonumber(size) or 0,
link = nil,
texture = nil,
}
if info.unlocked then
local invSlot = ResolveBankBagInvSlot(index)
if invSlot then
local link = GetInventoryItemLink("player", invSlot)
local state = GetBagLinkState(link)
if (state == "bag") or (state == "unknown" and info.size > 0) then
info.link = link
info.texture = GetInventoryItemTexture("player", invSlot)
end
end
end
db.bankBags[index] = info
end
end
local function ScanContainer(containerID, isBank)
if not SFramesGlobalDB or realmName == "" or playerName == "" then return end
local db = SFramesGlobalDB[realmName][playerName]
local targetDB = isBank and db.bank or db.bags
local size = GetContainerNumSlots(containerID)
targetDB[containerID] = { size = size, items = {} }
for slot = 1, size do
local link = GetContainerItemLink(containerID, slot)
if link then
local texture, itemCount, locked, quality = GetContainerItemInfo(containerID, slot)
local _, _, idStr = string.find(link, "item:(%d+):")
local itemID = idStr and tonumber(idStr) or nil
targetDB[containerID].items[slot] = { id = itemID,
link = link,
count = itemCount,
texture = texture,
quality = quality
}
end
end
end
function SFrames.Bags.Offline:ScanBags()
if realmName == "" or playerName == "" then return end
InitDB()
local db = SFramesGlobalDB[realmName] and SFramesGlobalDB[realmName][playerName]
if not db then return end
for bag = 0, 4 do
ScanContainer(bag, false)
end
CaptureEquippedBagMeta(db)
if isBankOpen then
ScanContainer(-1, true)
for bag = 5, 11 do
ScanContainer(bag, true)
end
CaptureBankBagMeta(db)
end
db.money = GetMoney()
end
local scanPending = false
local scanTimer = CreateFrame("Frame")
scanTimer:Hide()
local function RequestBagScan()
if scanPending then return end
scanPending = true
scanTimer.elapsed = 0
scanTimer:Show()
scanTimer:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + arg1
if this.elapsed > 0.5 then
this:SetScript("OnUpdate", nil)
this:Hide()
scanPending = false
SFrames.Bags.Offline:ScanBags()
end
end)
end
offlineFrame:SetScript("OnEvent", function()
if event == "PLAYER_LOGIN" then
realmName = GetRealmName() or ""
playerName = UnitName("player") or ""
InitDB()
SFrames.Bags.Offline:ScanBags()
elseif event == "BAG_UPDATE" or event == "PLAYERBANKSLOTS_CHANGED" or event == "PLAYERBANKBAGSLOTS_CHANGED" then
RequestBagScan()
elseif event == "BANKFRAME_OPENED" then
isBankOpen = true
RequestBagScan()
QueueBankRefreshScans()
elseif event == "BANKFRAME_CLOSED" then
isBankOpen = false
end
end)
function SFrames.Bags.Offline:GetCharacterList()
if not SFramesGlobalDB or realmName == "" then return {} end
local chars = {}
if SFramesGlobalDB[realmName] then
for name in pairs(SFramesGlobalDB[realmName]) do
table.insert(chars, name)
end
end
return chars
end
function SFrames.Bags.Offline:GetCharacterData(name)
if not SFramesGlobalDB or realmName == "" then return nil end
return SFramesGlobalDB[realmName] and SFramesGlobalDB[realmName][name]
end
function SFrames.Bags.Offline:GetCurrentPlayerName()
return playerName
end

483
Bags/Sort.lua Normal file
View File

@@ -0,0 +1,483 @@
--------------------------------------------------------------------------------
-- S-Frames: Bag Sorting Logic (Bags/Sort.lua)
-- Manual sorting algorithm compatible with Vanilla/Turtle WoW
--------------------------------------------------------------------------------
SFrames.Bags.Sort = SFrames.Bags.Sort or {}
local isSorting = false
local sortQueue = {}
local sortTimer = CreateFrame("Frame")
local sortDelay = 0.05 -- Increased speed from 0.15 to accelerate sorting.
local timeSinceLast = 0
local activeCompleteMessage = nil
local activeCompleteUpdate = nil
local activeBagOrder = nil
local activePhase = nil
local TEXT_LOCKED = "\233\131\168\229\136\134\231\137\169\229\147\129\229\183\178\233\148\129\229\174\154\239\188\140\232\175\183\231\168\141\229\144\142\233\135\141\232\175\149\227\128\130"
local TEXT_OFFLINE_BAGS = "\231\166\187\231\186\191\230\168\161\229\188\143\228\184\139\230\151\160\230\179\149\230\149\180\231\144\134\229\156\168\231\186\191\232\131\140\229\140\133\227\128\130"
local TEXT_BAG_DONE = "\232\131\140\229\140\133\230\149\180\231\144\134\229\174\140\230\136\144\227\128\130"
local TEXT_OFFLINE_BANK = "\231\166\187\231\186\191\230\168\161\229\188\143\228\184\139\230\151\160\230\179\149\230\149\180\231\144\134\229\156\168\231\186\191\233\147\182\232\161\140\227\128\130"
local TEXT_BANK_DONE = "\233\147\182\232\161\140\230\149\180\231\144\134\229\174\140\230\136\144\227\128\130"
local itemMaxStackCache = {}
local function GetItemSortValue(link)
if not link then return 0, "" end
local _, _, itemString = string.find(link, "^|c%x+|H(.+)|h%[.*%]")
if not itemString then itemString = link end
local itemName, _, itemRarity, _, itemType = GetItemInfo(itemString)
if not itemName then return 0, "" end
local score = 0
local _, _, itemID = string.find(link, "item:(%d+)")
if itemID == "6948" or string.find(itemName, "Hearthstone") then
score = 1000000
else
score = score + (itemRarity or 0) * 100000
if itemType == "Weapon" then
score = score + 50000
elseif itemType == "Armor" then
score = score + 40000
elseif itemType == "Consumable" then
score = score + 30000
elseif itemType == "Trade Goods" then
score = score + 20000
elseif itemType == "Recipe" then
score = score + 10000
end
end
return score, itemName
end
local function GetItemIdentity(link)
if not link then return nil end
local _, _, itemString = string.find(link, "|H([^|]+)|h")
if not itemString then itemString = link end
return itemString
end
local function GetItemMaxStack(link)
local itemKey = GetItemIdentity(link)
if not itemKey then return 1 end
if itemMaxStackCache[itemKey] then
return itemMaxStackCache[itemKey]
end
local maxStack = 1
local res = {GetItemInfo(itemKey)}
-- In standard vanilla, maxStack is parameter 7. In some modified schema (TBC) it is parameter 8.
local v7 = tonumber(res[7])
local v8 = tonumber(res[8])
if v7 and v7 > 1 then
maxStack = v7
elseif v8 and v8 > 1 then
maxStack = v8
end
itemMaxStackCache[itemKey] = maxStack
return maxStack
end
local function GetSpecialType(link, isBag)
if not link then return "Normal" end
local _, _, itemString = string.find(link, "^|c%x+|H(.+)|h%[.*%]")
if not itemString then itemString = link end
local itemName, _, _, _, itemType, itemSubType = GetItemInfo(itemString)
if not itemType and not itemName then return "Normal" end
local _, _, itemID = string.find(itemString, "item:(%d+)")
if isBag then
if itemType == "Quiver" or itemType == "箭袋" then return "Ammo" end
if itemSubType == "Quiver" or itemSubType == "Ammo Pouch" or itemSubType == "箭袋" or itemSubType == "子弹袋" then return "Ammo" end
if itemSubType == "Soul Bag" or itemSubType == "灵魂袋" then return "Soul" end
if itemSubType == "Enchanting Bag" or itemSubType == "附魔材料袋" then return "Enchanting" end
if itemSubType == "Herb Bag" or itemSubType == "草药袋" then return "Herb" end
else
if itemType == "Projectile" or itemType == "弹药" then return "Ammo" end
if itemSubType == "Arrow" or itemSubType == "Bullet" or itemSubType == "" or itemSubType == "子弹" then return "Ammo" end
if itemID == "6265" or itemName == "Soul Shard" or itemName == "灵魂碎片" then return "Soul" end
end
return "Normal"
end
local function SortItemsByRule(items)
table.sort(items, function(a, b)
if a.score ~= b.score then
return a.score > b.score
elseif a.name ~= b.name then
return a.name < b.name
else
return a.count > b.count
end
end)
end
local function ResetSortState()
isSorting = false
sortQueue = {}
activeCompleteMessage = nil
activeCompleteUpdate = nil
activeBagOrder = nil
activePhase = nil
timeSinceLast = 0
sortTimer:Hide()
end
local function FinishSort()
local msg = activeCompleteMessage
local updater = activeCompleteUpdate
ResetSortState()
if msg then SFrames:Print(msg) end
if updater then updater() end
end
function SFrames.Bags.Sort:ExecuteSimpleSort(items, bagOrder)
sortQueue = {}
local slotPool = { Normal = {}, Ammo = {}, Soul = {}, Enchanting = {}, Herb = {} }
local allocatedStream = {}
for _, bag in ipairs(bagOrder) do
local btype = "Normal"
if bag ~= 0 and bag ~= -1 then
local invID
if bag >= 1 and bag <= 4 then
invID = ContainerIDToInventoryID(bag)
elseif bag >= 5 and bag <= 11 then
invID = bag + 34
end
if invID then
local ok, link = pcall(GetInventoryItemLink, "player", invID)
if ok and link then btype = GetSpecialType(link, true) end
end
end
local bagSlots = GetContainerNumSlots(bag) or 0
for slot = 1, bagSlots do
local s = { bag = bag, slot = slot, type = btype }
table.insert(allocatedStream, s)
if slotPool[btype] then
table.insert(slotPool[btype], s)
else
table.insert(slotPool.Normal, s)
end
end
end
for _, item in ipairs(items) do
item.specialType = GetSpecialType(item.link, false)
end
-- Now assign target slot to each item
for _, item in ipairs(items) do
local targetSlot = nil
if item.specialType ~= "Normal" and slotPool[item.specialType] and table.getn(slotPool[item.specialType]) > 0 then
targetSlot = table.remove(slotPool[item.specialType], 1)
elseif table.getn(slotPool.Normal) > 0 then
targetSlot = table.remove(slotPool.Normal, 1)
end
if targetSlot then
targetSlot.idealItem = item
end
end
-- Build virtual moves to align physical items to targetSlots
for _, target in ipairs(allocatedStream) do
if target.idealItem then
local idealItem = target.idealItem
if idealItem.bag ~= target.bag or idealItem.slot ~= target.slot then
table.insert(sortQueue, {
fromBag = idealItem.bag,
fromSlot = idealItem.slot,
toBag = target.bag,
toSlot = target.slot,
retries = 0
})
-- Reflect the virtual swap in our model so later moves stay correct.
for j = 1, table.getn(items) do
if items[j].bag == target.bag and items[j].slot == target.slot then
items[j].bag = idealItem.bag
items[j].slot = idealItem.slot
break
end
end
idealItem.bag = target.bag
idealItem.slot = target.slot
end
end
end
end
function SFrames.Bags.Sort:ScanItems(bagOrder, ignoreLocked)
local items = {}
for _, bag in ipairs(bagOrder) do
local bagSlots = GetContainerNumSlots(bag) or 0
for slot = 1, bagSlots do
local link = GetContainerItemLink(bag, slot)
local _, itemCount, locked = GetContainerItemInfo(bag, slot)
if locked and not ignoreLocked then
return nil, TEXT_LOCKED
end
if link then
local score, name = GetItemSortValue(link)
table.insert(items, {
bag = bag,
slot = slot,
link = link,
score = score,
name = name,
count = itemCount or 1,
stackKey = GetItemIdentity(link),
maxStack = GetItemMaxStack(link)
})
end
end
end
return items, nil
end
function SFrames.Bags.Sort:BuildStackMergeMoves(items)
local grouped = {}
local moves = {}
for _, item in ipairs(items) do
if item and item.stackKey and item.maxStack and item.maxStack > 1 and (item.count or 0) > 0 then
if not grouped[item.stackKey] then
grouped[item.stackKey] = {
maxStack = item.maxStack,
slots = {}
}
end
if item.maxStack > grouped[item.stackKey].maxStack then
grouped[item.stackKey].maxStack = item.maxStack
end
table.insert(grouped[item.stackKey].slots, {
bag = item.bag,
slot = item.slot,
count = item.count
})
end
end
for _, group in pairs(grouped) do
local slots = group.slots
if table.getn(slots) > 1 then
table.sort(slots, function(a, b)
if a.count ~= b.count then
return a.count > b.count
elseif a.bag ~= b.bag then
return a.bag < b.bag
else
return a.slot < b.slot
end
end)
local left = 1
local right = table.getn(slots)
while left < right do
local target = slots[left]
local source = slots[right]
if target.count >= group.maxStack then
left = left + 1
elseif source.count <= 0 then
right = right - 1
else
local transfer = math.min(group.maxStack - target.count, source.count)
if transfer > 0 then
table.insert(moves, {
fromBag = source.bag,
fromSlot = source.slot,
toBag = target.bag,
toSlot = target.slot,
transferCount = transfer,
isStackMove = true
})
target.count = target.count + transfer
source.count = source.count - transfer
end
if source.count <= 0 then
right = right - 1
end
if target.count >= group.maxStack then
left = left + 1
end
end
end
end
end
return moves
end
function SFrames.Bags.Sort:StartPlacementPhase(ignoreLocked)
if not activeBagOrder then
FinishSort()
return
end
local items, err = self:ScanItems(activeBagOrder, ignoreLocked)
if not items then
ResetSortState()
if err then SFrames:Print(err) end
return
end
SortItemsByRule(items)
self:ExecuteSimpleSort(items, activeBagOrder)
if table.getn(sortQueue) == 0 then
FinishSort()
return
end
activePhase = "sort"
sortTimer:Show()
end
function SFrames.Bags.Sort:StartForBags(bagOrder, completeMessage, completeUpdate)
if isSorting then return end
isSorting = true
sortQueue = {}
timeSinceLast = 0
activeCompleteMessage = completeMessage
activeCompleteUpdate = completeUpdate
activeBagOrder = bagOrder
activePhase = nil
local items, err = self:ScanItems(bagOrder, false)
if not items then
ResetSortState()
if err then SFrames:Print(err) end
return
end
local stackMoves = self:BuildStackMergeMoves(items)
if table.getn(stackMoves) > 0 then
sortQueue = stackMoves
activePhase = "stack"
sortTimer:Show()
return
end
self:StartPlacementPhase(false)
end
function SFrames.Bags.Sort:Start()
if SFrames.Bags.Container and SFrames.Bags.Container.isOffline then
SFrames:Print(TEXT_OFFLINE_BAGS)
return
end
self:StartForBags(
{0, 1, 2, 3, 4},
TEXT_BAG_DONE,
function()
if SFrames.Bags.Container then
SFrames.Bags.Container:UpdateLayout()
end
end
)
end
function SFrames.Bags.Sort:StartBank()
if SFrames.Bags.Bank and SFrames.Bags.Bank.isOffline then
SFrames:Print(TEXT_OFFLINE_BANK)
return
end
self:StartForBags(
{-1, 5, 6, 7, 8, 9, 10, 11},
TEXT_BANK_DONE,
function()
if SFrames.Bags.Bank then
SFrames.Bags.Bank:UpdateLayout()
end
end
)
end
sortTimer:SetScript("OnUpdate", function()
if not isSorting then
this:Hide()
return
end
timeSinceLast = timeSinceLast + arg1
if timeSinceLast > sortDelay then
timeSinceLast = 0
if table.getn(sortQueue) > 0 then
local move = sortQueue[1]
local _, _, lockedFrom = GetContainerItemInfo(move.fromBag, move.fromSlot)
local _, _, lockedTo = GetContainerItemInfo(move.toBag, move.toSlot)
if lockedFrom or lockedTo or CursorHasItem() then
move.retries = (move.retries or 0) + 1
if move.retries > 40 then
table.remove(sortQueue, 1)
if CursorHasItem() then
PickupContainerItem(move.fromBag, move.fromSlot)
end
end
return
end
table.remove(sortQueue, 1)
if move.isStackMove and move.transferCount then
SplitContainerItem(move.fromBag, move.fromSlot, move.transferCount)
PickupContainerItem(move.toBag, move.toSlot)
else
PickupContainerItem(move.fromBag, move.fromSlot)
PickupContainerItem(move.toBag, move.toSlot)
if CursorHasItem() then
PickupContainerItem(move.fromBag, move.fromSlot)
if CursorHasItem() then
PickupContainerItem(move.toBag, move.toSlot)
end
end
end
else
if activePhase == "stack" then
if activeBagOrder then
for _, bag in ipairs(activeBagOrder) do
local bagSlots = GetContainerNumSlots(bag) or 0
for slot = 1, bagSlots do
local _, _, locked = GetContainerItemInfo(bag, slot)
if locked then
return
end
end
end
end
-- After consolidation, rescan inventory and run normal sorting.
SFrames.Bags.Sort:StartPlacementPhase(true)
else
if CursorHasItem() then return end
FinishSort()
end
end
end
end)
sortTimer:Hide()