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 ---@param depth number ---@return table 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") MyModGlobal.debugPrint("Completed building item tree") return itemTree end -- We would like to fill larger stacks first ---@param itemTree table ---@return table 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 ---@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, nil) 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, nil) 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 ---@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 -- This is a bit fucking sucky..... -- But I really don't know better -- Maybe it will be fine... ---@return Barotrauma.Item[] local function getOpenContainers() MyModGlobal.debugPrint("Attempting to find open container...") -- local containers = {} -- for item in Item.ItemList do -- ---@cast item Barotrauma.Item -- local isok = true -- isok = isok and item ~= nil -- isok = isok and item.OwnInventory ~= nil -- isok = isok and item.OwnInventory.visualSlots ~= nil -- isok = isok and #item.OwnInventory.visualSlots > 0 -- -- I don't know what rootContainer is -- -- It seems to be the parent of the current item...? -- -- Maybe the world object... -- -- Either way - static objects that we may open have it -- -- And our own inventory does not -- -- So it's a good selector for now -- isok = isok and item.rootContainer ~= nil -- if isok then -- containers[#containers + 1] = item -- end -- end local controlledCharacter = Character.Controlled if not controlledCharacter then return {} end local selectedItem = controlledCharacter.SelectedItem if not selectedItem then return {} end return { selectedItem } end ---@param character Barotrauma.Character ---@return table, 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 = 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 return { buildItemTree = buildItemTree, tryBuildCharacterItemTree = tryBuildCharacterItemTree, sortItemTree = sortItemTree, tryMoveItem = tryMoveItem, tryMoveItems = tryMoveItems, quickStackItems = quickStackItems }