Use specialty bags as temporary space during bag swaps

AssignSpecialtySlots() sends items that fit a free specialty-bag slot
(C_Item.GetItemFamily() on the item and on the bag) there before general
space is counted; own specialty bags first, supplemental storage only
when general space runs short. Bags themselves are never assigned. Used
by EmptyBag() and GetAdditionalSlotsNeededToSwapBag(), so the bag
tooltip's "slots needed" agrees. FillBag() only refills a new specialty
bag with items that fit it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019W3rwt5PZSpEDkXYzRuRhH
This commit is contained in:
2026-11-02 05:25:17 +00:00
co-authored by Claude Opus 5.5
parent e1a94f8b50
commit f9ff62e2b0
2 changed files with 155 additions and 11 deletions
+6
View File
@@ -1,5 +1,11 @@
# Bagshui Changelog
## Unreleased
### Added
- Bag swapping can use free space in specialty bags (soul, herb, enchanting and engineering bags, quivers, ammo pouches, and Turtle's meat, fish, leather and mining bags) for the items that fit them, so a swap needs less general space. Your own specialty bags are used first, then those in the bank/bags only if general space runs short. Swapping in a specialty bag only refills it with items that fit. Needs ClassicAPI (`C_Item.GetItemFamily`).
## 1.6.9 - 2026-11-02
### Performance
+149 -11
View File
@@ -771,6 +771,123 @@ Bagshui:AddComponent(function()
-- (Reusable table for `Inventory:EmptyBag()`.)
local emptyBag_potentialAvailableSlots = {}
-- Items from the bag being emptied that go to specialty bags: { [item] = target slot }.
-- (Reusable tables for `Inventory:EmptyBag()` and `Inventory:GetAdditionalSlotsNeededToSwapBag()`.)
local emptyBag_specialtyAssignments = {}
local slotsNeeded_specialtyAssignments = {}
local assignSpecialty_items = {}
local assignSpecialty_usedSlots = {}
--- Do the two bag family bitmasks share a bit? (No bitwise operators in Lua 5.0.)
---@param a number
---@param b number
---@return boolean
local function FamiliesOverlap(a, b)
local bitValue = 1
while bitValue <= a and bitValue <= b do
if math.mod(math.floor(a / bitValue), 2) == 1 and math.mod(math.floor(b / bitValue), 2) == 1 then
return true
end
bitValue = bitValue * 2
end
return false
end
--- ClassicAPI's C_Item.GetItemFamily() tells which specialty bags an item or a bag belongs to.
---@return boolean
local function SpecialtyBagsSupported()
return type(_G.C_Item) == "table" and type(_G.C_Item.GetItemFamily) == "function"
end
--- Bag family bitmask of the bag in a container slot (0 = general-purpose or no bag).
--- Specialty bags: quiver, ammo pouch, soul, herb, enchanting, engineering, and Turtle's
--- meat/fish/leather/mining bags.
---@param containerId number
---@return number
function Inventory:GetBagFamily(containerId)
if not SpecialtyBagsSupported() or containerId <= 0 then
return 0
end
local bagLink = _G.GetInventoryItemLink("player", _G.ContainerIDToInventoryID(containerId))
return bagLink and _G.C_Item.GetItemFamily(bagLink) or 0
end
--- Bag family bitmask of an item (0 = goes in general-purpose bags only).
---@param itemLinkOrId string|number?
---@return number
local function GetItemFamilyOf(itemLinkOrId)
if not itemLinkOrId or not SpecialtyBagsSupported() then
return 0
end
return _G.C_Item.GetItemFamily(itemLinkOrId) or 0
end
--- Choose free specialty-bag slots for the items in `bagNum` that fit them, so those items don't
--- need general-purpose space during a bag swap. Own specialty bags are used first; those of
--- supplemental storage (bank/bags) only when general-purpose space here runs short.
---@param bagNum number Container being emptied.
---@param assignments table Filled with `{ [item] = target slot }`.
---@return integer remaining Items that still need a general-purpose slot.
function Inventory:AssignSpecialtySlots(bagNum, assignments)
BsUtil.TableClear(assignments)
BsUtil.TableClear(assignSpecialty_items)
BsUtil.TableClear(assignSpecialty_usedSlots)
-- Items that have to come out, with their family.
for _, item in ipairs(self.inventory[bagNum] or {}) do
if item.emptySlot ~= 1 then
table.insert(assignSpecialty_items, item)
end
end
local remaining = table.getn(assignSpecialty_items)
if remaining == 0 or not SpecialtyBagsSupported() then
return remaining
end
local function assignFrom(inventoryClass)
for _, containerId in ipairs(inventoryClass.containerIds) do
if remaining <= 0 then
return
end
local bagFamily = (containerId ~= bagNum) and inventoryClass:GetBagFamily(containerId) or 0
if bagFamily > 0 and inventoryClass.inventory[containerId] then
for slotNum = 1, (_G.GetContainerNumSlots(containerId) or 0) do
local slot = inventoryClass.inventory[containerId][slotNum]
if slot and not assignSpecialty_usedSlots[slot] and not _G.GetContainerItemLink(containerId, slotNum) then
-- First item that hasn't been placed yet and fits this bag.
for _, item in ipairs(assignSpecialty_items) do
if
not assignments[item]
-- Bags report the family of what they hold, but can't go in a specialty bag.
and not BsItemInfo:IsContainer(item)
and FamiliesOverlap(GetItemFamilyOf(item.itemLink or item.id), bagFamily)
then
assignments[item] = slot
assignSpecialty_usedSlots[slot] = true
remaining = remaining - 1
break
end
end
end
end
end
end
end
assignFrom(self)
if remaining > self:GetAdjustedEmptySlotCount(bagNum) and type(self.bagSwappingSupplementalStorage) == "table" then
for _, inventoryClassName in ipairs(self.bagSwappingSupplementalStorage) do
local supplemental = Bagshui.components[inventoryClassName]
if supplemental and supplemental.online then
assignFrom(supplemental)
end
end
end
return remaining
end
--- Remove all items from a bag.
--- Automatically backs up the new locations to `Inventory.tempMoveTargets`.
---@param bagNum number Container to empty.
@@ -787,11 +904,14 @@ Bagshui:AddComponent(function()
-- loop where we fill `queuedMoveSources/Targets`.
BsUtil.TableCopyFlat(self.emptyGenericContainerSlots, emptyBag_potentialAvailableSlots)
-- Items that fit a specialty bag (herbs, soul shards, arrows...) go to one first.
local itemsNeedingGeneralSlots = self:AssignSpecialtySlots(bagNum, emptyBag_specialtyAssignments)
-- Find out how much free space there is, excluding the bag being emptied.
local availableEmptySlotCount = self:GetAdjustedEmptySlotCount(bagNum)
-- Extra space is needed. See if we can find any.
if (self.containers[bagNum].slotsFilled or 0) > availableEmptySlotCount then
if itemsNeedingGeneralSlots > availableEmptySlotCount then
availableEmptySlotCount = availableEmptySlotCount
+ self:GetSupplementalEmptySlotCount(emptyBag_potentialAvailableSlots)
end
@@ -799,7 +919,7 @@ Bagshui:AddComponent(function()
-- Before going any further, make sure there's enough space.
-- This is a variable instead of being in the `if` just below because it
-- needs to be updated if we run out of slots in `emptyBag_potentialAvailableSlots`.
local readyToEmpty = self.containers[bagNum].slotsFilled <= availableEmptySlotCount
local readyToEmpty = itemsNeedingGeneralSlots <= availableEmptySlotCount
if readyToEmpty then
-- Reset queues used by `Inventory:ProcessMoveQueue()`
@@ -813,11 +933,15 @@ Bagshui:AddComponent(function()
-- Find a corresponding empty slot for every item in this bag.
for _, item in ipairs(self.inventory[bagNum]) do
if item.emptySlot ~= 1 then
repeat
nextTargetSlotIndex = nextTargetSlotIndex + 1
nextTargetSlot = emptyBag_potentialAvailableSlots[nextTargetSlotIndex]
until not nextTargetSlot or nextTargetSlot.bagNum ~= bagNum
if not nextTargetSlot then
local targetSlot = emptyBag_specialtyAssignments[item]
if not targetSlot then
repeat
nextTargetSlotIndex = nextTargetSlotIndex + 1
nextTargetSlot = emptyBag_potentialAvailableSlots[nextTargetSlotIndex]
until not nextTargetSlot or nextTargetSlot.bagNum ~= bagNum
targetSlot = nextTargetSlot
end
if not targetSlot then
-- This hopefully shouldn't be hit but just in case.
readyToEmpty = false
break
@@ -826,12 +950,12 @@ Bagshui:AddComponent(function()
if type(itemsToTrack) == "table" then
for i = 1, table.getn(itemsToTrack) do
if itemsToTrack[i] == item then
itemsToTrack[i] = nextTargetSlot
itemsToTrack[i] = targetSlot
end
end
end
-- Instructions for MoveItems().
self:QueueMove(item, nextTargetSlot)
self:QueueMove(item, targetSlot)
end
end
end
@@ -844,8 +968,9 @@ Bagshui:AddComponent(function()
-- Move! Those! Items!
self:MoveItems(onComplete, onProgress, true)
-- No need to hold onto this.
-- No need to hold onto these.
BsUtil.TableClear(emptyBag_potentialAvailableSlots)
BsUtil.TableClear(emptyBag_specialtyAssignments)
else
-- Failure state.
self:ClearMoveQueue()
@@ -872,6 +997,17 @@ Bagshui:AddComponent(function()
BsUtil.TableCopyFlat(items, self.queuedMoveSources)
BsUtil.TableCopyFlat(self.inventory[bagNum], self.queuedMoveTargets)
-- The new bag is a specialty bag: only bring back the items that fit it.
local newBagFamily = self:GetBagFamily(bagNum)
if newBagFamily > 0 then
for i = table.getn(self.queuedMoveSources), 1, -1 do
local source = self.queuedMoveSources[i]
if not FamiliesOverlap(GetItemFamilyOf(_G.GetContainerItemLink(source.bagNum, source.slotNum)), newBagFamily) then
table.remove(self.queuedMoveSources, i)
end
end
end
-- Whole stacks (no per-move amounts).
BsUtil.TableClear(self.queuedMoveCounts)
@@ -952,7 +1088,9 @@ Bagshui:AddComponent(function()
---@return integer slotsNeeded
---@return table<string, true>? inventoriesChecked Second return value from `Inventory:GetSupplementalEmptySlotCount()`.
function Inventory:GetAdditionalSlotsNeededToSwapBag(bagNum)
local filledSlots = (self.containers[bagNum].slotsFilled or 0)
-- Items that fit a free specialty-bag slot don't need general-purpose space.
local filledSlots = self:AssignSpecialtySlots(bagNum, slotsNeeded_specialtyAssignments)
BsUtil.TableClear(slotsNeeded_specialtyAssignments)
if filledSlots == 0 then
return 0