Files
barotrauma-localmods/QuickStackToBag/Lua/Autorun/init.lua

729 lines
29 KiB
Lua

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<string, ItemLocation[]>
---@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)