if SERVER then return end -- Register necessary types and make fields accessible LuaUserData.RegisterType("Barotrauma.Items.Components.ItemContainer+SlotRestrictions") LuaUserData.RegisterType( 'System.Collections.Immutable.ImmutableArray`1[[Barotrauma.Items.Components.ItemContainer+SlotRestrictions, Barotrauma]]') LuaUserData.MakeFieldAccessible(Descriptors['Barotrauma.Items.Components.ItemContainer'], 'slotRestrictions') LuaUserData.MakeFieldAccessible(Descriptors['Barotrauma.ItemInventory'], 'slots') LuaUserData.MakeFieldAccessible(Descriptors["Barotrauma.CharacterInventory"], "slots") -- Simple configuration local CONFIG = { TRIGGER_KEY = Keys.F, -- Key to press for quick stacking NESTED_CONTAINERS = true, -- Whether to include nested containers DEBUG_MODE = true, -- Print debug messages HAND_SLOTS = { 6, 7 }, -- Slot numbers for hands (typically slots 6 and 7) PRIORITY_CONTAINERS = { "toolbelt", "backpack", "pouch" }, -- Priority order for containers EXCLUDE_TOOLS = true, -- Exclude tools from container list TOOL_IDENTIFIERS = { "welding", "gun", "weapon", "revolver", "smg", "rifle", "shotgun", "diving", "oxygen", "scanner", "card", "id", "fuel", "rod", "battery", "fabricator", "deconstructor" }, -- Common tool identifiers to exclude ALWAYS_INCLUDE = { "toolbelt", "backpack", "pouch" } -- Always include these items even if they match tool criteria } -- MOD INFO local MOD_NAME = "Quick Stack To Containers" local MOD_VERSION = "1.1.0" print(MOD_NAME .. " v" .. MOD_VERSION .. " loaded!") -- Debugging helper function local function debugPrint(message) if CONFIG.DEBUG_MODE then print("[" .. MOD_NAME .. "] " .. message) end end ---@param table table ---@param depth number? function DumpTable(table, depth) if depth == nil then depth = 0 end if (depth > 200) then print("Error: Depth > 200 in dumpTable()") return end for k, v in pairs(table) do if (type(v) == "table") then print(string.rep(" ", depth) .. k .. ":") DumpTable(v, depth + 1) else print(string.rep(" ", depth) .. k .. ": ", v) end end end -- Show notification to player -- Helper function to check if an item is a tool rather than a container local function isTool(item) if not item or not item.Prefab or not item.Prefab.Identifier then return false end -- Never exclude priority containers local identifier = item.Prefab.Identifier.Value:lower() for _, alwaysInclude in ipairs(CONFIG.ALWAYS_INCLUDE) do if identifier:find(alwaysInclude) then debugPrint("Always including high priority container: " .. item.Name) return false end end -- Check tags first - most reliable method local hasTags = false pcall(function() if item.Tags and item.Tags ~= "" then hasTags = true local tags = item.Tags:lower() -- If item has explicit container tag, it's a container if tags:find("container") then debugPrint("Item has container tag: " .. item.Name) return false end -- If item has tool or weapon tag, it's a tool if tags:find("tool") or tags:find("weapon") then debugPrint("Item has tool/weapon tag: " .. item.Name) return true end end end) -- If we found tags and made a decision based on them, return the result if hasTags then -- If we got here, the tags didn't conclusively identify this as a tool -- For items with tags but no tool/weapon tag, check the number of slots if item.OwnInventory and item.OwnInventory.slots and #item.OwnInventory.slots > 1 then -- Items with multiple slots are likely containers return false end end -- Fall back to identifier checks for _, toolId in ipairs(CONFIG.TOOL_IDENTIFIERS) do if identifier:find(toolId) then -- Skip known exceptions if toolId == "tool" and identifier:find("toolbox") or identifier:find("toolbelt") then return false end return true end end -- Final check based on slots - tools typically have 1 slot, containers have multiple if item.OwnInventory and item.OwnInventory.slots then -- Consider items with more than 2 slots as containers if #item.OwnInventory.slots > 2 then return false elseif #item.OwnInventory.slots == 1 then -- Most single-slot inventories are tools, but check for exceptions -- Some special container items might have 1 slot but still be containers if identifier:find("container") or identifier:find("box") or identifier:find("crate") then return false end -- Otherwise likely a tool return true end end -- When in doubt, don't classify as a tool return false end -- Helper function to move an item to a container, but only if matching items exist local function moveItemToContainer(item, containerInv) if not item or not containerInv then return false end debugPrint("Attempting to stack with existing items in container (" .. #containerInv.slots .. " slots)") -- Get max stack size for this item (default to 1 if we can't determine) local maxStackSize = 60 pcall(function() print(item.Prefab.MaxStackSize) if item.Prefab and item.Prefab.MaxStackSize then maxStackSize = item.Prefab.MaxStackSize end end) -- Get current condition/quality of item (for proper stacking) local itemCondition = 100 pcall(function() if item.Condition then itemCondition = item.Condition end end) -- Check if the container has any matching items first local foundMatchingItem = false local matchingSlots = {} -- First pass: find all matching slots for slotIndex = 0, #containerInv.slots - 1 do for _, containerItem in ipairs(containerInv.slots[slotIndex + 1].items) do if containerItem.Prefab.Identifier.Equals(item.Prefab.Identifier) then -- Check similar condition/quality (within 10%) local containerItemCondition = 100 pcall(function() if containerItem.Condition then containerItemCondition = containerItem.Condition end end) -- Only consider similar quality items if math.abs(containerItemCondition - itemCondition) <= 10 then foundMatchingItem = true table.insert(matchingSlots, { slotIndex = slotIndex, currentSize = #containerInv.slots[slotIndex + 1].items, remainingSpace = maxStackSize - #containerInv.slots[slotIndex + 1].items }) end end end end -- If no matching items exist in the container, don't move the item if not foundMatchingItem then debugPrint("No matching items in container, skipping " .. item.Name) return false end -- Sort slots by most available space first table.sort(matchingSlots, function(a, b) return a.remainingSpace > b.remainingSpace end) -- Try to stack with existing items that have space for _, matchingSlot in ipairs(matchingSlots) do -- Only try to put in slots that have space if matchingSlot.remainingSpace > 0 then debugPrint("Trying to stack " .. item.Name .. " with existing items (slot has " .. matchingSlot.remainingSpace .. " space)") -- Try to move the full item if containerInv.TryPutItem(item, matchingSlot.slotIndex, true, false, nil) then debugPrint("Successfully stacked " .. item.Name) return true else debugPrint("Failed full stack, likely due to size limits") -- If full stack failed, try to split and move part of it (if possible) pcall(function() -- Check if we can split stacks and if the item is a stack if item.SplitStack and maxStackSize > 1 then local currentStackSize = 1 -- Try to determine current stack size pcall(function() if item.Count then currentStackSize = item.Count end end) -- If current stack is larger than 1, try to split it if currentStackSize > 1 then -- Calculate how much we can move local amountToMove = math.min(matchingSlot.remainingSpace, currentStackSize - 1) if amountToMove > 0 then debugPrint("Attempting to split stack and move " .. amountToMove .. " items") -- Try to split the stack local splitItem = item:SplitStack(amountToMove) if splitItem then -- Try to put the split item into the slot if containerInv.TryPutItem(splitItem, matchingSlot.slotIndex, true, false, nil) then debugPrint("Successfully moved partial stack of " .. splitItem.Name) return true else debugPrint("Failed to move split stack") -- If it fails, try to recombine the original stack pcall(function() item:AddItem(splitItem) end) end else debugPrint("Failed to split stack") end end end end end) end end end -- If we couldn't stack with existing partial stacks, look for empty slots -- But only if we're still stacking by finding matching items debugPrint("Failed to stack with existing items, checking for empty slots") -- Find empty slots local emptySlots = {} for slotIndex = 0, #containerInv.slots - 1 do if #containerInv.slots[slotIndex + 1].items == 0 then table.insert(emptySlots, slotIndex) end end -- If there are empty slots, try to put the item there for _, slotIndex in ipairs(emptySlots) do debugPrint("Trying to put " .. item.Name .. " in empty slot " .. slotIndex) if containerInv.TryPutItem(item, slotIndex, false, false, nil) then debugPrint("Successfully put " .. item.Name .. " in empty slot") return true end end debugPrint("Failed to stack with existing items or find empty slots") return false end -- Sort containers by priority local function sortContainersByPriority(containers) table.sort(containers, function(a, b) local aPriority = -1 local bPriority = -1 -- Check priority for first container for i, identifier in ipairs(CONFIG.PRIORITY_CONTAINERS) do if a.Prefab.Identifier.Value:find(identifier) then aPriority = i break end end -- Check priority for second container for i, identifier in ipairs(CONFIG.PRIORITY_CONTAINERS) do if b.Prefab.Identifier.Value:find(identifier) then bPriority = i break end end return aPriority < bPriority end) return containers end -- Recursively find all containers in the player's inventory, including nested ones local function findAllContainers(inventory, containers) if not inventory or not inventory.slots then return containers end containers = containers or {} for _, slot in ipairs(inventory.slots) do for _, item in ipairs(slot.items) do if item.OwnInventory then -- Skip tools if configured to do so if CONFIG.EXCLUDE_TOOLS and isTool(item) then debugPrint("Skipping tool: " .. item.Name .. " (" .. item.Prefab.Identifier.Value .. ")") else -- Add this container debugPrint("Found container: " .. item.Name .. " (" .. item.Prefab.Identifier.Value .. ")") table.insert(containers, item) -- Recursively search inside this container if enabled if CONFIG.NESTED_CONTAINERS then debugPrint("Searching inside " .. item.Name .. " for nested containers") findAllContainers(item.OwnInventory, containers) end end end end end return containers end ---@class ItemLocation ---@field inventory Barotrauma.ItemInventory ---@field slotIndex 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 ---@return table local function buildItemTree(inventory, itemTree, iter) iter = iter or 0 itemTree = itemTree or {} if not inventory or not inventory.slots then debugPrint("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 -- This iteration is done only to skip the player inventory as a potential destination -- Since it will always be 0th iteration and there's no point stacking -- Items from the player inventory to itself debugPrint("Building item tree for inventory at iteration: " .. iter .. ", slot index: " .. slotIndex) debugPrint("Slot " .. slotIndex .. " has " .. #slot.items .. " items") if iter > 0 then if #slot.items == 0 then debugPrint("Slot " .. slotIndex .. " is empty, adding to itemTree as 'empty'") itemTree['empty'] = itemTree['empty'] or {} itemTree['empty'][#itemTree['empty'] + 1] = { inventory = inventory, slotIndex = slotIndex, maxFits = 60 } debugPrint("Added empty slot to itemTree at index: " .. slotIndex) else ---@type Barotrauma.Item local item = slot.items[1] local identifier = item.Prefab.Identifier.Value debugPrint("Found item: " .. item.Name .. " with identifier: " .. identifier .. ", max stack size: " .. maxStackSize) 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, maxFits = slot.HowManyCanBePut(item.Prefab) } debugPrint("Added item to itemTree under identifier: " .. identifier .. ", available fits: " .. (maxStackSize - #slot.items)) end end if #slot.items > 0 then ---@type Barotrauma.Item local item = slot.items[1] 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 debugPrint("Searching inside " .. item.Name .. " for nested containers") buildItemTree(item.OwnInventory, itemTree, iter + 1) end end end debugPrint("Completed building item tree for current inventory iteration: " .. iter) return itemTree end -- Find the currently open container - improved version local function getOpenContainer() debugPrint("Attempting to find open container...") local openContainer = nil local hasVisibleInventory = false -- Method 1: Try to use Inventory.CurrentInventory or similar properties pcall(function() if Inventory and Inventory.CurrentContainer and Inventory.CurrentContainer.Owner then openContainer = Inventory.CurrentContainer.Owner hasVisibleInventory = true debugPrint("Found open container via CurrentContainer: " .. openContainer.Name) end end) if hasVisibleInventory and openContainer then return openContainer end -- Method 2: Check if Inventory.OpenInventories exists and has entries pcall(function() if Inventory and Inventory.OpenInventories and #Inventory.OpenInventories > 0 then -- Get the last opened inventory for _, inv in ipairs(Inventory.OpenInventories) do if inv and inv.Owner and inv.Owner ~= Character.Controlled then openContainer = inv.Owner hasVisibleInventory = true debugPrint("Found open container via OpenInventories: " .. openContainer.Name) break end end end end) if hasVisibleInventory and openContainer then return openContainer end -- Method 3: Check if currently selected item is interacting with a container pcall(function() if Character.Controlled and Character.Controlled.SelectedItem then local selectedItem = Character.Controlled.SelectedItem -- Check if selected item is interacting with something if selectedItem.InteractingWith and selectedItem.InteractingWith.OwnInventory and selectedItem.InteractingWith ~= Character.Controlled then openContainer = selectedItem.InteractingWith hasVisibleInventory = true debugPrint("Found open container via current interaction: " .. openContainer.Name) end end end) if hasVisibleInventory and openContainer then return openContainer end -- For safety, add a visual check - only return a container if it has a visible UI element if openContainer then -- Verify this container actually has a visible UI local isVisible = false pcall(function() -- Common method to check if inventory is visible - look for visual components if openContainer.OwnInventory and openContainer.OwnInventory.visualSlots and #openContainer.OwnInventory.visualSlots > 0 then isVisible = true end end) if not isVisible then debugPrint("Container found but not visibly open in UI: " .. openContainer.Name) return nil end end debugPrint("No open container found") return nil end -- Function to stack from open container to player containers local function stackFromOpenContainer(character, playerContainers, openContainer) if not character or not openContainer or not openContainer.OwnInventory then return 0 end debugPrint("Stacking from open container: " .. openContainer.Name) local itemsMoved = 0 local openContainerInv = openContainer.OwnInventory -- Create a cache of processable items to avoid redundant checks local itemsToProcess = {} local processedIdentifiers = {} -- Process each slot in the open container for slotIndex = 0, #openContainerInv.slots - 1 do local slot = openContainerInv.slots[slotIndex + 1] -- Process items in the slot for i = #slot.items, 1, -1 do local item = slot.items[i] local identifierValue = item.Prefab.Identifier.Value -- Skip container items if item.OwnInventory then debugPrint("Skipping container item: " .. item.Name) goto nextItem end -- Skip if we've already processed an item of this type if processedIdentifiers[identifierValue] then debugPrint("Already processed items of type: " .. identifierValue .. " from open container") goto nextItem end table.insert(itemsToProcess, { item = item, slotIndex = slotIndex }) -- Mark identifier as scheduled for processing processedIdentifiers[identifierValue] = true ::nextItem:: end end -- Now process the collected items - this greatly reduces redundant container checks for _, itemData in ipairs(itemsToProcess) do local item = itemData.item debugPrint("Processing item from container: " .. item.Name) -- Try to move the item to each player container for _, container in ipairs(playerContainers) do debugPrint("Trying to stack " .. item.Name .. " into " .. container.Name) if moveItemToContainer(item, container.OwnInventory) then debugPrint("Stacked " .. item.Name .. " from open container into " .. container.Name) itemsMoved = itemsMoved + 1 break end end end return itemsMoved end -- Function to quickly stack items from inventory to containers local function quickStackItems(character) if not character then debugPrint("No character found") return end debugPrint("Quick stack function called") local inventory = character.Inventory if not inventory or not inventory.slots then debugPrint("Character has no inventory") return end local itemTree = buildItemTree(inventory, {}) DumpTable(itemTree) -- Find all containers in player inventory, including nested ones -- local containers = findAllContainers(inventory, {}) -- -- Fallback: If no containers found, try a direct search for known container types -- if #containers == 0 then -- debugPrint("No containers found with standard method, trying fallback method...") -- -- Direct slot search for common container types -- for _, slot in ipairs(inventory.slots) do -- for _, item in ipairs(slot.items) do -- if item.OwnInventory then -- local identifier = item.Prefab.Identifier.Value:lower() -- -- Force include specific items that we know should be containers -- if identifier:find("toolbelt") or identifier:find("backpack") or -- identifier:find("pouch") or identifier:find("container") or -- identifier:find("box") or identifier:find("crate") or -- item.Name:find("Container") or item.Name:find("Box") then -- debugPrint("Fallback: Force including known container: " .. item.Name) -- table.insert(containers, item) -- -- Also check container slots -- if CONFIG.NESTED_CONTAINERS and #item.OwnInventory.slots > 2 then -- for _, containerSlot in ipairs(item.OwnInventory.slots) do -- for _, containerItem in ipairs(containerSlot.items) do -- if containerItem.OwnInventory and -- (containerItem.Prefab.Identifier.Value:lower():find("container") or -- containerItem.Name:find("Container")) then -- debugPrint("Fallback: Found nested container: " .. containerItem.Name) -- table.insert(containers, containerItem) -- end -- end -- end -- end -- end -- end -- end -- end -- end -- debugPrint("Found " .. #containers .. " containers" .. (CONFIG.NESTED_CONTAINERS and " (including nested)" or "")) -- if #containers == 0 then -- debugPrint("No containers with inventory found!") -- showNotification("No containers found! Make sure you have a backpack, toolbelt, or storage box.") -- return -- end -- -- Sort containers by priority -- containers = sortContainersByPriority(containers) -- for i, container in ipairs(containers) do -- debugPrint(i .. ": " .. container.Name .. " - priority container: " .. -- tostring(container.Prefab.Identifier.Value:find(CONFIG.PRIORITY_CONTAINERS[1]) ~= nil)) -- end -- local itemsMoved = 0 -- -- Create a cache of processable items to avoid redundant checks -- local itemsToProcess = {} -- local processedIdentifiers = {} -- -- First collect all items we might want to stack -- for slotIndex, slot in ipairs(inventory.slots) do -- -- Skip hand slots -- local isHandSlot = false -- for _, handSlot in ipairs(CONFIG.HAND_SLOTS) do -- if slotIndex == handSlot then -- isHandSlot = true -- break -- end -- end -- if isHandSlot then -- debugPrint("Skipping hand slot: " .. slotIndex) -- goto continueSlot -- end -- -- Process items in the slot -- for i = #slot.items, 1, -1 do -- local item = slot.items[i] -- local identifierValue = item.Prefab.Identifier.Value -- -- Skip container items -- if item.OwnInventory then -- debugPrint("Skipping container item: " .. item.Name) -- goto nextItem -- end -- -- Skip if we've already processed an item of this type -- if processedIdentifiers[identifierValue] then -- debugPrint("Already processed items of type: " .. identifierValue) -- goto nextItem -- end -- debugPrint("Adding to process list: " .. item.Name) -- table.insert(itemsToProcess, { -- item = item, -- slotIndex = slotIndex -- }) -- -- Mark identifier as scheduled for processing -- processedIdentifiers[identifierValue] = true -- ::nextItem:: -- end -- ::continueSlot:: -- end -- -- Now process the collected items - this greatly reduces redundant container checks -- for _, itemData in ipairs(itemsToProcess) do -- local item = itemData.item -- debugPrint("Processing inventory item: " .. item.Name .. " (" .. item.Prefab.Identifier.Value .. ")") -- -- Try to move the item to each container -- for _, container in ipairs(containers) do -- debugPrint("Trying container: " .. container.Name) -- if moveItemToContainer(item, container.OwnInventory) then -- debugPrint("Stacked " .. item.Name .. " into " .. container.Name) -- itemsMoved = itemsMoved + 1 -- break -- end -- end -- end -- -- Check if there's an open container to stack from -- local openContainer = getOpenContainer() -- if openContainer then -- debugPrint("Found open container to stack from: " .. openContainer.Name) -- local openContainerItemsMoved = stackFromOpenContainer(character, containers, openContainer) -- itemsMoved = itemsMoved + openContainerItemsMoved -- else -- debugPrint("No open container found to stack from") -- end -- if itemsMoved > 0 then -- debugPrint("Stacked " .. itemsMoved .. " items into containers") -- else -- debugPrint("No matching items to stack") -- end end -- Hook into player control to listen for key press Hook.Patch("Barotrauma.Character", "ControlLocalPlayer", function(instance, ptable) if not PlayerInput.KeyHit(CONFIG.TRIGGER_KEY) then return end local character = instance if not character then return end quickStackItems(character) end, Hook.HookMethodType.After)