diff --git a/QuickStackToBag/Lua/Autorun/init.lua b/QuickStackToBag/Lua/Autorun/init.lua index deb3b65..74af1f9 100644 --- a/QuickStackToBag/Lua/Autorun/init.lua +++ b/QuickStackToBag/Lua/Autorun/init.lua @@ -1,6 +1,56 @@ if SERVER then return end -- Docs: https://evilfactory.github.io/LuaCsForBarotrauma/lua-docs/manual/common-questions/ +---@class MyModGlobal +---@field CONFIG {QUICKSTACK_KEYS: Keys, FABRICATOR_KEY: Keys, MAX_BUY: Keys, NESTED_CONTAINERS: boolean, DEBUG_MODE: boolean} +---@field MOD_NAME string +---@field MOD_VERSION string +---@field DumpTable fun(table: table, depth?: number) +---@field debugPrint fun(message: string) +MyModGlobal = { + CONFIG = { + QUICKSTACK_KEYS = Keys.F, + FABRICATOR_KEY = Keys.V, + MAX_BUY = Keys.B, + NESTED_CONTAINERS = true, + DEBUG_MODE = true, + }, + MOD_NAME = "Quick Stack To Containers", + MOD_VERSION = "1.1.0", + BAG_SLOT = 8, +} + +---@param table table +---@param depth number? +MyModGlobal.DumpTable = function(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 .. ":") + MyModGlobal.DumpTable(v, depth + 1) + else + print(string.rep(" ", depth) .. k .. ": ", v) + end + end +end + +-- Debugging helper function +MyModGlobal.debugPrint = function(message) + if MyModGlobal.CONFIG.DEBUG_MODE then + print("[" .. MyModGlobal.MOD_NAME .. "] " .. message) + end +end + +require("Cyka.quickstack") + +print(MyModGlobal.MOD_NAME .. " v" .. MyModGlobal.MOD_VERSION .. " loaded!") + -- Register necessary types and make fields accessible LuaUserData.RegisterType("Barotrauma.Items.Components.ItemContainer+SlotRestrictions") LuaUserData.RegisterType( @@ -15,342 +65,15 @@ LuaUserData.RegisterType("Barotrauma.ItemPrefab") LuaUserData.RegisterType("Barotrauma.Location+StoreInfo") LuaUserData.MakeMethodAccessible(Descriptors["Barotrauma.CargoManager"], "GetConfirmedSoldEntities") --- TODO: Implement quick stacking from existing contasiners to existing containers --- So we can have 1 or 2 full containers instead of 14 almost empty ones --- Simple configuration -local CONFIG = { - QUICKSTACK_KEYS = Keys.F, - FABRICATOR_KEY = Keys.V, - MAX_BUY = Keys.B, - NESTED_CONTAINERS = true, - DEBUG_MODE = true, -} - --- MOD INFO -local MOD_NAME = "Quick Stack To Containers" -local MOD_VERSION = "1.1.0" - -print(MOD_NAME .. " v" .. MOD_VERSION .. " loaded!") - ----@param table table ----@param depth number? -local 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 - --- Debugging helper function -local function debugPrint(message) - if CONFIG.DEBUG_MODE then - print("[" .. MOD_NAME .. "] " .. message) - end -end - ----@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 - -- 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 - -- debugPrint(string.format("Building item tree for inventory at slot index: %d", slotIndex)) - -- debugPrint(string.format("Slot %d has %d items", slotIndex, #slot.items)) - if #slot.items == 0 then - -- 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 - } - -- 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 - -- 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 - } - -- 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 - -- debugPrint(string.format("Searching inside %s for nested containers", item.Name)) - buildItemTree(item.OwnInventory, itemTree, depth + 1) - end - end - end - - -- debugPrint("Completed building item tree") - 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 - --- We got to do this shit because enqueueInventory calls enqueueItem --- And enqueueItem calls enqueueInventory --- So unless we define them both before using them --- We will get an error saying either is undefined -local enqueueItem -local enqueueSlot -local enqueueInventory -local _ - ----@param item Barotrauma.Item ----@param queue Barotrauma.Item[] ----@param predicate? fun(item: Barotrauma.Item): boolean ----@return Barotrauma.Item[], string? -enqueueItem = function(item, queue, predicate) - queue = queue or {} - predicate = predicate or function() return true end - -- debugPrint(string.format("Enqueuing item: %s", item.Prefab.Identifier.Value)) - -- local err - -- This should make it breadth first, right...? - -- No, not yet... - local ok, stop = predicate(item) - if ok then - queue[#queue + 1] = item - end - if stop then return queue, "Stop" end - if item.OwnInventory then - -- As far as I know every item has only one inventory - -- Only machines have multiple - -- So inventrorY should be fine here - -- debugPrint("Item has its own inventory, enqueuing inventory...") - queue, _ = enqueueInventory(item.OwnInventory, queue, predicate) - -- if err then - -- debugPrint(string.format("Error enqueuing inventory: %s", err)) - -- end - end - -- debugPrint(string.format("Item enqueued. Current queue size: %d", #queue)) - return queue, nil -end - ----@param slot Barotrauma.ItemInventory.Slot ----@param queue Barotrauma.Item[] ----@param predicate? fun(item: Barotrauma.Item): boolean ----@return Barotrauma.Item[], string? -enqueueSlot = function(slot, queue, predicate) - queue = queue or {} - predicate = predicate or function() return true end - -- debugPrint(string.format("Enqueuing slot with %d items.", #slot.items)) - -- We don't want to shadow queue - local err - -- If the slot is empty there's nothing to iterate - -- And we will naturally return queue as is - for _, item in ipairs(slot.items) do - -- Only the final leaf nodes decide upon the predicate - queue, err = enqueueItem(item, queue, predicate) - if err then - return queue, err - end - end - -- debugPrint(string.format("Finished enqueuing slot. Current queue size: %d", #queue)) - return queue -end - ----@param inventory Barotrauma.ItemInventory ----@param queue Barotrauma.Item[] ----@param predicate? fun(item: Barotrauma.Item): boolean ----@return Barotrauma.Item[], string? -enqueueInventory = function(inventory, queue, predicate) - queue = queue or {} - predicate = predicate or function() return true end - -- debugPrint(string.format("Enqueuing inventory with %d slots.", #inventory.slots)) - local err - for _, slot in ipairs(inventory.slots) do - -- Only the final leaf nodes decide upon the predicate - queue, err = enqueueSlot(slot, queue, predicate) - if err then - return queue, err - end - end - -- debugPrint(string.format("Finished enqueuing inventory. Current queue size: %d", #queue)) - return queue -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() - 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 = {} - -- debugPrint(string.format("Preparing to stack items into the bag...")) - local bagSlot = inventory.slots[8] - if bagSlot then - -- debugPrint(string.format("Bag slot found at index 8 with %d items.", #bagSlot.items)) - if #bagSlot.items > 0 then - local item = bagSlot.items[1] - -- debugPrint(string.format("Found item in bag slot: %s", item.Name)) - if item and item.OwnInventory then - -- 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 - -- -- Register necessary type to access VisualSlot -- LuaUserData.RegisterType("Barotrauma.VisualSlot") --- +-- -- ---@return Barotrauma.VisualSlot|nil, Barotrauma.ItemInventory|nil, Barotrauma.Item|nil -- local function getInventorySlotUnderCursor() -- -- Make sure we have a controlled character -- local controlledCharacter = Character.Controlled -- if not controlledCharacter then return nil, nil, nil end --- +-- -- -- Check player inventory first -- local charInventory = controlledCharacter.Inventory -- if charInventory and charInventory.visualSlots then @@ -366,7 +89,7 @@ end -- end -- end -- end --- +-- -- -- Check if selected item has inventory (containers, etc.) -- local selectedItem = controlledCharacter.SelectedItem -- if selectedItem and selectedItem.OwnInventory and selectedItem.OwnInventory.visualSlots then @@ -382,7 +105,7 @@ end -- end -- end -- end --- +-- -- -- Check open containers or other items with visible inventories -- for item in Item.ItemList do -- if item and item.OwnInventory and item.OwnInventory.visualSlots then @@ -399,98 +122,48 @@ end -- end -- end -- end --- +-- -- return nil, nil, 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 BAG_SLOT = 8 -local function quickStackItems(character) - if not character then - debugPrint("No character found") - return - end +-- -- Register necessary types for detecting repairable objects +-- LuaUserData.RegisterType("Barotrauma.Items.Components.Repairable") +-- LuaUserData.MakeFieldAccessible(Descriptors["Barotrauma.Items.Components.Repairable"], "RepairButton") +-- +-- ---@return Barotrauma.Item|nil, Barotrauma.Items.Components.Repairable|nil +-- local function getRepairableObjectInFocus() +-- -- Make sure we have a controlled character +-- local controlledCharacter = Character.Controlled +-- if not controlledCharacter then return nil, nil end +-- +-- -- Check if we have a selected item - this is necessary for repair interfaces to show +-- local selectedItem = controlledCharacter.SelectedItem +-- if not selectedItem then return nil, nil end +-- +-- -- Check if the selected item is in fact the repairable object itself +-- for _, component in pairs(selectedItem.Components) do +-- if component.name == "Repairable" then +-- -- Check if repair interface should be shown +-- if component:ShouldDrawHUD(controlledCharacter) then +-- return selectedItem, component +-- end +-- end +-- end +-- +-- -- Nothing found +-- return nil, nil +-- end +-- +-- ---@return boolean +-- local function isRepairButtonVisible() +-- local _, repairableComponent = getRepairableObjectInFocus() +-- if not repairableComponent then return false end +-- +-- -- Check if the repair button exists and is visible +-- local repairButton = repairableComponent.RepairButton +-- return repairButton ~= nil and repairButton.Visible +-- 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, err = tryBuildItemTree(inventory) - if err then - 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 - 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 - 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 = enqueueSlot(slot, toMove) - local after = #toMove - 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 - debugPrint(string.format("Found %d inventories in the open container", #inventories)) - for containerInventory in inventories do - debugPrint(string.format("Enqueuing inventory with %d slots", #containerInventory.slots)) - local before = #toMove - toMove = enqueueInventory(containerInventory, toMove) - local after = #toMove - 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(CONFIG.QUICKSTACK_KEYS) then return end - - local character = instance - if not character then return end - - quickStackItems(character) -end, Hook.HookMethodType.After) ---------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------- diff --git a/QuickStackToBag/Lua/Cyka/quickstack.lua b/QuickStackToBag/Lua/Cyka/quickstack.lua new file mode 100644 index 0000000..1e7e78b --- /dev/null +++ b/QuickStackToBag/Lua/Cyka/quickstack.lua @@ -0,0 +1,292 @@ +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) diff --git a/QuickStackToBag/Lua/Cyka/utils.lua b/QuickStackToBag/Lua/Cyka/utils.lua new file mode 100644 index 0000000..30c525a --- /dev/null +++ b/QuickStackToBag/Lua/Cyka/utils.lua @@ -0,0 +1,87 @@ +-- We got to do this shit because enqueueInventory calls enqueueItem +-- And enqueueItem calls enqueueInventory +-- So unless we define them both before using them +-- We will get an error saying either is undefined +local enqueueItem +local enqueueSlot +local enqueueInventory +local _ + +---@param item Barotrauma.Item +---@param queue Barotrauma.Item[] +---@param predicate? fun(item: Barotrauma.Item): boolean +---@return Barotrauma.Item[], string? +enqueueItem = function(item, queue, predicate) + queue = queue or {} + predicate = predicate or function() return true end + -- debugPrint(string.format("Enqueuing item: %s", item.Prefab.Identifier.Value)) + -- local err + -- This should make it breadth first, right...? + -- No, not yet... + local ok, stop = predicate(item) + if ok then + queue[#queue + 1] = item + end + if stop then return queue, "Stop" end + if item.OwnInventory then + -- As far as I know every item has only one inventory + -- Only machines have multiple + -- So inventrorY should be fine here + -- debugPrint("Item has its own inventory, enqueuing inventory...") + queue, _ = enqueueInventory(item.OwnInventory, queue, predicate) + -- if err then + -- debugPrint(string.format("Error enqueuing inventory: %s", err)) + -- end + end + -- debugPrint(string.format("Item enqueued. Current queue size: %d", #queue)) + return queue, nil +end + +---@param slot Barotrauma.ItemInventory.Slot +---@param queue Barotrauma.Item[] +---@param predicate? fun(item: Barotrauma.Item): boolean +---@return Barotrauma.Item[], string? +enqueueSlot = function(slot, queue, predicate) + queue = queue or {} + predicate = predicate or function() return true end + -- debugPrint(string.format("Enqueuing slot with %d items.", #slot.items)) + -- We don't want to shadow queue + local err + -- If the slot is empty there's nothing to iterate + -- And we will naturally return queue as is + for _, item in ipairs(slot.items) do + -- Only the final leaf nodes decide upon the predicate + queue, err = enqueueItem(item, queue, predicate) + if err then + return queue, err + end + end + -- debugPrint(string.format("Finished enqueuing slot. Current queue size: %d", #queue)) + return queue +end + +---@param inventory Barotrauma.ItemInventory +---@param queue Barotrauma.Item[] +---@param predicate? fun(item: Barotrauma.Item): boolean +---@return Barotrauma.Item[], string? +enqueueInventory = function(inventory, queue, predicate) + queue = queue or {} + predicate = predicate or function() return true end + -- debugPrint(string.format("Enqueuing inventory with %d slots.", #inventory.slots)) + local err + for _, slot in ipairs(inventory.slots) do + -- Only the final leaf nodes decide upon the predicate + queue, err = enqueueSlot(slot, queue, predicate) + if err then + return queue, err + end + end + -- debugPrint(string.format("Finished enqueuing inventory. Current queue size: %d", #queue)) + return queue +end + +return { + enqueueItem = enqueueItem, + enqueueSlot = enqueueSlot, + enqueueInventory = enqueueInventory +}