Fix up quick stack

This commit is contained in:
2025-04-01 20:40:19 +02:00
parent bb7dc1da32
commit 88187358e8
3 changed files with 137 additions and 158 deletions

View File

@@ -5,20 +5,14 @@ if not CLIENT then return end
local utils = require("Cyka.utils") local utils = require("Cyka.utils")
local dump = require("Cyka.dump") local dump = require("Cyka.dump")
---@class ItemLocation
---@field inventory Barotrauma.ItemInventory
---@field slotIndex number
---@field depth number
---@field maxFits number
-- The resulting item tree is a table where the key is an ID of an item -- The resulting item tree is a table where the key is an ID of an item
-- And the value is an object that represents where that item is located -- And the value is an object that represents where that item is located
-- In our inventory -- In our inventory
-- Special case are empty slots where any item fits -- Special case are empty slots where any item fits
---@param inventory Barotrauma.ItemInventory ---@param inventory Barotrauma.Inventory
---@param itemTree table<string, ItemLocation[]> ---@param itemTree? table<string, InventorySlot[]>
---@param depth number ---@param depth? number
---@return table<string, ItemLocation[]> ---@return table<string, InventorySlot[]>
local function buildItemTree(inventory, itemTree, depth) local function buildItemTree(inventory, itemTree, depth)
itemTree = itemTree or {} itemTree = itemTree or {}
depth = depth or 0 depth = depth or 0
@@ -29,38 +23,24 @@ local function buildItemTree(inventory, itemTree, depth)
-- One slot can have one item but multiple of it -- One slot can have one item but multiple of it
-- The number of an item in a slot is #slot.items -- The number of an item in a slot is #slot.items
for slotIndex, slot in ipairs(inventory.slots) do for slotIndex, _ in ipairs(inventory.slots) do
-- MyModGlobal.debugPrint(string.format("Building item tree for inventory at slot index: %d", slotIndex)) local invSlot = MyModGlobal.InventorySlot.new(inventory, slotIndex):with({ depth = depth })
-- MyModGlobal.debugPrint(string.format("Slot %d has %d items", slotIndex, #slot.items)) if not invSlot.item then
if #slot.items == 0 then
-- MyModGlobal.debugPrint(string.format("Slot %d is empty, adding to itemTree as 'empty'", slotIndex)) -- MyModGlobal.debugPrint(string.format("Slot %d is empty, adding to itemTree as 'empty'", slotIndex))
itemTree['empty'] = itemTree['empty'] or {} itemTree['empty'] = itemTree['empty'] or {}
itemTree['empty'][#itemTree['empty'] + 1] = { itemTree['empty'][#itemTree['empty'] + 1] = invSlot
inventory = inventory,
slotIndex = slotIndex - 1,
maxFits = 60,
depth = depth
}
-- MyModGlobal.debugPrint(string.format("Added empty slot to itemTree at index: %d", slotIndex)) -- MyModGlobal.debugPrint(string.format("Added empty slot to itemTree at index: %d", slotIndex))
else else
---@type Barotrauma.Item local identifier = invSlot.item.Prefab.Identifier.Value
local item = slot.items[1]
local identifier = item.Prefab.Identifier.Value
-- MyModGlobal.debugPrint(string.format("Found item: %s with identifier: %s", item.Name, identifier))
itemTree[identifier] = itemTree[identifier] or {} itemTree[identifier] = itemTree[identifier] or {}
-- We DO want even slots with maxFits = 0 -- We DO want even slots with maxFits = 0
-- Because that indicates that we DO HAVE the item -- Because that indicates that we DO HAVE the item
-- At all -- At all
-- And based on that we decide to move it -- And based on that we decide to move it
itemTree[identifier][#itemTree[identifier] + 1] = { itemTree[identifier][#itemTree[identifier] + 1] = invSlot
inventory = inventory,
slotIndex = slotIndex - 1,
maxFits = slot.HowManyCanBePut(item.Prefab),
depth = depth
}
-- MyModGlobal.debugPrint(string.format("Added item to itemTree under identifier: %s", identifier)) -- MyModGlobal.debugPrint(string.format("Added item to itemTree under identifier: %s", identifier))
local tags = item.Prefab.Tags local tags = invSlot.item.Prefab.Tags
local shouldSuss = false local shouldSuss = false
for tag in tags do for tag in tags do
if tag.value:find("container") then if tag.value:find("container") then
@@ -71,7 +51,7 @@ local function buildItemTree(inventory, itemTree, depth)
if shouldSuss then if shouldSuss then
-- MyModGlobal.debugPrint(string.format("Searching inside %s for nested containers", item.Name)) -- MyModGlobal.debugPrint(string.format("Searching inside %s for nested containers", item.Name))
buildItemTree(item.OwnInventory, itemTree, depth + 1) buildItemTree(invSlot.item.OwnInventory, itemTree, depth + 1)
end end
end end
end end
@@ -81,19 +61,20 @@ local function buildItemTree(inventory, itemTree, depth)
end end
-- We would like to fill larger stacks first -- We would like to fill larger stacks first
---@param itemTree table<string, ItemLocation[]> ---@param itemTree table<string, InventorySlot[]>
---@return table<string, ItemLocation[]> ---@return table<string, InventorySlot[]>
local function sortItemTree(itemTree) local function sortItemTree(itemTree)
for _, item in pairs(itemTree) do for _, item in pairs(itemTree) do
table.sort(item, function(a, b) table.sort(item, function(a, b)
---@cast a ItemLocation ---@cast a InventorySlot
---@cast b ItemLocation ---@cast b InventorySlot
local maxfitsA, maxfitsB = a:maxFits(), b:maxFits()
if a.depth ~= b.depth then if a.depth ~= b.depth then
return a.depth < b.depth return a.depth < b.depth
elseif a.maxFits ~= b.maxFits then elseif maxfitsA ~= maxfitsB then
return a.maxFits > b.maxFits return maxfitsA > maxfitsB
else else
return a.slotIndex < b.slotIndex return a.slotIndex0 < b.slotIndex0
end end
end) end)
end end
@@ -102,16 +83,16 @@ local function sortItemTree(itemTree)
end end
---@param item Barotrauma.Item ---@param item Barotrauma.Item
---@param itemTree table<string, ItemLocation[]> ---@param itemTree table<string, InventorySlot[]>
---@param force boolean ---@param force boolean
---@return string ---@return string?
local function tryMoveItem(item, itemTree, force) local function tryMoveItem(item, itemTree, force)
-- MyModGlobal.debugPrint(string.format("Attempting to move item: %s", item.Prefab.Identifier.Value)) -- MyModGlobal.debugPrint(string.format("Attempting to move item: %s", item.Prefab.Identifier.Value))
force = force or false force = force or false
local location = itemTree[item.Prefab.Identifier.Value] local location = itemTree[item.Prefab.Identifier.Value]
if not location and not force then if not location and not force then
-- MyModGlobal.debugPrint("No locations for item, not stacking (not forced)") -- MyModGlobal.debugPrint("No locations for item, not stacking (not forced)")
return nil, "No locations for item, not stacking (not forced)" return "No locations for item, not stacking (not forced)"
end end
-- MyModGlobal.debugPrint(string.format("Attempting to move item: %s", item.Prefab.Identifier.Value)) -- MyModGlobal.debugPrint(string.format("Attempting to move item: %s", item.Prefab.Identifier.Value))
-- MyModGlobal.DumpTable(location) -- MyModGlobal.DumpTable(location)
@@ -121,23 +102,13 @@ local function tryMoveItem(item, itemTree, force)
-- First try to move to existing stacks -- First try to move to existing stacks
for _, itemLocation in ipairs(location) do for _, itemLocation in ipairs(location) do
-- We cannot stack items with decreased condition -- We cannot stack items with decreased condition
local canBePut = itemLocation.inventory.CanBePutInSlot(item.Prefab, itemLocation.slotIndex, item.Condition) local canFit = itemLocation:canFit(item.Prefab)
-- MyModGlobal.debugPrint(string.format("Can be put in slot %d: %s", itemLocation.slotIndex, tostring(canBePut))) if canFit then
-- There's no more guess work, if we call move then we must be sure we can move
if itemLocation.maxFits > 0 and canBePut then utils.enqueueMove(item, itemLocation)
moved = moved or moved = true
itemLocation.inventory.TryPutItem(item, itemLocation.slotIndex, false, true, Character.Controlled,
true)
if moved then
itemLocation.maxFits = itemLocation.inventory.HowManyCanBePut(item.Prefab, itemLocation.slotIndex)
break break
end end
-- if moved then
-- MyModGlobal.debugPrint(string.format("Moved item to existing stack at slot index: %d", itemLocation .slotIndex))
-- else
-- MyModGlobal.debugPrint(string.format("Failed to move item to existing stack at slot index: %d", itemLocation .slotIndex))
-- end
end
end end
end end
@@ -149,29 +120,17 @@ local function tryMoveItem(item, itemTree, force)
return "No empty slots found" return "No empty slots found"
end end
for _, itemLocation in ipairs(itemTree['empty']) do for _, itemLocation in ipairs(itemTree['empty']) do
local maxFits = itemLocation.maxFits
-- These empty slots are not guranteed to be empty, ironically -- These empty slots are not guranteed to be empty, ironically
-- After we insert an item into one it's no longer empty -- After we insert an item into one it's no longer empty
-- But it still is in the empty table -- But it still is in the empty table
-- So we want to make sure we can insert our item -- So we want to make sure we can insert our item
-- Into the maybe empty slots -- Into the maybe empty slots
itemLocation.maxFits = itemLocation.inventory.HowManyCanBePut(item.Prefab, itemLocation.slotIndex) local canFit = itemLocation:canFit(item.Prefab)
if canFit then
if maxFits > 0 then utils.enqueueMove(item, itemLocation)
-- MyModGlobal.debugPrint(string.format("Trying to move item to empty slot at index: %d", itemLocation.slotIndex)) moved = true
moved = moved or
itemLocation.inventory.TryPutItem(item, itemLocation.slotIndex, true, false, Character.Controlled,
true)
if moved then
itemLocation.maxFits = itemLocation.inventory.HowManyCanBePut(item.Prefab, itemLocation.slotIndex)
break break
end end
-- if moved then
-- MyModGlobal.debugPrint(string.format("Moved item to empty slot at index: %d", itemLocation.slotIndex))
-- else
-- MyModGlobal.debugPrint(string.format("Failed to move item to empty slot at index: %d", itemLocation.slotIndex))
-- end
end
end end
end end
@@ -185,8 +144,8 @@ local function tryMoveItem(item, itemTree, force)
end end
---@param items Barotrauma.Item[] ---@param items Barotrauma.Item[]
---@param itemTree table<string, ItemLocation[]> ---@param itemTree table<string, InventorySlot[]>
---@param force boolean ---@param force? boolean
---@return string[] ---@return string[]
local function tryMoveItems(items, itemTree, force) local function tryMoveItems(items, itemTree, force)
force = force or false force = force or false
@@ -202,7 +161,7 @@ local function tryMoveItems(items, itemTree, force)
end end
---@param character Barotrauma.Character ---@param character Barotrauma.Character
---@return table<string, ItemLocation[]>, string ---@return table<string, InventorySlot[]>, string?
local function tryBuildCharacterItemTree(character) local function tryBuildCharacterItemTree(character)
local itemTree = {} local itemTree = {}
-- MyModGlobal.debugPrint(string.format("Preparing to stack items into the bag...")) -- MyModGlobal.debugPrint(string.format("Preparing to stack items into the bag..."))
@@ -267,7 +226,7 @@ end
-- Function to quickly stack items from inventory to containers -- Function to quickly stack items from inventory to containers
-- 6 and 7 are hands -- 6 and 7 are hands
-- 9..18 are main slots -- 9..18 are main slots
local inventorySlotsToStack = { 6, 7, } -- local inventorySlotsToStack = { 6, 7, }
-- local inventorySlotsToStack = { 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 } -- local inventorySlotsToStack = { 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 }
---@param character Barotrauma.Character ---@param character Barotrauma.Character
local function quickStackItems(character) local function quickStackItems(character)
@@ -276,12 +235,10 @@ local function quickStackItems(character)
-- Then stack all items from the parent inventory into the mouseover container -- Then stack all items from the parent inventory into the mouseover container
local mouseover = utils.getFirstSlotUnderCursor() local mouseover = utils.getFirstSlotUnderCursor()
if mouseover then if mouseover then
---@type Barotrauma.Item local itemInventory = mouseover.item.OwnInventory
local slotItem = mouseover.slot.items[1]
local itemInventory = slotItem.OwnInventory
if itemInventory then if itemInventory then
MyModGlobal.debugPrint(string.format("Item inventory found: %s", tostring(itemInventory))) MyModGlobal.debugPrint(string.format("Item inventory found: %s", tostring(itemInventory)))
local err = stackToContainer(slotItem) local err = stackToContainer(mouseover.item)
if err then if err then
MyModGlobal.debugPrint(string.format("Error stacking items to container: %s", err)) MyModGlobal.debugPrint(string.format("Error stacking items to container: %s", err))
end end
@@ -341,6 +298,7 @@ local function quickStackItems(character)
-- end -- end
-- end -- end
-- TODO: enqueueOpenContainers?
local openContainers = utils.getOpenContainers() local openContainers = utils.getOpenContainers()
for _, container in ipairs(openContainers) do for _, container in ipairs(openContainers) do
local inventories = container.OwnInventories local inventories = container.OwnInventories
@@ -367,48 +325,38 @@ local function stackToCursor()
return return
end end
local item, slot
local function predicate(ititem) local function predicate(ititem)
if ititem.Prefab.Identifier.Value == item.Prefab.Identifier.Value then for _, invSlot in ipairs(slots) do
if item == ititem then return false end if invSlot:canFit(ititem.Prefab) then
-- We are moving items in the predicate because we expect to only utils.enqueueMove(ititem, invSlot)
-- Select a small subset of all items
-- And it is much easier to let the game decide when we can not move
-- Any more items (via return value of TryPutItem)
-- And we then know that we can safely stop
-- UPDATE: OK well that was a stupid idea, it returns an error for other shit as well
-- What other shit? Wish I knew
-- So we'll use HowManyCanBePut instead...
local moved = slot.inventory.TryPutItem(ititem, slot.slotIndex - 1, false, true, Character.Controlled,
true)
if not moved then
MyModGlobal.debugPrint(string.format("Failed to move item %s to slot %d", ititem.Name, slot
.slotIndex - 1))
-- return false, true
end
local maxFits = slot.inventory.HowManyCanBePut(ititem.Prefab, slot.slotIndex - 1)
if maxFits <= 0 then
MyModGlobal.debugPrint(string.format("Item %s has no more fits in slot %d", ititem.Name, slot
.slotIndex - 1))
return false, true
end end
end end
local haveSpace = false
for _, invSlot in ipairs(slots) do
-- Empty slot or has space for more items
if (invSlot.stackSize < invSlot.maxStackSize) or not invSlot.item then
haveSpace = true
break
end
end
if not haveSpace then return false, true end
end end
---@type EnqueueOptions
local options = {
itemPredicate = predicate,
recurse = true,
}
-- We gotta do a little juggling... -- We gotta do a little juggling...
for _, sslot in ipairs(slots) do for _, invSlot in ipairs(slots) do
slot = sslot if not invSlot.item then
local items
if not slot.slot.items or #slot.slot.items == 0 then
MyModGlobal.debugPrint("No items in slot") MyModGlobal.debugPrint("No items in slot")
goto continue goto continue
end end
item = slot.slot.items[1] MyModGlobal.debugPrint(string.format("Stacking all player items to %s", invSlot.item.Prefab.Identifier.Value))
MyModGlobal.debugPrint(string.format("Stacking all player items to %s", item.Prefab.Identifier.Value)) utils.enqueuePlayerItems(options)
items = {} utils.enqueueOpenContainers(options)
utils.enqueueAllPlayerItems(items, predicate)
utils.enqueueOpenContainers(items, predicate)
::continue:: ::continue::
end end
end end
@@ -420,45 +368,38 @@ local function stackAllToCursor()
return return
end end
for _, slot in ipairs(slots) do local function predicate(ititem)
local item, predicate for _, invSlot in ipairs(slots) do
if not slot.slot.items or #slot.slot.items == 0 then if invSlot:canFit(ititem.Prefab) then
utils.enqueueMove(ititem, invSlot)
end
end
local haveSpace = false
for _, invSlot in ipairs(slots) do
-- Empty slot or has space for more items
if (invSlot.stackSize < invSlot.maxStackSize) or not invSlot.item then
haveSpace = true
break
end
end
if not haveSpace then return false, true end
end
---@type EnqueueOptions
local options = {
itemPredicate = predicate,
recurse = true,
}
for _, invSlot in ipairs(slots) do
if not invSlot.item then
MyModGlobal.debugPrint("No items in slot") MyModGlobal.debugPrint("No items in slot")
goto continue goto continue
end end
item = slot.slot.items[1] MyModGlobal.debugPrint(string.format("Stacking all items to %s", invSlot.item.Prefab.Identifier.Value))
MyModGlobal.debugPrint(string.format("Stacking all items to %s", item.Prefab.Identifier.Value)) utils.enqueueSubmarineItems(options)
predicate = function(ititem) utils.enqueuePlayerItems(options)
if ititem.Prefab.Identifier.Value == item.Prefab.Identifier.Value then
if item == ititem then return false end
-- We are moving items in the predicate because we expect to only
-- Select a small subset of all items
-- And it is much easier to let the game decide when we can not move
-- Any more items (via return value of TryPutItem)
-- And we then know that we can safely stop
-- UPDATE: OK well that was a stupid idea, it returns an error for other shit as well
-- What other shit? Wish I knew
-- So we'll use HowManyCanBePut instead...
local moved = slot.inventory.TryPutItem(ititem, slot.slotIndex - 1, false, true, Character.Controlled,
true)
if not moved then
MyModGlobal.debugPrint(string.format("Failed to move item %s to slot %d", ititem.Name, slot
.slotIndex - 1))
-- return false, true
end
local maxFits = slot.inventory.HowManyCanBePut(ititem.Prefab, slot.slotIndex - 1)
if maxFits <= 0 then
MyModGlobal.debugPrint(string.format("Item %s has no more fits in slot %d", ititem.Name, slot
.slotIndex - 1))
return false, true
end
end
end
utils.enqueueAllSubmarineItems({}, predicate)
utils.enqueueAllPlayerItems({}, predicate)
::continue:: ::continue::
end end
end end

View File

@@ -27,7 +27,7 @@ local function tryUnloadSlot(invSlot)
-- Where can we put our toUnload items? -- Where can we put our toUnload items?
local nearbySlots = invSlot:getNearbySlots(function(islot) local nearbySlots = invSlot:getNearbySlots(function(islot)
local isEmpty = islot.slot.items and #islot.slot.items == 0 local isEmpty = not islot.item
if isEmpty then return true end if isEmpty then return true end
for prefab, _ in pairs(toUnloadByPrefab) do for prefab, _ in pairs(toUnloadByPrefab) do

View File

@@ -4,15 +4,24 @@
---@class Barotrauma.Inventory.ItemSlot ---@class Barotrauma.Inventory.ItemSlot
---@field items Barotrauma.Item[] ---@field items Barotrauma.Item[]
---@class HollowInventorySlot
---@field inventory? Barotrauma.Inventory
---@field slotIndex1? number Lua based item slots
---@field slotIndex0? number Barotrauma API based item slots
---@field item? Barotrauma.Item
---@field stackSize? number
---@field maxStackSize? number
---@field depth? number Currently almost always 0
-- local globalInventorySlotCache = {} -- local globalInventorySlotCache = {}
---@class InventorySlot ---@class InventorySlot ---@field slot Barotrauma.Inventory.ItemSlot
---@field slot Barotrauma.Inventory.ItemSlot
---@field inventory Barotrauma.Inventory ---@field inventory Barotrauma.Inventory
---@field slotIndex1 number Lua based item slots ---@field slotIndex1 number Lua based item slots
---@field slotIndex0 number Barotrauma API based item slots ---@field slotIndex0 number Barotrauma API based item slots
---@field item Barotrauma.Item ---@field item Barotrauma.Item
---@field stackSize number ---@field stackSize number
---@field maxStackSize number ---@field maxStackSize number
---@field depth number Currently almost always 0
-- ---@field lastUpdated number -- ---@field lastUpdated number
MyModGlobal.InventorySlot = { MyModGlobal.InventorySlot = {
---@param inventory Barotrauma.Inventory ---@param inventory Barotrauma.Inventory
@@ -27,6 +36,7 @@ MyModGlobal.InventorySlot = {
self.slotIndex0 = slotIndex1 - 1 self.slotIndex0 = slotIndex1 - 1
self.stackSize = 0 self.stackSize = 0
self.maxStackSize = 0 self.maxStackSize = 0
self.depth = 0
-- self:update() -- self:update()
if inventory and inventory.slots and #inventory.slots > 0 then if inventory and inventory.slots and #inventory.slots > 0 then
@@ -42,6 +52,33 @@ MyModGlobal.InventorySlot = {
return self return self
end, end,
--- A very weird builder indeed
---@param self InventorySlot
---@param other HollowInventorySlot
with = function(self, other)
if other.inventory ~= nil then
self.inventory = other.inventory
end
if other.slotIndex1 ~= nil then
self.slotIndex1 = other.slotIndex1
end
if other.slotIndex0 ~= nil then
self.slotIndex0 = other.slotIndex0
end
if other.item ~= nil then
self.item = other.item
end
if other.stackSize ~= nil then
self.stackSize = other.stackSize
end
if other.maxStackSize ~= nil then
self.maxStackSize = other.maxStackSize
end
if other.depth ~= nil then
self.depth = other.depth
end
return self
end,
---@param self InventorySlot ---@param self InventorySlot
---@param item Barotrauma.Item ---@param item Barotrauma.Item
pretendMoved = function(self, item) pretendMoved = function(self, item)
@@ -49,10 +86,6 @@ MyModGlobal.InventorySlot = {
MyModGlobal.debugPrint("Error pretending moved but it was moved to nil inventory") MyModGlobal.debugPrint("Error pretending moved but it was moved to nil inventory")
return return
end end
if not self.slot then
MyModGlobal.debugPrint("Error pretending moved but it was moved to nil slot")
return
end
-- Slot was previously empty, we want to figure out its max stack for the new item -- Slot was previously empty, we want to figure out its max stack for the new item
if not self.item then if not self.item then
self.maxStackSize = item.Prefab.GetMaxStackSize(self.inventory) self.maxStackSize = item.Prefab.GetMaxStackSize(self.inventory)
@@ -158,6 +191,11 @@ MyModGlobal.InventorySlot = {
---@return boolean ---@return boolean
canFit = function(self, itemPrefab) canFit = function(self, itemPrefab)
return self:howManyCanFit(itemPrefab) > 0 return self:howManyCanFit(itemPrefab) > 0
end,
---@param self InventorySlot
---@return number
maxFits = function(self)
return self.maxStackSize - self.stackSize
end end
-- hash = function(self) -- hash = function(self)
-- return string.format("%s:%d:%d", tostring(self.inventory), self.slotIndex1, self.slotIndex0) -- return string.format("%s:%d:%d", tostring(self.inventory), self.slotIndex1, self.slotIndex0)
@@ -605,7 +643,7 @@ local function getFirstSlotUnderCursor()
if err then return slots, err end if err then return slots, err end
if #slots == 0 then return slots, "No slots found under cursor" end if #slots == 0 then return slots, "No slots found under cursor" end
for _, slot in ipairs(slots) do for _, slot in ipairs(slots) do
if slot.slot.items and #slot.slot.items > 0 then if slot.item then
return slot return slot
end end
end end