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 sortItemtreeBySlots(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 ---@return string local function tryMoveItem(item, itemTree) local location = itemTree[item.Prefab.Identifier.Value] if not location then return nil, "No locations for item, not stacking" end local moved = false -- First try to move to existing stacks for _, itemLocation in ipairs(location) do if itemLocation.maxFits > 0 then moved = moved or itemLocation.inventory.TryPutItem(item, itemLocation.slotIndex, false, true, nil) itemLocation.maxFits = itemLocation.inventory.HowManyCanBePut(item.Prefab, itemLocation.slotIndex) end end -- If we can not find an existing stack -- Then move to any of the empty slots if not moved then for _, itemLocation in ipairs(itemTree['empty']) do moved = moved or itemLocation.inventory.TryPutItem(item, itemLocation.slotIndex, false, true, nil) itemLocation.maxFits = itemLocation.inventory.HowManyCanBePut(item.Prefab, itemLocation.slotIndex) end end -- If we still can not move the item give up if not moved then return "Failed to find valid location for item" end return nil end ---@param items Barotrauma.Item[] ---@param itemTree table ---@return string[] local function tryMoveItems(items, itemTree) local errs = {} for _, item in ipairs(items) do local err = tryMoveItem(item, itemTree) -- 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 inventory Barotrauma.ItemInventory ---@return table, string local function tryBuildItemTree(inventory) local itemTree = {} -- MyModGlobal.debugPrint(string.format("Preparing to stack items into the bag...")) local bagSlot = inventory.slots[8] 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 8." 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 MyModGlobal.debugPrint("Quick stack function called") local inventory = character.Inventory if not inventory or not inventory.slots then MyModGlobal.debugPrint("Character has no inventory") return end local itemTree, err = tryBuildItemTree(inventory) if err then MyModGlobal.debugPrint(string.format("Error building item tree: %s", err)) return end itemTree = sortItemtreeBySlots(itemTree) --DumpTable(itemTree) local toMove = {} -- 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 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 print(string.format("Error stacking item: %s", error)) end end -- Hook into player control to listen for key press Hook.Patch("Barotrauma.Character", "ControlLocalPlayer", function(instance, ptable) if not PlayerInput.KeyHit(MyModGlobal.CONFIG.QUICKSTACK_KEYS) then return end local character = instance if not character then return end quickStackItems(character) end, Hook.HookMethodType.After)