init
This commit is contained in:
2087
Bags/Bank.lua
Normal file
2087
Bags/Bank.lua
Normal file
File diff suppressed because it is too large
Load Diff
1619
Bags/Container.lua
Normal file
1619
Bags/Container.lua
Normal file
File diff suppressed because it is too large
Load Diff
113
Bags/Core.lua
Normal file
113
Bags/Core.lua
Normal 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
139
Bags/Features.lua
Normal 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
337
Bags/Offline.lua
Normal 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
483
Bags/Sort.lua
Normal 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()
|
||||
Reference in New Issue
Block a user