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 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
-- And the value is an object that represents where that item is located
-- In our inventory
-- Special case are empty slots where any item fits
---@param inventory Barotrauma.ItemInventory
---@param itemTree table<string, ItemLocation[]>
---@param depth number
---@return table<string, ItemLocation[]>
---@param inventory Barotrauma.Inventory
---@param itemTree? table<string, InventorySlot[]>
---@param depth? number
---@return table<string, InventorySlot[]>
local function buildItemTree(inventory, itemTree, depth)
itemTree = itemTree or {}
depth = depth or 0
@@ -29,38 +23,24 @@ local function buildItemTree(inventory, itemTree, depth)
-- One slot can have one item but multiple of it
-- The number of an item in a slot is #slot.items
for slotIndex, slot in ipairs(inventory.slots) do
-- MyModGlobal.debugPrint(string.format("Building item tree for inventory at slot index: %d", slotIndex))
-- MyModGlobal.debugPrint(string.format("Slot %d has %d items", slotIndex, #slot.items))
if #slot.items == 0 then
for slotIndex, _ in ipairs(inventory.slots) do
local invSlot = MyModGlobal.InventorySlot.new(inventory, slotIndex):with({ depth = depth })
if not invSlot.item then
-- MyModGlobal.debugPrint(string.format("Slot %d is empty, adding to itemTree as 'empty'", slotIndex))
itemTree['empty'] = itemTree['empty'] or {}
itemTree['empty'][#itemTree['empty'] + 1] = {
inventory = inventory,
slotIndex = slotIndex - 1,
maxFits = 60,
depth = depth
}
itemTree['empty'][#itemTree['empty'] + 1] = invSlot
-- MyModGlobal.debugPrint(string.format("Added empty slot to itemTree at index: %d", slotIndex))
else
---@type Barotrauma.Item
local item = slot.items[1]
local identifier = item.Prefab.Identifier.Value
-- MyModGlobal.debugPrint(string.format("Found item: %s with identifier: %s", item.Name, identifier))
local identifier = invSlot.item.Prefab.Identifier.Value
itemTree[identifier] = itemTree[identifier] or {}
-- We DO want even slots with maxFits = 0
-- Because that indicates that we DO HAVE the item
-- At all
-- And based on that we decide to move it
itemTree[identifier][#itemTree[identifier] + 1] = {
inventory = inventory,
slotIndex = slotIndex - 1,
maxFits = slot.HowManyCanBePut(item.Prefab),
depth = depth
}
itemTree[identifier][#itemTree[identifier] + 1] = invSlot
-- 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
for tag in tags do
if tag.value:find("container") then
@@ -71,7 +51,7 @@ local function buildItemTree(inventory, itemTree, depth)
if shouldSuss then
-- 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
@@ -81,19 +61,20 @@ local function buildItemTree(inventory, itemTree, depth)
end
-- We would like to fill larger stacks first
---@param itemTree table<string, ItemLocation[]>
---@return table<string, ItemLocation[]>
---@param itemTree table<string, InventorySlot[]>
---@return table<string, InventorySlot[]>
local function sortItemTree(itemTree)
for _, item in pairs(itemTree) do
table.sort(item, function(a, b)
---@cast a ItemLocation
---@cast b ItemLocation
---@cast a InventorySlot
---@cast b InventorySlot
local maxfitsA, maxfitsB = a:maxFits(), b:maxFits()
if a.depth ~= b.depth then
return a.depth < b.depth
elseif a.maxFits ~= b.maxFits then
return a.maxFits > b.maxFits
elseif maxfitsA ~= maxfitsB then
return maxfitsA > maxfitsB
else
return a.slotIndex < b.slotIndex
return a.slotIndex0 < b.slotIndex0
end
end)
end
@@ -102,16 +83,16 @@ local function sortItemTree(itemTree)
end
---@param item Barotrauma.Item
---@param itemTree table<string, ItemLocation[]>
---@param itemTree table<string, InventorySlot[]>
---@param force boolean
---@return string
---@return string?
local function tryMoveItem(item, itemTree, force)
-- MyModGlobal.debugPrint(string.format("Attempting to move item: %s", item.Prefab.Identifier.Value))
force = force or false
local location = itemTree[item.Prefab.Identifier.Value]
if not location and not force then
-- 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
-- MyModGlobal.debugPrint(string.format("Attempting to move item: %s", item.Prefab.Identifier.Value))
-- MyModGlobal.DumpTable(location)
@@ -121,23 +102,13 @@ local function tryMoveItem(item, itemTree, force)
-- First try to move to existing stacks
for _, itemLocation in ipairs(location) do
-- We cannot stack items with decreased condition
local canBePut = itemLocation.inventory.CanBePutInSlot(item.Prefab, itemLocation.slotIndex, item.Condition)
-- MyModGlobal.debugPrint(string.format("Can be put in slot %d: %s", itemLocation.slotIndex, tostring(canBePut)))
if itemLocation.maxFits > 0 and canBePut then
moved = moved or
itemLocation.inventory.TryPutItem(item, itemLocation.slotIndex, false, true, Character.Controlled,
true)
if moved then
itemLocation.maxFits = itemLocation.inventory.HowManyCanBePut(item.Prefab, itemLocation.slotIndex)
local canFit = itemLocation:canFit(item.Prefab)
if canFit then
-- There's no more guess work, if we call move then we must be sure we can move
utils.enqueueMove(item, itemLocation)
moved = true
break
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
@@ -149,29 +120,17 @@ local function tryMoveItem(item, itemTree, force)
return "No empty slots found"
end
for _, itemLocation in ipairs(itemTree['empty']) do
local maxFits = itemLocation.maxFits
-- These empty slots are not guranteed to be empty, ironically
-- After we insert an item into one it's no longer empty
-- But it still is in the empty table
-- So we want to make sure we can insert our item
-- Into the maybe empty slots
itemLocation.maxFits = itemLocation.inventory.HowManyCanBePut(item.Prefab, itemLocation.slotIndex)
if maxFits > 0 then
-- MyModGlobal.debugPrint(string.format("Trying to move item to empty slot at index: %d", itemLocation.slotIndex))
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)
local canFit = itemLocation:canFit(item.Prefab)
if canFit then
utils.enqueueMove(item, itemLocation)
moved = true
break
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
@@ -185,8 +144,8 @@ local function tryMoveItem(item, itemTree, force)
end
---@param items Barotrauma.Item[]
---@param itemTree table<string, ItemLocation[]>
---@param force boolean
---@param itemTree table<string, InventorySlot[]>
---@param force? boolean
---@return string[]
local function tryMoveItems(items, itemTree, force)
force = force or false
@@ -202,7 +161,7 @@ local function tryMoveItems(items, itemTree, force)
end
---@param character Barotrauma.Character
---@return table<string, ItemLocation[]>, string
---@return table<string, InventorySlot[]>, string?
local function tryBuildCharacterItemTree(character)
local itemTree = {}
-- 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
-- 6 and 7 are hands
-- 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 }
---@param character Barotrauma.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
local mouseover = utils.getFirstSlotUnderCursor()
if mouseover then
---@type Barotrauma.Item
local slotItem = mouseover.slot.items[1]
local itemInventory = slotItem.OwnInventory
local itemInventory = mouseover.item.OwnInventory
if itemInventory then
MyModGlobal.debugPrint(string.format("Item inventory found: %s", tostring(itemInventory)))
local err = stackToContainer(slotItem)
local err = stackToContainer(mouseover.item)
if err then
MyModGlobal.debugPrint(string.format("Error stacking items to container: %s", err))
end
@@ -341,6 +298,7 @@ local function quickStackItems(character)
-- end
-- end
-- TODO: enqueueOpenContainers?
local openContainers = utils.getOpenContainers()
for _, container in ipairs(openContainers) do
local inventories = container.OwnInventories
@@ -367,48 +325,38 @@ local function stackToCursor()
return
end
local item, slot
local function predicate(ititem)
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
for _, invSlot in ipairs(slots) do
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,
}
-- We gotta do a little juggling...
for _, sslot in ipairs(slots) do
slot = sslot
local items
if not slot.slot.items or #slot.slot.items == 0 then
for _, invSlot in ipairs(slots) do
if not invSlot.item then
MyModGlobal.debugPrint("No items in slot")
goto continue
end
item = slot.slot.items[1]
MyModGlobal.debugPrint(string.format("Stacking all player items to %s", item.Prefab.Identifier.Value))
items = {}
utils.enqueueAllPlayerItems(items, predicate)
utils.enqueueOpenContainers(items, predicate)
MyModGlobal.debugPrint(string.format("Stacking all player items to %s", invSlot.item.Prefab.Identifier.Value))
utils.enqueuePlayerItems(options)
utils.enqueueOpenContainers(options)
::continue::
end
end
@@ -420,45 +368,38 @@ local function stackAllToCursor()
return
end
for _, slot in ipairs(slots) do
local item, predicate
if not slot.slot.items or #slot.slot.items == 0 then
local function predicate(ititem)
for _, invSlot in ipairs(slots) do
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")
goto continue
end
item = slot.slot.items[1]
MyModGlobal.debugPrint(string.format("Stacking all items to %s", item.Prefab.Identifier.Value))
predicate = function(ititem)
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)
MyModGlobal.debugPrint(string.format("Stacking all items to %s", invSlot.item.Prefab.Identifier.Value))
utils.enqueueSubmarineItems(options)
utils.enqueuePlayerItems(options)
::continue::
end
end

View File

@@ -27,7 +27,7 @@ local function tryUnloadSlot(invSlot)
-- Where can we put our toUnload items?
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
for prefab, _ in pairs(toUnloadByPrefab) do

View File

@@ -4,15 +4,24 @@
---@class Barotrauma.Inventory.ItemSlot
---@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 = {}
---@class InventorySlot
---@field slot Barotrauma.Inventory.ItemSlot
---@class InventorySlot ---@field slot Barotrauma.Inventory.ItemSlot
---@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
-- ---@field lastUpdated number
MyModGlobal.InventorySlot = {
---@param inventory Barotrauma.Inventory
@@ -27,6 +36,7 @@ MyModGlobal.InventorySlot = {
self.slotIndex0 = slotIndex1 - 1
self.stackSize = 0
self.maxStackSize = 0
self.depth = 0
-- self:update()
if inventory and inventory.slots and #inventory.slots > 0 then
@@ -42,6 +52,33 @@ MyModGlobal.InventorySlot = {
return self
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 item Barotrauma.Item
pretendMoved = function(self, item)
@@ -49,10 +86,6 @@ MyModGlobal.InventorySlot = {
MyModGlobal.debugPrint("Error pretending moved but it was moved to nil inventory")
return
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
if not self.item then
self.maxStackSize = item.Prefab.GetMaxStackSize(self.inventory)
@@ -158,6 +191,11 @@ MyModGlobal.InventorySlot = {
---@return boolean
canFit = function(self, itemPrefab)
return self:howManyCanFit(itemPrefab) > 0
end,
---@param self InventorySlot
---@return number
maxFits = function(self)
return self.maxStackSize - self.stackSize
end
-- hash = function(self)
-- 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 #slots == 0 then return slots, "No slots found under cursor" end
for _, slot in ipairs(slots) do
if slot.slot.items and #slot.slot.items > 0 then
if slot.item then
return slot
end
end