407 lines
14 KiB
Lua
407 lines
14 KiB
Lua
-- luacheck: globals MyModGlobal Character
|
|
-- luacheck: max line length 420
|
|
local utils = require("Cyka.utils")
|
|
|
|
---@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[]>
|
|
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, 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
|
|
-- 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
|
|
}
|
|
-- 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))
|
|
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
|
|
}
|
|
-- MyModGlobal.debugPrint(string.format("Added item to itemTree under identifier: %s", identifier))
|
|
|
|
local tags = 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(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, ItemLocation[]>
|
|
---@return table<string, ItemLocation[]>
|
|
local function sortItemTree(itemTree)
|
|
for _, item in pairs(itemTree) do
|
|
table.sort(item, function(a, b)
|
|
---@cast a ItemLocation
|
|
---@cast b ItemLocation
|
|
if a.depth ~= b.depth then
|
|
return a.depth < b.depth
|
|
elseif a.maxFits ~= b.maxFits then
|
|
return a.maxFits > b.maxFits
|
|
else
|
|
return a.slotIndex < b.slotIndex
|
|
end
|
|
end)
|
|
end
|
|
|
|
return itemTree
|
|
end
|
|
|
|
---@param item Barotrauma.Item
|
|
---@param itemTree table<string, ItemLocation[]>
|
|
---@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 nil, "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 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)
|
|
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
|
|
|
|
-- 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...")
|
|
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)
|
|
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
|
|
|
|
-- 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, ItemLocation[]>
|
|
---@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, ItemLocation[]>, 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
|
|
|
|
-- Function to quickly stack items from inventory to containers
|
|
-- 6 and 7 are hands
|
|
-- 9..18 are main slots
|
|
local inventorySlotsToStack = { 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 }
|
|
local function quickStackItems(character)
|
|
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
|
|
|
|
-- for i, slot in ipairs(inventory.slots) do
|
|
-- if #slot.items > 0 then
|
|
-- local item = slot.items[1]
|
|
-- local identifier = item.Prefab.Identifier.Value
|
|
-- print(string.format("Item at slot %d is %s", i, identifier))
|
|
-- end
|
|
-- end
|
|
|
|
MyModGlobal.debugPrint("Quick stack function called")
|
|
|
|
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 _, 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
|
|
|
|
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
|
|
|
|
for _, slot in ipairs(slots) do
|
|
local item
|
|
if not slot.slot.items or #slot.slot.items == 0 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))
|
|
utils.enqueueAllPlayerItems({}, 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)
|
|
|
|
::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
|
|
|
|
for _, slot in ipairs(slots) do
|
|
local item, predicate
|
|
if not slot.slot.items or #slot.slot.items == 0 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)
|
|
|
|
::continue::
|
|
end
|
|
end
|
|
|
|
return {
|
|
buildItemTree = buildItemTree,
|
|
tryBuildCharacterItemTree = tryBuildCharacterItemTree,
|
|
sortItemTree = sortItemTree,
|
|
tryMoveItem = tryMoveItem,
|
|
tryMoveItems = tryMoveItems,
|
|
quickStackItems = quickStackItems,
|
|
stackToCursor = stackToCursor,
|
|
stackAllToCursor = stackAllToCursor,
|
|
}
|