Factor quickstack out
This commit is contained in:
@@ -1,6 +1,56 @@
|
|||||||
if SERVER then return end
|
if SERVER then return end
|
||||||
-- Docs: https://evilfactory.github.io/LuaCsForBarotrauma/lua-docs/manual/common-questions/
|
-- 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
|
-- Register necessary types and make fields accessible
|
||||||
LuaUserData.RegisterType("Barotrauma.Items.Components.ItemContainer+SlotRestrictions")
|
LuaUserData.RegisterType("Barotrauma.Items.Components.ItemContainer+SlotRestrictions")
|
||||||
LuaUserData.RegisterType(
|
LuaUserData.RegisterType(
|
||||||
@@ -15,333 +65,6 @@ LuaUserData.RegisterType("Barotrauma.ItemPrefab")
|
|||||||
LuaUserData.RegisterType("Barotrauma.Location+StoreInfo")
|
LuaUserData.RegisterType("Barotrauma.Location+StoreInfo")
|
||||||
LuaUserData.MakeMethodAccessible(Descriptors["Barotrauma.CargoManager"], "GetConfirmedSoldEntities")
|
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<string, ItemLocation[]>
|
|
||||||
---@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<string, ItemLocation[]>
|
|
||||||
---@return table<string, ItemLocation[]>
|
|
||||||
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<string, ItemLocation[]>
|
|
||||||
---@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<string, ItemLocation[]>
|
|
||||||
---@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, ItemLocation[]>, 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
|
-- -- Register necessary type to access VisualSlot
|
||||||
-- LuaUserData.RegisterType("Barotrauma.VisualSlot")
|
-- LuaUserData.RegisterType("Barotrauma.VisualSlot")
|
||||||
--
|
--
|
||||||
@@ -403,94 +126,44 @@ end
|
|||||||
-- return nil, nil, nil
|
-- return nil, nil, nil
|
||||||
-- end
|
-- end
|
||||||
|
|
||||||
-- Function to quickly stack items from inventory to containers
|
-- -- Register necessary types for detecting repairable objects
|
||||||
-- 6 and 7 are hands
|
-- LuaUserData.RegisterType("Barotrauma.Items.Components.Repairable")
|
||||||
-- 9..18 are main slots
|
-- LuaUserData.MakeFieldAccessible(Descriptors["Barotrauma.Items.Components.Repairable"], "RepairButton")
|
||||||
local inventorySlotsToStack = { 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 }
|
--
|
||||||
local BAG_SLOT = 8
|
-- ---@return Barotrauma.Item|nil, Barotrauma.Items.Components.Repairable|nil
|
||||||
local function quickStackItems(character)
|
-- local function getRepairableObjectInFocus()
|
||||||
if not character then
|
-- -- Make sure we have a controlled character
|
||||||
debugPrint("No character found")
|
-- local controlledCharacter = Character.Controlled
|
||||||
return
|
-- if not controlledCharacter then return nil, nil end
|
||||||
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)
|
|
||||||
|
|
||||||
----------------------------------------------------------------------------------------------------
|
----------------------------------------------------------------------------------------------------
|
||||||
----------------------------------------------------------------------------------------------------
|
----------------------------------------------------------------------------------------------------
|
||||||
|
292
QuickStackToBag/Lua/Cyka/quickstack.lua
Normal file
292
QuickStackToBag/Lua/Cyka/quickstack.lua
Normal file
@@ -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<string, ItemLocation[]>
|
||||||
|
---@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<string, ItemLocation[]>
|
||||||
|
---@return table<string, ItemLocation[]>
|
||||||
|
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<string, ItemLocation[]>
|
||||||
|
---@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<string, ItemLocation[]>
|
||||||
|
---@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, ItemLocation[]>, 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)
|
87
QuickStackToBag/Lua/Cyka/utils.lua
Normal file
87
QuickStackToBag/Lua/Cyka/utils.lua
Normal file
@@ -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
|
||||||
|
}
|
Reference in New Issue
Block a user