Instant restack and bag swapping with C_Container.MoveItem
MoveItems() uses ClassicAPI's C_Container.MoveItem() when available (and no move equips a bag): each round sends every move whose slots are free and unlocked, then waits for BAG_UPDATE_DELAYED (1 s fallback) before the next round. Restack() queues exact amounts; EmptyBag()/FillBag() move whole stacks. Locked slots are retried up to 5 times, stock-state handling matches ProcessMoveQueue(), and the cursor-based queue remains the fallback. Approach taken from brues-code/pfUI libs/libbagsort.lua. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019W3rwt5PZSpEDkXYzRuRhH
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
# Bagshui Changelog
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Performance
|
||||
|
||||
- Restack and bag swapping move items with ClassicAPI's `C_Container.MoveItem()` instead of picking each item up and putting it down, the same way pfUI's instant bag sort does. Moves are sent in batches and each batch waits for the server's confirmation, so restacking many stacks or emptying a bag takes about a second instead of several. Without ClassicAPI, the old one-at-a-time method is used.
|
||||
|
||||
## 1.6.8 - 2026-11-01
|
||||
|
||||
### Performance
|
||||
|
||||
@@ -161,14 +161,14 @@ Bagshui:AddComponent(function()
|
||||
--self:PrintDebug(">> source count: " .. source.count)
|
||||
|
||||
if source ~= target and source.count > 0 and target.count < target.maxStackCount and target.count > 0 then
|
||||
-- Queue move of source stacks onto target.
|
||||
self:QueueMove(source, target)
|
||||
--self:PrintDebug(string.format(">>> queued %s:%s (%s) to %s:%s (%s)", source.bagNum, source.slotNum, source.count, target.bagNum, target.slotNum, target.count))
|
||||
|
||||
-- Calculate the changes.
|
||||
oldTargetCount = target.count
|
||||
target.count = math.min(target.count + source.count, target.maxStackCount)
|
||||
source.count = source.count - (target.count - oldTargetCount)
|
||||
|
||||
-- Queue move of source stacks onto target (with the exact amount, for instant moves).
|
||||
self:QueueMove(source, target, target.count - oldTargetCount)
|
||||
--self:PrintDebug(string.format(">>> queued %s:%s (%s) to %s:%s (%s)", source.bagNum, source.slotNum, source.count, target.bagNum, target.slotNum, target.count))
|
||||
--self:PrintDebug(">> NEW source count: " .. source.count)
|
||||
--self:PrintDebug(">> NEW target count: " .. target.count)
|
||||
|
||||
@@ -203,11 +203,16 @@ Bagshui:AddComponent(function()
|
||||
function Inventory:ClearMoveQueue()
|
||||
BsUtil.TableClear(self.queuedMoveSources)
|
||||
BsUtil.TableClear(self.queuedMoveTargets)
|
||||
BsUtil.TableClear(self.queuedMoveCounts)
|
||||
end
|
||||
|
||||
function Inventory:QueueMove(source, target)
|
||||
---@param source table Inventory cache entry to move from.
|
||||
---@param target table Inventory cache entry to move to.
|
||||
---@param count number? How many to move (whole stack when omitted).
|
||||
function Inventory:QueueMove(source, target, count)
|
||||
table.insert(self.queuedMoveSources, source)
|
||||
table.insert(self.queuedMoveTargets, target)
|
||||
table.insert(self.queuedMoveCounts, count or 0)
|
||||
end
|
||||
|
||||
--- Perform queued item move operations.
|
||||
@@ -233,7 +238,177 @@ Bagshui:AddComponent(function()
|
||||
self.moveQueue_resort = resortAfterEachMove
|
||||
-- Reset tracking.
|
||||
self.moveQueue_LastSource = nil
|
||||
self:ProcessMoveQueue()
|
||||
if self:CanBatchMoves() then
|
||||
self:StartMoveQueueBatched()
|
||||
else
|
||||
self:ProcessMoveQueue()
|
||||
end
|
||||
end
|
||||
|
||||
--- Can the queued moves be done with ClassicAPI's C_Container.MoveItem() (no cursor, all at once)?
|
||||
--- Equipping a bag (a numeric target) still needs the cursor.
|
||||
---@return boolean
|
||||
function Inventory:CanBatchMoves()
|
||||
if type(_G.C_Container) ~= "table" or type(_G.C_Container.MoveItem) ~= "function" then
|
||||
return false
|
||||
end
|
||||
for _, target in ipairs(self.queuedMoveTargets) do
|
||||
if type(target) ~= "table" then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
--- Copy the queued moves into the batch lists and start the first round.
|
||||
function Inventory:StartMoveQueueBatched()
|
||||
self.moveBatch_sources = self.moveBatch_sources or {}
|
||||
self.moveBatch_targets = self.moveBatch_targets or {}
|
||||
self.moveBatch_counts = self.moveBatch_counts or {}
|
||||
self.moveBatch_usedSlots = self.moveBatch_usedSlots or {}
|
||||
BsUtil.TableClear(self.moveBatch_sources)
|
||||
BsUtil.TableClear(self.moveBatch_targets)
|
||||
BsUtil.TableClear(self.moveBatch_counts)
|
||||
|
||||
-- Amounts are only valid when they line up with the queue (FillBag() doesn't set any).
|
||||
local haveCounts = table.getn(self.queuedMoveCounts) == table.getn(self.queuedMoveSources)
|
||||
for i = 1, table.getn(self.queuedMoveSources) do
|
||||
table.insert(self.moveBatch_sources, self.queuedMoveSources[i])
|
||||
table.insert(self.moveBatch_targets, self.queuedMoveTargets[i])
|
||||
table.insert(self.moveBatch_counts, haveCounts and self.queuedMoveCounts[i] or 0)
|
||||
end
|
||||
self:ClearMoveQueue()
|
||||
|
||||
self.moveBatch_retries = 0
|
||||
self.moveBatch_waiting = false
|
||||
self.moveBatch_done = 0
|
||||
self:ProcessMoveQueueBatched()
|
||||
end
|
||||
|
||||
--- # 🛑 Do not call directly; use `Inventory:MoveItems()` 🛑.
|
||||
--- Instant item mover using ClassicAPI's C_Container.MoveItem() (same approach as pfUI's bag sort).
|
||||
--- Each round sends every move whose slots aren't already part of this round and aren't locked,
|
||||
--- then waits for BAG_UPDATE_DELAYED (server confirmation) before the next round. A stack that
|
||||
--- is split onto several targets, or a locked slot, simply waits for a later round.
|
||||
function Inventory:ProcessMoveQueueBatched()
|
||||
self.moveBatch_waiting = false
|
||||
|
||||
local sources = self.moveBatch_sources
|
||||
local targets = self.moveBatch_targets
|
||||
local counts = self.moveBatch_counts
|
||||
|
||||
-- Are we done?
|
||||
if table.getn(sources) <= 0 then
|
||||
self:QueueUpdate(0.1)
|
||||
if type(self.moveQueue_onComplete) == "function" then
|
||||
self.moveQueue_onComplete(self.moveQueue_success)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Set resorting for this inventory and any supplemental storage.
|
||||
if self.moveQueue_resort then
|
||||
self.forceResort = true
|
||||
if type(self.bagSwappingSupplementalStorage) == "table" then
|
||||
for _, inventoryClassName in ipairs(self.bagSwappingSupplementalStorage) do
|
||||
if Bagshui.components[inventoryClassName] and Bagshui.components[inventoryClassName].online then
|
||||
Bagshui.components[inventoryClassName].forceResort = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local usedSlots = self.moveBatch_usedSlots
|
||||
BsUtil.TableClear(usedSlots)
|
||||
local fired = 0
|
||||
local i = 1
|
||||
|
||||
while i <= table.getn(sources) do
|
||||
local source = sources[i]
|
||||
local target = targets[i]
|
||||
local sourceKey = source.bagNum .. ":" .. source.slotNum
|
||||
local targetKey = target.bagNum .. ":" .. target.slotNum
|
||||
local handled = false
|
||||
|
||||
if not usedSlots[sourceKey] and not usedSlots[targetKey] then
|
||||
local _, sourceCount, sourceLocked = _G.GetContainerItemInfo(source.bagNum, source.slotNum)
|
||||
local _, _, targetLocked = _G.GetContainerItemInfo(target.bagNum, target.slotNum)
|
||||
|
||||
if not sourceLocked and not targetLocked then
|
||||
sourceCount = sourceCount or 0
|
||||
if sourceCount > 0 then
|
||||
-- Amount to move: the queued amount, but never more than is actually there.
|
||||
local count = counts[i]
|
||||
if not count or count <= 0 or count > sourceCount then
|
||||
count = sourceCount
|
||||
end
|
||||
|
||||
-- Try to make stock states and dates make sense (same rules as ProcessMoveQueue()).
|
||||
local oldSourceStockState = source.bagshuiStockState
|
||||
local oldTargetStockState = target.bagshuiStockState
|
||||
if
|
||||
oldSourceStockState
|
||||
and oldSourceStockState ~= BS_ITEM_STOCK_STATE.NO_CHANGE
|
||||
and (
|
||||
oldSourceStockState ~= BS_ITEM_STOCK_STATE.DOWN
|
||||
or not oldTargetStockState
|
||||
or oldTargetStockState == BS_ITEM_STOCK_STATE.NO_CHANGE
|
||||
)
|
||||
then
|
||||
target.bagshuiStockState = oldSourceStockState
|
||||
end
|
||||
target.bagshuiDate = math.max(source.bagshuiDate or 0, target.bagshuiDate or 0)
|
||||
|
||||
_G.C_Container.MoveItem(source.bagNum, source.slotNum, target.bagNum, target.slotNum, count)
|
||||
fired = fired + 1
|
||||
end
|
||||
-- Done with this move (an empty source means there's nothing left to move).
|
||||
usedSlots[sourceKey] = true
|
||||
usedSlots[targetKey] = true
|
||||
handled = true
|
||||
end
|
||||
end
|
||||
|
||||
if handled then
|
||||
table.remove(sources, i)
|
||||
table.remove(targets, i)
|
||||
table.remove(counts, i)
|
||||
self.moveBatch_done = self.moveBatch_done + 1
|
||||
if type(self.moveQueue_onProgress) == "function" then
|
||||
self.moveQueue_onProgress(self.moveBatch_done)
|
||||
end
|
||||
else
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
if fired > 0 then
|
||||
self.moveBatch_retries = 0
|
||||
-- Wait for the server to confirm (BAG_UPDATE_DELAYED, see Inventory:OnEvent()),
|
||||
-- with a fallback in case that event never arrives.
|
||||
self.moveBatch_waiting = true
|
||||
Bagshui:QueueClassCallback(self, self.ProcessMoveQueueBatched, 1.0)
|
||||
return
|
||||
end
|
||||
|
||||
if table.getn(sources) <= 0 then
|
||||
-- Everything left was already empty; finish up.
|
||||
Bagshui:QueueClassCallback(self, self.ProcessMoveQueueBatched, 0.05)
|
||||
return
|
||||
end
|
||||
|
||||
-- Nothing could be moved this round (slots still locked). Retry a few times, then give up.
|
||||
self.moveBatch_retries = self.moveBatch_retries + 1
|
||||
if self.moveBatch_retries > 5 then
|
||||
for j = 1, table.getn(sources) do
|
||||
Bagshui:ShowAndLogErrorMessage(string.format(L.Error_RestackFailed, sources[j].name or "?"))
|
||||
end
|
||||
self.moveQueue_success = false
|
||||
BsUtil.TableClear(sources)
|
||||
BsUtil.TableClear(targets)
|
||||
BsUtil.TableClear(counts)
|
||||
end
|
||||
Bagshui:QueueClassCallback(self, self.ProcessMoveQueueBatched, 0.5)
|
||||
end
|
||||
|
||||
--- # 🛑 Do not call directly; use `Inventory:MoveItems()` 🛑.
|
||||
@@ -657,6 +832,8 @@ Bagshui:AddComponent(function()
|
||||
|
||||
BsUtil.TableCopyFlat(items, self.queuedMoveSources)
|
||||
BsUtil.TableCopyFlat(self.inventory[bagNum], self.queuedMoveTargets)
|
||||
-- Whole stacks (no per-move amounts).
|
||||
BsUtil.TableClear(self.queuedMoveCounts)
|
||||
|
||||
-- Prune source table if there aren't enough slots in the destination.
|
||||
if table.getn(self.queuedMoveSources) > table.getn(self.queuedMoveTargets) then
|
||||
|
||||
@@ -330,6 +330,8 @@ Bagshui:AddComponent(function()
|
||||
-- Item move queue -- used for restacking and bag swapping.
|
||||
queuedMoveSources = {},
|
||||
queuedMoveTargets = {},
|
||||
-- How many to move for each queued move (0 = the whole stack). Only Restack() sets amounts.
|
||||
queuedMoveCounts = {},
|
||||
tempMoveTargets = {},
|
||||
|
||||
-- Available empty slots (excluding profession bags -- see `Inventory:SwapBag()` comments).
|
||||
@@ -663,6 +665,11 @@ Bagshui:AddComponent(function()
|
||||
-- Handled before `self.lastEvent` is set so the ITEM_LOCK_CHANGED check in UpdateCache()
|
||||
-- still sees the event that actually caused the update.
|
||||
if event == "BAG_UPDATE_DELAYED" then
|
||||
-- A batch of instant moves (see ProcessMoveQueueBatched()) has been confirmed by the server.
|
||||
if self.moveBatch_waiting then
|
||||
self.moveBatch_waiting = false
|
||||
Bagshui:QueueClassCallback(self, self.ProcessMoveQueueBatched, 0.05)
|
||||
end
|
||||
if self.bagUpdatePending and not Bagshui.pickedUpBagSlotNum and not Bagshui.putDownBagSlotNum then
|
||||
self.bagUpdatePending = false
|
||||
self:QueueUpdate(0)
|
||||
|
||||
Reference in New Issue
Block a user