From f9ff62e2b03a2ae52fb25b1c5ddf9c064bd7a2fa Mon Sep 17 00:00:00 2001 From: OEVG <950+oevg@noreply.octowow.st> Date: Mon, 2 Nov 2026 05:25:17 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_019W3rwt5PZSpEDkXYzRuRhH --- Changelog.md | 6 ++ Components/Inventory.Actions.lua | 160 ++++++++++++++++++++++++++++--- 2 files changed, 155 insertions(+), 11 deletions(-) diff --git a/Changelog.md b/Changelog.md index 0e7026c..10c01a5 100644 --- a/Changelog.md +++ b/Changelog.md @@ -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 diff --git a/Components/Inventory.Actions.lua b/Components/Inventory.Actions.lua index 0f6ddeb..8725ae4 100644 --- a/Components/Inventory.Actions.lua +++ b/Components/Inventory.Actions.lua @@ -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? 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