Files
barotrauma-localmods/CykaQuick/Lua/Cyka/quickstack.lua
2025-04-01 20:40:19 +02:00

417 lines
13 KiB
Lua

-- luacheck: globals MyModGlobal Character CLIENT
-- luacheck: max line length 420
if not CLIENT then return end
local utils = require("Cyka.utils")
local dump = require("Cyka.dump")
-- 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.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
if not inventory or not inventory.slots then
-- MyModGlobal.debugPrint(string.format("Inventory is nil or has no slots, returning empty itemTree"))
return itemTree
end
-- One slot can have one item but multiple of it
-- The number of an item in a slot is #slot.items
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] = invSlot
-- MyModGlobal.debugPrint(string.format("Added empty slot to itemTree at index: %d", slotIndex))
else
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] = invSlot
-- MyModGlobal.debugPrint(string.format("Added item to itemTree under identifier: %s", identifier))
local tags = invSlot.item.Prefab.Tags
local shouldSuss = false
for tag in tags do
if tag.value:find("container") then
shouldSuss = true
break
end
end
if shouldSuss then
-- MyModGlobal.debugPrint(string.format("Searching inside %s for nested containers", item.Name))
buildItemTree(invSlot.item.OwnInventory, itemTree, depth + 1)
end
end
end
-- MyModGlobal.debugPrint("Completed building item tree")
return itemTree
end
-- We would like to fill larger stacks first
---@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 InventorySlot
---@cast b InventorySlot
local maxfitsA, maxfitsB = a:maxFits(), b:maxFits()
if a.depth ~= b.depth then
return a.depth < b.depth
elseif maxfitsA ~= maxfitsB then
return maxfitsA > maxfitsB
else
return a.slotIndex0 < b.slotIndex0
end
end)
end
return itemTree
end
---@param item Barotrauma.Item
---@param itemTree table<string, InventorySlot[]>
---@param force boolean
---@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 "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)
local moved = false
if location then
-- First try to move to existing stacks
for _, itemLocation in ipairs(location) do
-- We cannot stack items with decreased condition
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
end
end
-- If we can not find an existing stack
-- Then move to any of the empty slots
if not moved then
-- MyModGlobal.debugPrint("No existing stacks found, trying empty slots...")
if not itemTree['empty'] then
return "No empty slots found"
end
for _, itemLocation in ipairs(itemTree['empty']) do
-- 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
local canFit = itemLocation:canFit(item.Prefab)
if canFit then
utils.enqueueMove(item, itemLocation)
moved = true
break
end
end
end
-- If we still can not move the item give up
if not moved then
-- MyModGlobal.debugPrint("Failed to find valid location for item")
return "Failed to find valid location for item"
end
-- MyModGlobal.debugPrint("Item moved successfully")
return nil
end
---@param items Barotrauma.Item[]
---@param itemTree table<string, InventorySlot[]>
---@param force? boolean
---@return string[]
local function tryMoveItems(items, itemTree, force)
force = force or false
local errs = {}
for _, item in ipairs(items) do
local err = tryMoveItem(item, itemTree, force)
-- oops, this one failed, continue...
if err then
errs[#errs + 1] = string.format("Failed to move item: %s", item.Prefab.Identifier.Value)
end
end
return errs
end
---@param character Barotrauma.Character
---@return table<string, InventorySlot[]>, string?
local function tryBuildCharacterItemTree(character)
local itemTree = {}
-- MyModGlobal.debugPrint(string.format("Preparing to stack items into the bag..."))
local inventory = character.Inventory
if not inventory or not inventory.slots then
return itemTree, "Character has no inventory"
end
local bagSlot = inventory.slots[MyModGlobal.BAG_SLOT]
if bagSlot then
-- MyModGlobal.debugPrint(string.format("Bag slot found at index 8 with %d items.", #bagSlot.items))
if #bagSlot.items > 0 then
local item = bagSlot.items[1]
-- MyModGlobal.debugPrint(string.format("Found item in bag slot: %s", item.Name))
if item and item.OwnInventory then
-- MyModGlobal.debugPrint(string.format("Item has its own inventory, building item tree for it..."))
itemTree = buildItemTree(item.OwnInventory, itemTree)
else
return itemTree, "Bag does not have its own inventory"
end
else
return itemTree, "Bag slot is empty"
end
else
return itemTree, "No bag slot found at index " .. tostring(MyModGlobal.BAG_SLOT)
end
return itemTree, nil
end
---@param item Barotrauma.Item
---@return string?
local function stackToContainer(item)
MyModGlobal.debugPrint(string.format("Attempting to stack items to container: %s", tostring(item)))
local itemInventory = item.OwnInventory
if not itemInventory then
return "Item has no own inventory"
end
local parentInventory = item.ParentInventory
if not parentInventory then
return "Item has no parent inventory"
end
local itemTree = buildItemTree(itemInventory)
itemTree = sortItemTree(itemTree)
local toMove = {}
for slot in parentInventory.slots do
for slotItem in slot.items do
if slotItem.Prefab.Identifier.Value ~= item.Prefab.Identifier.Value then
toMove[#toMove + 1] = slotItem
end
end
end
MyModGlobal.debugPrint(string.format("Enqueued %d items to stack", #toMove))
-- dump(toMove)
local errors = tryMoveItems(toMove, itemTree)
for _, error in ipairs(errors) do
MyModGlobal.debugPrint(string.format("Error stacking item: %s", error))
end
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, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 }
---@param character Barotrauma.Character
local function quickStackItems(character)
MyModGlobal.debugPrint("Quick stack function called")
-- If we are mousing over an item that has an inventory (ie. is a container)
-- Then stack all items from the parent inventory into the mouseover container
local mouseover = utils.getFirstSlotUnderCursor()
if mouseover then
local itemInventory = mouseover.item.OwnInventory
if itemInventory then
MyModGlobal.debugPrint(string.format("Item inventory found: %s", tostring(itemInventory)))
local err = stackToContainer(mouseover.item)
if err then
MyModGlobal.debugPrint(string.format("Error stacking items to container: %s", err))
end
return
end
end
if not character then
MyModGlobal.debugPrint("No character found")
return
end
local inventory = character.Inventory
if not inventory or not inventory.slots then
MyModGlobal.debugPrint("Character has no inventory")
return
end
local itemTree, err = tryBuildCharacterItemTree(character)
if err then
MyModGlobal.debugPrint(string.format("Error building item tree: %s", err))
return
end
itemTree = sortItemTree(itemTree)
--DumpTable(itemTree)
local toMove = {}
for item in character.HeldItems do
MyModGlobal.debugPrint(string.format("Item: %s", item.Prefab.Identifier.Value))
if item.OwnInventory then
toMove = utils.enqueueInventory(item.OwnInventory, toMove)
end
end
-- for _, slotid in ipairs(inventorySlotsToStack) do
-- MyModGlobal.debugPrint(string.format("Processing inventory slot: %d", slotid))
-- local slot = inventory.slots[slotid]
-- if #slot.items > 0 then
-- -- local item = slot.items[1]
-- -- local tags = item.Prefab.Tags
-- local shouldSuss = true
-- -- for tag in tags do
-- -- if tag.value:find("tool") or tag.value:find("weapon") then
-- -- MyModGlobal.debugPrint(string.format("Item '%s' is a tool or weapon, skipping", item.Name))
-- -- shouldSuss = false
-- -- break
-- -- end
-- -- end
-- if shouldSuss then
-- local before = #toMove
-- toMove = utils.enqueueSlot(slot, toMove)
-- local after = #toMove
-- MyModGlobal.debugPrint(string.format("Enqueued %d items from the inventory slot %d", after - before,
-- slotid))
-- end
-- end
-- end
-- TODO: enqueueOpenContainers?
local openContainers = utils.getOpenContainers()
for _, container in ipairs(openContainers) do
local inventories = container.OwnInventories
MyModGlobal.debugPrint(string.format("Found %d inventories in the open container", #inventories))
for containerInventory in inventories do
MyModGlobal.debugPrint(string.format("Enqueuing inventory with %d slots", #containerInventory.slots))
local before = #toMove
toMove = utils.enqueueInventory(containerInventory, toMove)
local after = #toMove
MyModGlobal.debugPrint(string.format("Enqueued %d items from the open container", after - before))
end
end
local errors = tryMoveItems(toMove, itemTree)
for _, error in ipairs(errors) do
MyModGlobal.debugPrint(string.format("Error stacking item: %s", error))
end
end
local function stackToCursor()
local slots, err = utils.getSlotsUnderCursor()
if err then
MyModGlobal.debugPrint(string.format("Error getting slots under cursor: %s", err))
return
end
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,
}
-- We gotta do a little juggling...
for _, invSlot in ipairs(slots) do
if not invSlot.item then
MyModGlobal.debugPrint("No items in slot")
goto continue
end
MyModGlobal.debugPrint(string.format("Stacking all player items to %s", invSlot.item.Prefab.Identifier.Value))
utils.enqueuePlayerItems(options)
utils.enqueueOpenContainers(options)
::continue::
end
end
local function stackAllToCursor()
local slots, err = utils.getSlotsUnderCursor()
if err then
MyModGlobal.debugPrint(string.format("Error getting slots under cursor: %s", err))
return
end
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
MyModGlobal.debugPrint(string.format("Stacking all items to %s", invSlot.item.Prefab.Identifier.Value))
utils.enqueueSubmarineItems(options)
utils.enqueuePlayerItems(options)
::continue::
end
end
return {
buildItemTree = buildItemTree,
tryBuildCharacterItemTree = tryBuildCharacterItemTree,
sortItemTree = sortItemTree,
tryMoveItem = tryMoveItem,
tryMoveItems = tryMoveItems,
quickStackItems = quickStackItems,
stackToCursor = stackToCursor,
stackAllToCursor = stackAllToCursor,
}