Compare commits

8 Commits
old ... master

10 changed files with 758 additions and 548 deletions

23
CykaQuick/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,23 @@
{
"highlight.regex.workspace.regexes": [
{
"name": "namedGroups",
"regexes": [
{
"regex": "(?:(\\?<[^>]+>)[^)]+)",
"decorations": [
{
"backgroundColor": "#000000",
"color": "#1CAFC4"
}
]
}
]
}
],
"Lua.diagnostics.libraryFiles": "Enable",
"Lua.workspace.library": [
"../Meta/Types/client",
"../Meta/Types/shared"
],
}

View File

@@ -7,7 +7,6 @@ if SERVER then
require("Cyka.xpticker") require("Cyka.xpticker")
else else
---@class MyModGlobal ---@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_NAME string
---@field MOD_VERSION string ---@field MOD_VERSION string
---@field DumpTable fun(table: table, depth?: number) ---@field DumpTable fun(table: table, depth?: number)

View File

@@ -4,7 +4,7 @@ if not CLIENT then return end
local dump = require("Cyka.dump") local dump = require("Cyka.dump")
local quickstack = require("Cyka.quickstack") local quickstack = require("Cyka.quickstack")
---@class ItemLocation ---@class ItemLocationDistance
---@field item Barotrauma.Item ---@field item Barotrauma.Item
---@field distance number ---@field distance number

View File

@@ -60,7 +60,7 @@ local function tryStackCursorItem()
end end
itemTree = quickstack.sortItemTree(itemTree) itemTree = quickstack.sortItemTree(itemTree)
local itemsToMove = {} local options = {}
local now = Timer.GetTime() local now = Timer.GetTime()
for _, slot in ipairs(slots) do for _, slot in ipairs(slots) do
local runAfter = slotThrottle[slot] or 0 local runAfter = slotThrottle[slot] or 0
@@ -68,7 +68,7 @@ local function tryStackCursorItem()
goto continue goto continue
end end
-- MyModGlobal.debugPrint(string.format("Enqueuing slot: %s, before: %d", tostring(slot), #itemsToMove)) -- MyModGlobal.debugPrint(string.format("Enqueuing slot: %s, before: %d", tostring(slot), #itemsToMove))
utils.enqueueSlot(slot.slot, itemsToMove) options = utils.enqueueSlot(slot.inventory.slots[slot.slotIndex1], options)
-- MyModGlobal.debugPrint(string.format("Enqueuing slot: %s, after: %d", tostring(slot), #itemsToMove)) -- MyModGlobal.debugPrint(string.format("Enqueuing slot: %s, after: %d", tostring(slot), #itemsToMove))
slotThrottle[slot] = now + 1 slotThrottle[slot] = now + 1
::continue:: ::continue::
@@ -79,7 +79,7 @@ local function tryStackCursorItem()
-- -- MyModGlobal.debugPrint(string.format("Enqueued %d items from the inventory slot", #itemsToMove)) -- -- MyModGlobal.debugPrint(string.format("Enqueued %d items from the inventory slot", #itemsToMove))
-- MyModGlobal.DumpTable(itemTree) -- MyModGlobal.DumpTable(itemTree)
quickstack.tryMoveItems(itemsToMove, itemTree, true) quickstack.tryMoveItems(options.itemQueue, itemTree, true)
-- local errors = quickstack.tryMoveItems(itemsToMove, itemTree, true) -- local errors = quickstack.tryMoveItems(itemsToMove, itemTree, true)
-- for _, error in ipairs(errors) do -- for _, error in ipairs(errors) do
-- MyModGlobal.debugPrint(string.format("Error moving item: %s", error)) -- MyModGlobal.debugPrint(string.format("Error moving item: %s", error))
@@ -101,14 +101,7 @@ local function setTargetInventory()
-- If we get multiple we'll use the first valid one -- If we get multiple we'll use the first valid one
-- Although everything is valid to us... -- Although everything is valid to us...
for _, slot in ipairs(slots) do for _, slot in ipairs(slots) do
local item local item = slot.item
local items = slot.slot.items
if not items or #items == 0 then
print(string.format("Slot is empty, setting target inventory to %s", tostring(slot.inventory)))
targetInventory = slot.inventory
goto continue
end
item = items[1]
if not item then if not item then
print(string.format("Item in slot is nil, setting target inventory to %s", tostring(slot.inventory))) print(string.format("Item in slot is nil, setting target inventory to %s", tostring(slot.inventory)))
targetInventory = slot.inventory targetInventory = slot.inventory

View File

@@ -3,7 +3,7 @@ if not CLIENT then return end
local utils = require("Cyka.utils") local utils = require("Cyka.utils")
local dump = require("Cyka.dump") local dump = require("Cyka.dump")
---@return {item: Barotrauma.Item, fabricator: Barotrauma.FabricatorComponent}, string? ---@return {item: Barotrauma.Item, fabricator: Barotrauma.Items.Components.Fabricator}?, string?
local function getOpenFabricator() local function getOpenFabricator()
-- Get the controlled character -- Get the controlled character
local controlledCharacter = Character.Controlled local controlledCharacter = Character.Controlled
@@ -29,8 +29,8 @@ end
---@field targetItem {identifier: string, name: string, amount: number} ---@field targetItem {identifier: string, name: string, amount: number}
---@field requiredItems {amount: number, minCondition: number, maxCondition: number, prefabs: string[]}[] ---@field requiredItems {amount: number, minCondition: number, maxCondition: number, prefabs: string[]}[]
---@param fabricator Barotrauma.FabricatorComponent ---@param fabricator Barotrauma.Items.Components.Fabricator
---@return RecipeInfo, string? ---@return RecipeInfo?, string?
local function getSelectedRecipeRequirements(fabricator) local function getSelectedRecipeRequirements(fabricator)
-- local openFabricator, err = getOpenFabricator() -- local openFabricator, err = getOpenFabricator()
-- if err then return nil, err end -- if err then return nil, err end
@@ -73,7 +73,7 @@ local function tryStackFabricator(character)
MyModGlobal.debugPrint("Character inventory is nil.") MyModGlobal.debugPrint("Character inventory is nil.")
return return
end end
---@type Barotrauma.ItemInventory.Slot ---@type Barotrauma.Inventory.ItemSlot
local bagSlot = inventory.slots[MyModGlobal.BAG_SLOT] local bagSlot = inventory.slots[MyModGlobal.BAG_SLOT]
if not bagSlot then if not bagSlot then
MyModGlobal.debugPrint("Bag slot not found.") MyModGlobal.debugPrint("Bag slot not found.")
@@ -91,14 +91,14 @@ local function tryStackFabricator(character)
end end
local fabricator, err = getOpenFabricator() local fabricator, err = getOpenFabricator()
if err then if err or not fabricator then
print(string.format("Error getting open fabricator: %s", err)) print(string.format("Error getting open fabricator: %s", err))
return return
end end
local recipe local recipe
recipe, err = getSelectedRecipeRequirements(fabricator.fabricator) recipe, err = getSelectedRecipeRequirements(fabricator.fabricator)
if err then if err or not recipe then
print(string.format("Error getting selected recipe requirements: %s", err)) print(string.format("Error getting selected recipe requirements: %s", err))
return return
end end
@@ -147,7 +147,10 @@ local function tryStackFabricator(character)
-- dump(itemsOnSubmarine) -- dump(itemsOnSubmarine)
-- MyModGlobal.DumpTable(toGet) -- MyModGlobal.DumpTable(toGet)
local items, _ = utils.enqueueAllOwnedItems({}, filter) local items, _ = utils.enqueueAllOwnedItems({
recurse = true,
itemPredicate = filter
})
-- if err then -- if err then
-- print(string.format("Error enqueueing all owned items: %s", err)) -- print(string.format("Error enqueueing all owned items: %s", err))
-- return -- return

View File

@@ -4,12 +4,12 @@ if not CLIENT then return end
---@return Barotrauma.Location.StoreInfo[], string? ---@return Barotrauma.Location.StoreInfo[], string?
local function getCurrentStore() local function getCurrentStore()
if not Game or not Game.GameSession or not Game.GameSession.Campaign then if not Game or not Game.GameSession or not Game.GameSession.Campaign then
return nil, "No game session found" return {}, "No game session found"
end end
local map = Game.GameSession.Campaign.Map local map = Game.GameSession.Campaign.Map
if not map or not map.CurrentLocation or not map.CurrentLocation.Stores then if not map or not map.CurrentLocation or not map.CurrentLocation.Stores then
return nil, "No map found" return {}, "No map found"
end end
local location = map.CurrentLocation local location = map.CurrentLocation
@@ -17,13 +17,13 @@ local function getCurrentStore()
-- Otherwise, determine which store is active by checking the cargo manager -- Otherwise, determine which store is active by checking the cargo manager
local cargoManager = Game.GameSession.Campaign.CargoManager local cargoManager = Game.GameSession.Campaign.CargoManager
if not cargoManager then if not cargoManager then
return nil, "No cargo manager found" return {}, "No cargo manager found"
end end
-- Find which store has items in the cart -- Find which store has items in the cart
local stores = {} local stores = {}
for _, store in pairs(location.Stores) do for _, store in pairs(location.Stores) do
if #cargoManager:GetBuyCrateItems(store) > 0 then if #cargoManager:GetBuyCrateItems() > 0 then
stores[#stores + 1] = store stores[#stores + 1] = store
end end
end end
@@ -47,7 +47,7 @@ local function tryBuy()
for _, store in ipairs(stores) do for _, store in ipairs(stores) do
local toAdd = {} local toAdd = {}
-- Get items available at the store -- Get items available at the store
local items = cargoManager:GetBuyCrateItems(store) local items = cargoManager:GetBuyCrateItems()
for item in items do for item in items do
-- We have already added this many of item -- We have already added this many of item
toAdd[item.ItemPrefab.Identifier.Value] = { toAdd[item.ItemPrefab.Identifier.Value] = {
@@ -55,6 +55,7 @@ local function tryBuy()
prefab = item.ItemPrefab -- Store the ItemPrefab object prefab = item.ItemPrefab -- Store the ItemPrefab object
} }
end end
---@diagnostic disable-next-line: undefined-field
for item in store.Stock do for item in store.Stock do
-- So if we add the total amount available -- So if we add the total amount available
-- We get the amount we have to add to buy entire stock -- We get the amount we have to add to buy entire stock
@@ -68,6 +69,7 @@ local function tryBuy()
if info.quantity > 0 then if info.quantity > 0 then
MyModGlobal.debugPrint(string.format("Adding %d of %s to the buy crate", info.quantity, idValue)) MyModGlobal.debugPrint(string.format("Adding %d of %s to the buy crate", info.quantity, idValue))
-- Use the stored ItemPrefab object, not the string identifier -- Use the stored ItemPrefab object, not the string identifier
---@diagnostic disable-next-line: undefined-field
cargoManager:ModifyItemQuantityInBuyCrate(store.Identifier, info.prefab, info.quantity) cargoManager:ModifyItemQuantityInBuyCrate(store.Identifier, info.prefab, info.quantity)
end end
end end

View File

@@ -16,14 +16,13 @@ local LOAD_MAP = require("Cyka.quickreload_loadmap")
---@param inventory Barotrauma.ItemInventory ---@param inventory Barotrauma.ItemInventory
---@return InventorySlot[] ---@return InventorySlot[]
local function getSlots(inventory) local function getSlots(inventory)
---@type InventorySlot[]
local slots = {} local slots = {}
---@type Barotrauma.Inventory.ItemSlot[]
local inventorySlots = inventory.slots local inventorySlots = inventory.slots
for i, inventorySlot in ipairs(inventorySlots) do for i, _ in ipairs(inventorySlots) do
slots[#slots + 1] = { local invSlot = MyModGlobal.InventorySlot.new(inventory, i)
inventory = inventory, slots[#slots + 1] = invSlot
slotIndex = i - 1,
slot = inventorySlot
}
end end
return slots return slots
end end
@@ -36,28 +35,33 @@ local function getItemsPerSlot(slots)
---@type table<InventorySlot, Barotrauma.Item[]> ---@type table<InventorySlot, Barotrauma.Item[]>
local movableBySlot = {} local movableBySlot = {}
-- Get all the items and then we will sort them by condition and shit -- Get all the items and then we will sort them by condition and shit
utils.enqueueAllPlayerItems({}, function(ititem, itemRef) utils.enqueuePlayerItems({
-- We don't want to take oxygen out of our diving suit to load our plasma cutter recurse = true,
-- Most loadable items have 1 capacity loadRefs = true,
-- But some have 2 or 3 (coil speargun) itemPredicate = function(ititem, itemRef)
if itemRef and itemRef.inventory and itemRef.inventory.Capacity < 4 then MyModGlobal.debugPrint(string.format("Checking item: %s", tostring(ititem)))
-- MyModGlobal.debugPrint(string.format("Skipping small inventory %s", tostring(itemRef.inventory))) -- We don't want to take oxygen out of our diving suit to load our plasma cutter
-- Most loadable items have 1 capacity
-- But some have 2 or 3 (coil speargun)
if itemRef and itemRef.inventory and itemRef.inventory.Capacity < 4 then
-- MyModGlobal.debugPrint(string.format("Skipping small inventory %s", tostring(itemRef.inventory)))
return false
end
-- MyModGlobal.debugPrint("Checking item:")
-- dump(slots)
-- MyModGlobal.debugPrint(ititem.Prefab.Identifier.Value)
for _, inventorySlot in ipairs(slots) do
local canMove = inventorySlot.inventory.CanBePutInSlot(ititem, inventorySlot.slotIndex1 - 1)
-- MyModGlobal.debugPrint(string.format("Can move to slot %d: %s", inventorySlot.slotIndex, tostring(canMove)))
if canMove then
movableBySlot[inventorySlot] = movableBySlot[inventorySlot] or {}
movableBySlot[inventorySlot][#movableBySlot[inventorySlot] + 1] = ititem
return true
end
end
return false return false
end end
-- MyModGlobal.debugPrint("Checking item:") })
-- dump(slots)
-- MyModGlobal.debugPrint(ititem.Prefab.Identifier.Value)
for _, inventorySlot in ipairs(slots) do
local canMove = inventorySlot.inventory.CanBePutInSlot(ititem, inventorySlot.slotIndex)
-- MyModGlobal.debugPrint(string.format("Can move to slot %d: %s", inventorySlot.slotIndex, tostring(canMove)))
if canMove then
movableBySlot[inventorySlot] = movableBySlot[inventorySlot] or {}
movableBySlot[inventorySlot][#movableBySlot[inventorySlot] + 1] = ititem
return true
end
end
return false
end, true)
return movableBySlot return movableBySlot
end end
@@ -93,11 +97,11 @@ local function printPermissibleItems(movableBySlot)
end end
---@param slot InventorySlot ---@param invSlot InventorySlot
---@param preferMinCondition boolean ---@param preferMinCondition boolean
local function tryReloadSlot(slot, preferMinCondition) local function tryReloadSlot(invSlot, preferMinCondition)
---@type Barotrauma.Item ---@type Barotrauma.Item
local item = slot.slot.items[1] local item = invSlot.item
if not item then if not item then
MyModGlobal.debugPrint("No item in slot") MyModGlobal.debugPrint("No item in slot")
return return
@@ -115,13 +119,10 @@ local function tryReloadSlot(slot, preferMinCondition)
MyModGlobal.debugPrint("No slots") MyModGlobal.debugPrint("No slots")
return return
end end
-- MyModGlobal.debugPrint("Slots:")
-- dump(slots)
---@type table<InventorySlot, Barotrauma.Item[]> ---@type table<InventorySlot, Barotrauma.Item[]>
local movableBySlot = getItemsPerSlot(slots) local movableBySlot = getItemsPerSlot(slots)
-- MyModGlobal.debugPrint("Movable by slot:") -- MyModGlobal.debugPrint("Movable by slot:")
-- dump(movableBySlot)
local permissibleItems = LOAD_MAP[tostring(item.Prefab.Identifier.Value)] local permissibleItems = LOAD_MAP[tostring(item.Prefab.Identifier.Value)]
if not permissibleItems then if not permissibleItems then
@@ -156,22 +157,19 @@ local function tryReloadSlot(slot, preferMinCondition)
-- We loaded as many as we have been allowed to -- We loaded as many as we have been allowed to
-- And we do this check up front because an item may already -- And we do this check up front because an item may already
-- Be partially loaded -- Be partially loaded
local nowHave = #inventorySlot.slot.items local nowHave = inventorySlot.stackSize
if nowHave >= permissible then if nowHave >= permissible then
-- MyModGlobal.debugPrint(string.format( MyModGlobal.debugPrint(string.format(
-- "Finished processing item: %s. Current slot has reached the permissible limit of %d items.", "Finished processing item: %s. Current slot has reached the permissible limit of %d items.",
-- tostring(ititem.Prefab.Identifier.Value), permissible)) tostring(ititem.Prefab.Identifier.Value), permissible))
break break
end end
local moved = inventorySlot.inventory.TryPutItem(ititem, inventorySlot.slotIndex, false, true, Character.Controlled, true) if not inventorySlot:canFit(ititem.Prefab) then
-- When the slot is full no more will be able to be moved break
-- And tat that point we're done with that slot end
if not moved then break end utils.enqueueMove(ititem, inventorySlot)
numMoved = numMoved + 1 numMoved = numMoved + 1
-- else
-- MyModGlobal.debugPrint(string.format("Not permissible: %s", tostring(ititem.Prefab.Identifier.Value)))
end end
end end
end end

View File

@@ -5,20 +5,14 @@ if not CLIENT then return end
local utils = require("Cyka.utils") local utils = require("Cyka.utils")
local dump = require("Cyka.dump") local dump = require("Cyka.dump")
---@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 -- 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 -- And the value is an object that represents where that item is located
-- In our inventory -- In our inventory
-- Special case are empty slots where any item fits -- Special case are empty slots where any item fits
---@param inventory Barotrauma.ItemInventory ---@param inventory Barotrauma.Inventory
---@param itemTree table<string, ItemLocation[]> ---@param itemTree? table<string, InventorySlot[]>
---@param depth number ---@param depth? number
---@return table<string, ItemLocation[]> ---@return table<string, InventorySlot[]>
local function buildItemTree(inventory, itemTree, depth) local function buildItemTree(inventory, itemTree, depth)
itemTree = itemTree or {} itemTree = itemTree or {}
depth = depth or 0 depth = depth or 0
@@ -29,38 +23,24 @@ local function buildItemTree(inventory, itemTree, depth)
-- One slot can have one item but multiple of it -- One slot can have one item but multiple of it
-- The number of an item in a slot is #slot.items -- The number of an item in a slot is #slot.items
for slotIndex, slot in ipairs(inventory.slots) do for slotIndex, _ in ipairs(inventory.slots) do
-- MyModGlobal.debugPrint(string.format("Building item tree for inventory at slot index: %d", slotIndex)) local invSlot = MyModGlobal.InventorySlot.new(inventory, slotIndex):with({ depth = depth })
-- MyModGlobal.debugPrint(string.format("Slot %d has %d items", slotIndex, #slot.items)) if not invSlot.item then
if #slot.items == 0 then
-- MyModGlobal.debugPrint(string.format("Slot %d is empty, adding to itemTree as 'empty'", slotIndex)) -- MyModGlobal.debugPrint(string.format("Slot %d is empty, adding to itemTree as 'empty'", slotIndex))
itemTree['empty'] = itemTree['empty'] or {} itemTree['empty'] = itemTree['empty'] or {}
itemTree['empty'][#itemTree['empty'] + 1] = { itemTree['empty'][#itemTree['empty'] + 1] = invSlot
inventory = inventory,
slotIndex = slotIndex - 1,
maxFits = 60,
depth = depth
}
-- MyModGlobal.debugPrint(string.format("Added empty slot to itemTree at index: %d", slotIndex)) -- MyModGlobal.debugPrint(string.format("Added empty slot to itemTree at index: %d", slotIndex))
else else
---@type Barotrauma.Item local identifier = invSlot.item.Prefab.Identifier.Value
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 {} itemTree[identifier] = itemTree[identifier] or {}
-- We DO want even slots with maxFits = 0 -- We DO want even slots with maxFits = 0
-- Because that indicates that we DO HAVE the item -- Because that indicates that we DO HAVE the item
-- At all -- At all
-- And based on that we decide to move it -- And based on that we decide to move it
itemTree[identifier][#itemTree[identifier] + 1] = { itemTree[identifier][#itemTree[identifier] + 1] = invSlot
inventory = inventory,
slotIndex = slotIndex - 1,
maxFits = slot.HowManyCanBePut(item.Prefab),
depth = depth
}
-- MyModGlobal.debugPrint(string.format("Added item to itemTree under identifier: %s", identifier)) -- MyModGlobal.debugPrint(string.format("Added item to itemTree under identifier: %s", identifier))
local tags = item.Prefab.Tags local tags = invSlot.item.Prefab.Tags
local shouldSuss = false local shouldSuss = false
for tag in tags do for tag in tags do
if tag.value:find("container") then if tag.value:find("container") then
@@ -71,7 +51,7 @@ local function buildItemTree(inventory, itemTree, depth)
if shouldSuss then if shouldSuss then
-- MyModGlobal.debugPrint(string.format("Searching inside %s for nested containers", item.Name)) -- MyModGlobal.debugPrint(string.format("Searching inside %s for nested containers", item.Name))
buildItemTree(item.OwnInventory, itemTree, depth + 1) buildItemTree(invSlot.item.OwnInventory, itemTree, depth + 1)
end end
end end
end end
@@ -81,19 +61,20 @@ local function buildItemTree(inventory, itemTree, depth)
end end
-- We would like to fill larger stacks first -- We would like to fill larger stacks first
---@param itemTree table<string, ItemLocation[]> ---@param itemTree table<string, InventorySlot[]>
---@return table<string, ItemLocation[]> ---@return table<string, InventorySlot[]>
local function sortItemTree(itemTree) local function sortItemTree(itemTree)
for _, item in pairs(itemTree) do for _, item in pairs(itemTree) do
table.sort(item, function(a, b) table.sort(item, function(a, b)
---@cast a ItemLocation ---@cast a InventorySlot
---@cast b ItemLocation ---@cast b InventorySlot
local maxfitsA, maxfitsB = a:maxFits(), b:maxFits()
if a.depth ~= b.depth then if a.depth ~= b.depth then
return a.depth < b.depth return a.depth < b.depth
elseif a.maxFits ~= b.maxFits then elseif maxfitsA ~= maxfitsB then
return a.maxFits > b.maxFits return maxfitsA > maxfitsB
else else
return a.slotIndex < b.slotIndex return a.slotIndex0 < b.slotIndex0
end end
end) end)
end end
@@ -102,16 +83,16 @@ local function sortItemTree(itemTree)
end end
---@param item Barotrauma.Item ---@param item Barotrauma.Item
---@param itemTree table<string, ItemLocation[]> ---@param itemTree table<string, InventorySlot[]>
---@param force boolean ---@param force boolean
---@return string ---@return string?
local function tryMoveItem(item, itemTree, force) local function tryMoveItem(item, itemTree, force)
-- MyModGlobal.debugPrint(string.format("Attempting to move item: %s", item.Prefab.Identifier.Value)) -- MyModGlobal.debugPrint(string.format("Attempting to move item: %s", item.Prefab.Identifier.Value))
force = force or false force = force or false
local location = itemTree[item.Prefab.Identifier.Value] local location = itemTree[item.Prefab.Identifier.Value]
if not location and not force then if not location and not force then
-- MyModGlobal.debugPrint("No locations for item, not stacking (not forced)") -- MyModGlobal.debugPrint("No locations for item, not stacking (not forced)")
return nil, "No locations for item, not stacking (not forced)" return "No locations for item, not stacking (not forced)"
end end
-- MyModGlobal.debugPrint(string.format("Attempting to move item: %s", item.Prefab.Identifier.Value)) -- MyModGlobal.debugPrint(string.format("Attempting to move item: %s", item.Prefab.Identifier.Value))
-- MyModGlobal.DumpTable(location) -- MyModGlobal.DumpTable(location)
@@ -121,22 +102,12 @@ local function tryMoveItem(item, itemTree, force)
-- First try to move to existing stacks -- First try to move to existing stacks
for _, itemLocation in ipairs(location) do for _, itemLocation in ipairs(location) do
-- We cannot stack items with decreased condition -- We cannot stack items with decreased condition
local canBePut = itemLocation.inventory.CanBePutInSlot(item.Prefab, itemLocation.slotIndex, item.Condition) local canFit = itemLocation:canFit(item.Prefab)
-- MyModGlobal.debugPrint(string.format("Can be put in slot %d: %s", itemLocation.slotIndex, tostring(canBePut))) if canFit then
-- There's no more guess work, if we call move then we must be sure we can move
if itemLocation.maxFits > 0 and canBePut then utils.enqueueMove(item, itemLocation)
moved = moved or moved = true
itemLocation.inventory.TryPutItem(item, itemLocation.slotIndex, false, true, Character.Controlled, break
true)
if moved then
itemLocation.maxFits = itemLocation.inventory.HowManyCanBePut(item.Prefab, itemLocation.slotIndex)
break
end
-- if moved then
-- MyModGlobal.debugPrint(string.format("Moved item to existing stack at slot index: %d", itemLocation .slotIndex))
-- else
-- MyModGlobal.debugPrint(string.format("Failed to move item to existing stack at slot index: %d", itemLocation .slotIndex))
-- end
end end
end end
end end
@@ -149,28 +120,16 @@ local function tryMoveItem(item, itemTree, force)
return "No empty slots found" return "No empty slots found"
end end
for _, itemLocation in ipairs(itemTree['empty']) do for _, itemLocation in ipairs(itemTree['empty']) do
local maxFits = itemLocation.maxFits
-- These empty slots are not guranteed to be empty, ironically -- These empty slots are not guranteed to be empty, ironically
-- After we insert an item into one it's no longer empty -- After we insert an item into one it's no longer empty
-- But it still is in the empty table -- But it still is in the empty table
-- So we want to make sure we can insert our item -- So we want to make sure we can insert our item
-- Into the maybe empty slots -- Into the maybe empty slots
itemLocation.maxFits = itemLocation.inventory.HowManyCanBePut(item.Prefab, itemLocation.slotIndex) local canFit = itemLocation:canFit(item.Prefab)
if canFit then
if maxFits > 0 then utils.enqueueMove(item, itemLocation)
-- MyModGlobal.debugPrint(string.format("Trying to move item to empty slot at index: %d", itemLocation.slotIndex)) moved = true
moved = moved or break
itemLocation.inventory.TryPutItem(item, itemLocation.slotIndex, true, false, Character.Controlled,
true)
if moved then
itemLocation.maxFits = itemLocation.inventory.HowManyCanBePut(item.Prefab, itemLocation.slotIndex)
break
end
-- if moved then
-- MyModGlobal.debugPrint(string.format("Moved item to empty slot at index: %d", itemLocation.slotIndex))
-- else
-- MyModGlobal.debugPrint(string.format("Failed to move item to empty slot at index: %d", itemLocation.slotIndex))
-- end
end end
end end
end end
@@ -185,8 +144,8 @@ local function tryMoveItem(item, itemTree, force)
end end
---@param items Barotrauma.Item[] ---@param items Barotrauma.Item[]
---@param itemTree table<string, ItemLocation[]> ---@param itemTree table<string, InventorySlot[]>
---@param force boolean ---@param force? boolean
---@return string[] ---@return string[]
local function tryMoveItems(items, itemTree, force) local function tryMoveItems(items, itemTree, force)
force = force or false force = force or false
@@ -202,7 +161,7 @@ local function tryMoveItems(items, itemTree, force)
end end
---@param character Barotrauma.Character ---@param character Barotrauma.Character
---@return table<string, ItemLocation[]>, string ---@return table<string, InventorySlot[]>, string?
local function tryBuildCharacterItemTree(character) local function tryBuildCharacterItemTree(character)
local itemTree = {} local itemTree = {}
-- MyModGlobal.debugPrint(string.format("Preparing to stack items into the bag...")) -- MyModGlobal.debugPrint(string.format("Preparing to stack items into the bag..."))
@@ -267,7 +226,7 @@ end
-- Function to quickly stack items from inventory to containers -- Function to quickly stack items from inventory to containers
-- 6 and 7 are hands -- 6 and 7 are hands
-- 9..18 are main slots -- 9..18 are main slots
local inventorySlotsToStack = { 6, 7, } -- local inventorySlotsToStack = { 6, 7, }
-- local inventorySlotsToStack = { 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 } -- local inventorySlotsToStack = { 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 }
---@param character Barotrauma.Character ---@param character Barotrauma.Character
local function quickStackItems(character) local function quickStackItems(character)
@@ -276,12 +235,10 @@ local function quickStackItems(character)
-- Then stack all items from the parent inventory into the mouseover container -- Then stack all items from the parent inventory into the mouseover container
local mouseover = utils.getFirstSlotUnderCursor() local mouseover = utils.getFirstSlotUnderCursor()
if mouseover then if mouseover then
---@type Barotrauma.Item local itemInventory = mouseover.item.OwnInventory
local slotItem = mouseover.slot.items[1]
local itemInventory = slotItem.OwnInventory
if itemInventory then if itemInventory then
MyModGlobal.debugPrint(string.format("Item inventory found: %s", tostring(itemInventory))) MyModGlobal.debugPrint(string.format("Item inventory found: %s", tostring(itemInventory)))
local err = stackToContainer(slotItem) local err = stackToContainer(mouseover.item)
if err then if err then
MyModGlobal.debugPrint(string.format("Error stacking items to container: %s", err)) MyModGlobal.debugPrint(string.format("Error stacking items to container: %s", err))
end end
@@ -341,6 +298,7 @@ local function quickStackItems(character)
-- end -- end
-- end -- end
-- TODO: enqueueOpenContainers?
local openContainers = utils.getOpenContainers() local openContainers = utils.getOpenContainers()
for _, container in ipairs(openContainers) do for _, container in ipairs(openContainers) do
local inventories = container.OwnInventories local inventories = container.OwnInventories
@@ -367,48 +325,38 @@ local function stackToCursor()
return return
end end
local item, slot
local function predicate(ititem) local function predicate(ititem)
if ititem.Prefab.Identifier.Value == item.Prefab.Identifier.Value then for _, invSlot in ipairs(slots) do
if item == ititem then return false end if invSlot:canFit(ititem.Prefab) then
-- We are moving items in the predicate because we expect to only utils.enqueueMove(ititem, invSlot)
-- Select a small subset of all items
-- And it is much easier to let the game decide when we can not move
-- Any more items (via return value of TryPutItem)
-- And we then know that we can safely stop
-- UPDATE: OK well that was a stupid idea, it returns an error for other shit as well
-- What other shit? Wish I knew
-- So we'll use HowManyCanBePut instead...
local moved = slot.inventory.TryPutItem(ititem, slot.slotIndex - 1, false, true, Character.Controlled,
true)
if not moved then
MyModGlobal.debugPrint(string.format("Failed to move item %s to slot %d", ititem.Name, slot
.slotIndex - 1))
-- return false, true
end
local maxFits = slot.inventory.HowManyCanBePut(ititem.Prefab, slot.slotIndex - 1)
if maxFits <= 0 then
MyModGlobal.debugPrint(string.format("Item %s has no more fits in slot %d", ititem.Name, slot
.slotIndex - 1))
return false, true
end end
end end
local haveSpace = false
for _, invSlot in ipairs(slots) do
-- Empty slot or has space for more items
if (invSlot.stackSize < invSlot.maxStackSize) or not invSlot.item then
haveSpace = true
break
end
end
if not haveSpace then return false, true end
end end
---@type EnqueueOptions
local options = {
itemPredicate = predicate,
recurse = true,
}
-- We gotta do a little juggling... -- We gotta do a little juggling...
for _, sslot in ipairs(slots) do for _, invSlot in ipairs(slots) do
slot = sslot if not invSlot.item then
local items
if not slot.slot.items or #slot.slot.items == 0 then
MyModGlobal.debugPrint("No items in slot") MyModGlobal.debugPrint("No items in slot")
goto continue goto continue
end end
item = slot.slot.items[1] MyModGlobal.debugPrint(string.format("Stacking all player items to %s", invSlot.item.Prefab.Identifier.Value))
MyModGlobal.debugPrint(string.format("Stacking all player items to %s", item.Prefab.Identifier.Value)) utils.enqueuePlayerItems(options)
items = {} utils.enqueueOpenContainers(options)
utils.enqueueAllPlayerItems(items, predicate)
utils.enqueueOpenContainers(items, predicate)
::continue:: ::continue::
end end
end end
@@ -420,45 +368,38 @@ local function stackAllToCursor()
return return
end end
for _, slot in ipairs(slots) do local function predicate(ititem)
local item, predicate for _, invSlot in ipairs(slots) do
if not slot.slot.items or #slot.slot.items == 0 then if invSlot:canFit(ititem.Prefab) then
utils.enqueueMove(ititem, invSlot)
end
end
local haveSpace = false
for _, invSlot in ipairs(slots) do
-- Empty slot or has space for more items
if (invSlot.stackSize < invSlot.maxStackSize) or not invSlot.item then
haveSpace = true
break
end
end
if not haveSpace then return false, true end
end
---@type EnqueueOptions
local options = {
itemPredicate = predicate,
recurse = true,
}
for _, invSlot in ipairs(slots) do
if not invSlot.item then
MyModGlobal.debugPrint("No items in slot") MyModGlobal.debugPrint("No items in slot")
goto continue goto continue
end end
item = slot.slot.items[1] MyModGlobal.debugPrint(string.format("Stacking all items to %s", invSlot.item.Prefab.Identifier.Value))
MyModGlobal.debugPrint(string.format("Stacking all items to %s", item.Prefab.Identifier.Value)) utils.enqueueSubmarineItems(options)
predicate = function(ititem) utils.enqueuePlayerItems(options)
if ititem.Prefab.Identifier.Value == item.Prefab.Identifier.Value then
if item == ititem then return false end
-- We are moving items in the predicate because we expect to only
-- Select a small subset of all items
-- And it is much easier to let the game decide when we can not move
-- Any more items (via return value of TryPutItem)
-- And we then know that we can safely stop
-- UPDATE: OK well that was a stupid idea, it returns an error for other shit as well
-- What other shit? Wish I knew
-- So we'll use HowManyCanBePut instead...
local moved = slot.inventory.TryPutItem(ititem, slot.slotIndex - 1, false, true, Character.Controlled,
true)
if not moved then
MyModGlobal.debugPrint(string.format("Failed to move item %s to slot %d", ititem.Name, slot
.slotIndex - 1))
-- return false, true
end
local maxFits = slot.inventory.HowManyCanBePut(ititem.Prefab, slot.slotIndex - 1)
if maxFits <= 0 then
MyModGlobal.debugPrint(string.format("Item %s has no more fits in slot %d", ititem.Name, slot
.slotIndex - 1))
return false, true
end
end
end
utils.enqueueAllSubmarineItems({}, predicate)
utils.enqueueAllPlayerItems({}, predicate)
::continue:: ::continue::
end end
end end

View File

@@ -2,106 +2,49 @@
if not CLIENT then return end if not CLIENT then return end
local utils = require("Cyka.utils") local utils = require("Cyka.utils")
local dump = require("Cyka.dump")
---@param inventory Barotrauma.ItemInventory ---@param invSlot InventorySlot
---@param predicate fun(slot: InventorySlot): boolean local function tryUnloadSlot(invSlot)
---@return InventorySlot[], string? ---@type table<Barotrauma.ItemPrefab, boolean>
local function findSlotsThat(inventory, predicate)
local slots = {}
for i, slot in ipairs(inventory.slots) do
local inventorySlot = {
slot = slot,
inventory = inventory,
slotIndex = i - 1
}
if predicate(inventorySlot) then
slots[#slots + 1] = inventorySlot
end
end
return slots
end
---@param slot InventorySlot
local function tryUnloadSlot(slot)
---@type Barotrauma.Item
local item = slot.slot.items[1]
if not item then
MyModGlobal.debugPrint("No item in slot")
return
end
local inventory = item.OwnInventory
if not inventory then
MyModGlobal.debugPrint("Item has no own inventory")
return
end
local toUnload = {}
local toUnloadByPrefab = {} local toUnloadByPrefab = {}
local inventorySlots = inventory.slots
for _, inventorySlot in ipairs(inventorySlots) do local itemInventory = invSlot.item.OwnInventory
for _, inventoryItem in ipairs(inventorySlot.items) do if not itemInventory then
toUnload[#toUnload + 1] = inventoryItem MyModGlobal.debugPrint("No inventory for item")
-- This will only serve as O(1) lookup return
toUnloadByPrefab[inventoryItem.Prefab] = true
end
end end
MyModGlobal.debugPrint(string.format("Enqueuing inventory %s", tostring(itemInventory)))
local toUnload = utils.enqueueInventory(itemInventory, {
itemPredicate = function(item)
toUnloadByPrefab[item.Prefab] = true
return true
end,
recurse = false,
})
MyModGlobal.debugPrint(string.format("Moving %d items to unload %s", #toUnload.itemQueue, tostring(invSlot.item)))
-- Where can we put our toUnload items? -- Where can we put our toUnload items?
local nearbySlots = findSlotsThat(slot.inventory, function(islot) local nearbySlots = invSlot:getNearbySlots(function(islot)
local isEmpty = #islot.slot.items == 0 local isEmpty = not islot.item
if isEmpty then return true end if isEmpty then return true end
for _, prefab in ipairs(toUnloadByPrefab) do for prefab, _ in pairs(toUnloadByPrefab) do
local canAccept = islot.inventory.CanBePutInSlot(prefab, islot.slotIndex) local canFit = islot:canFit(prefab)
if canAccept then return true end if canFit then return true end
end end
return false return false
end) end)
-- print("Before sorting:") MyModGlobal.debugPrint(string.format("Into %d nearby slots", #nearbySlots))
-- dump(nearbySlots)
-- Some inventories don't have slots per row, like the player inventory
local slotsPerRow = 900
local ok, err = pcall(function()
slotsPerRow = slot.inventory.slotsPerRow
end)
if not ok then
MyModGlobal.debugPrint(string.format("Error getting slots per row: %s", err))
end
local getGridPos = function(slotIndex) for _, iitem in ipairs(toUnload.itemQueue) do
local x = slotIndex % slotsPerRow
local y = math.floor(slotIndex / slotsPerRow)
return x, y
end
-- We are offsetting here by 1 because the backend uses 0-indexed slots
-- And the lua uses 1-indexed slots
-- We are trying to match the backend behavior for sorting
local slotx, sloty = getGridPos(slot.slotIndex - 1)
-- print(string.format("Slot position %d: %d, %d", slot.slotIndex, slotx, sloty))
table.sort(nearbySlots, function(a, b)
local ax, ay = getGridPos(a.slotIndex)
local bx, by = getGridPos(b.slotIndex)
local distA = math.max(math.abs(ax - slotx), math.abs(ay - sloty))
local distB = math.max(math.abs(bx - slotx), math.abs(by - sloty))
if distA == distB then
return a.slotIndex < b.slotIndex
end
return distA < distB
end)
-- print(string.format("Current slot: %d at (%d, %d)", slot.slotIndex, slotx, sloty))
for _, iitem in ipairs(toUnload) do
for _, nearbySlot in ipairs(nearbySlots) do for _, nearbySlot in ipairs(nearbySlots) do
local canAccept = nearbySlot.inventory.CanBePutInSlot(iitem.Prefab, nearbySlot.slotIndex) local canAccept = nearbySlot:canFit(iitem.Prefab)
if canAccept then if canAccept then
local moved = nearbySlot.inventory.TryPutItem(iitem, nearbySlot.slotIndex, true, false, utils.enqueueMove(iitem, nearbySlot)
Character.Controlled, true) break
-- print(string.format("Moved item %s to slot %d", iitem.Name, nearbySlot.slotIndex))
if moved then break end
end end
end end
end end
@@ -110,12 +53,12 @@ end
local function tryUnloadCursorItem() local function tryUnloadCursorItem()
local slots, err = utils.getSlotsUnderCursor() local slots, err = utils.getSlotsUnderCursor()
if err then if err then
-- MyModGlobal.debugPrint(string.format("Error getting inventory slot: %s", err)) MyModGlobal.debugPrint(string.format("Error getting inventory slot: %s", err))
return return
end end
if not slots or #slots == 0 then if not slots or #slots == 0 then
-- MyModGlobal.debugPrint("No items in slot") MyModGlobal.debugPrint("No items in slot")
return return
end end

View File

@@ -1,15 +1,290 @@
-- luacheck: globals Character MyModGlobal -- luacheck: globals Character MyModGlobal Timer _
-- luacheck: max line length 420 -- luacheck: max line length 420
---@class ItemRefs ---@class Barotrauma.Inventory.ItemSlot
---@field item Barotrauma.Item ---@field items Barotrauma.Item[]
---@field inventory Barotrauma.ItemInventory
---@field slot Barotrauma.ItemInventory.Slot ---@class HollowInventorySlot
---@field inventory? Barotrauma.Inventory
---@field slotIndex1? number Lua based item slots
---@field slotIndex0? number Barotrauma API based item slots
---@field item? Barotrauma.Item
---@field stackSize? number
---@field maxStackSize? number
---@field depth? number Currently almost always 0
-- local globalInventorySlotCache = {}
---@class InventorySlot ---@field slot Barotrauma.Inventory.ItemSlot
---@field inventory Barotrauma.Inventory
---@field slotIndex1 number Lua based item slots
---@field slotIndex0 number Barotrauma API based item slots
---@field item Barotrauma.Item
---@field stackSize number
---@field maxStackSize number
---@field depth number Currently almost always 0
-- ---@field lastUpdated number
MyModGlobal.InventorySlot = {
---@param inventory Barotrauma.Inventory
---@param slotIndex1 number
---@return InventorySlot
new = function(inventory, slotIndex1)
local self = setmetatable({}, {
__index = MyModGlobal.InventorySlot
})
self.inventory = inventory
self.slotIndex1 = slotIndex1
self.slotIndex0 = slotIndex1 - 1
self.stackSize = 0
self.maxStackSize = 0
self.depth = 0
-- self:update()
if inventory and inventory.slots and #inventory.slots > 0 then
self.slot = inventory.slots[slotIndex1]
end
if self.slot and self.slot.items and #self.slot.items > 0 then
self.item = self.slot.items[1]
self.stackSize = #self.slot.items
-- At this point inventory has to exist
-- If it didn't slot wouldn't either and then this wouldn't either
self.maxStackSize = self.item.Prefab.GetMaxStackSize(inventory)
end
return self
end,
--- A very weird builder indeed
---@param self InventorySlot
---@param other HollowInventorySlot
with = function(self, other)
if other.inventory ~= nil then
self.inventory = other.inventory
end
if other.slotIndex1 ~= nil then
self.slotIndex1 = other.slotIndex1
end
if other.slotIndex0 ~= nil then
self.slotIndex0 = other.slotIndex0
end
if other.item ~= nil then
self.item = other.item
end
if other.stackSize ~= nil then
self.stackSize = other.stackSize
end
if other.maxStackSize ~= nil then
self.maxStackSize = other.maxStackSize
end
if other.depth ~= nil then
self.depth = other.depth
end
return self
end,
---@param self InventorySlot
---@param item Barotrauma.Item
pretendMoved = function(self, item)
if not self.inventory then
MyModGlobal.debugPrint("Error pretending moved but it was moved to nil inventory")
return
end
-- Slot was previously empty, we want to figure out its max stack for the new item
if not self.item then
self.maxStackSize = item.Prefab.GetMaxStackSize(self.inventory)
end
self.item = item
self.stackSize = self.stackSize + 1
end,
update = function(self)
-- self.lastUpdated = Timer.GetTime()
if not self.inventory then
MyModGlobal.debugPrint("Error updating inventory slot, inventory not found")
return
end
if not self.inventory.slots then
MyModGlobal.debugPrint("Error updating inventory slot, inventory has no slots")
return
end
local slot = self.inventory.slots[self.slotIndex1]
if not slot then
MyModGlobal.debugPrint("Error updating inventory slot, slot not found")
return
end
self.slot = slot
if not slot.items or #slot.items == 0 then
-- MyModGlobal.debugPrint("Error updating inventory slot, slot is empty")
return
end
self.item = slot.items[1]
self.stackSize = #slot.items
self.maxStackSize = self.item.Prefab.GetMaxStackSize(self.inventory)
end,
__tostring = function(self)
return string.format(
"InventorySlot(inventory=%s, item=%s, stackSize=%d, maxStackSize=%d, slotIndex1=%d, slotIndex0=%d)",
tostring(self.inventory), tostring(self.item), self.stackSize, self.maxStackSize, self.slotIndex1,
self.slotIndex0)
end,
---@param self InventorySlot
---@param predicate? fun(slot: InventorySlot): boolean
---@return InventorySlot[]
getNearbySlots = function(self, predicate)
predicate = predicate or function() return true end
local slotsPerRow = 900
local ok, err = pcall(function()
slotsPerRow = self.inventory.slotsPerRow
end)
if not ok then
MyModGlobal.debugPrint(string.format("Error getting slots per row: %s", err))
end
local getGridPos = function(slotIndex)
local x = slotIndex % slotsPerRow
local y = math.floor(slotIndex / slotsPerRow)
return x, y
end
local slots = {}
for slotIndex, _ in ipairs(self.inventory.slots) do
local inventorySlot = MyModGlobal.InventorySlot.new(self.inventory, slotIndex)
if predicate(inventorySlot) then
slots[#slots + 1] = inventorySlot
end
end
local slotx, sloty = getGridPos(self.slotIndex0)
table.sort(slots, function(a, b)
local ax, ay = getGridPos(a.slotIndex0)
local bx, by = getGridPos(b.slotIndex0)
-- Chebyshev distance
local distA = math.max(math.abs(ax - slotx), math.abs(ay - sloty))
local distB = math.max(math.abs(bx - slotx), math.abs(by - sloty))
if distA == distB then
return a.slotIndex0 < b.slotIndex0
end
return distA < distB
end)
return slots
end,
--- TODO: What about item condition?
---@param self InventorySlot
---@param itemPrefab Barotrauma.ItemPrefab
---@return number
howManyCanFit = function(self, itemPrefab)
-- There is an item in the slot and it's not stackable with itemPrefab
if self.item and not self.item.Prefab.Equals(itemPrefab) then
return 0
end
-- The slot is empty - we can fit as many as the game tells us
if not self.item then
return itemPrefab.GetMaxStackSize(self.inventory)
end
-- The slot has an item that is stackable with itemPrefab
-- We can fit as many as to fill the stack
return self.maxStackSize - self.stackSize
end,
---@param self InventorySlot
---@param itemPrefab Barotrauma.ItemPrefab
---@return boolean
canFit = function(self, itemPrefab)
return self:howManyCanFit(itemPrefab) > 0
end,
---@param self InventorySlot
---@return number
maxFits = function(self)
return self.maxStackSize - self.stackSize
end
-- hash = function(self)
-- return string.format("%s:%d:%d", tostring(self.inventory), self.slotIndex1, self.slotIndex0)
-- end
}
---@class ItemMoveRequest
---@field what Barotrauma.Item
---@field where InventorySlot
---@field allowSwap boolean
---@field allowCombine boolean
local enqueueMove
do
-- A bit of cheeky scoping
local enabled = true
---@type ItemMoveRequest[]
local itemMoveQueue = {}
---@type table<Barotrauma.Item, boolean>
local itemLookup = {}
local rate = 500
local perIteration = 30
local noQueue = true
-- rate / 1000 is ms to seconds and *perIteraion is number of items per second
local maxQueueSize = 10 * (1000 / rate * perIteration)
local function processQueue()
if noQueue then return end
-- MyModGlobal.debugPrint("Processing queue")
Timer.Wait(processQueue, rate)
if not enabled then return end
if #itemMoveQueue == 0 then return end
local iterations = math.min(perIteration, #itemMoveQueue)
for _ = 1, iterations do
---@type ItemMoveRequest
local moveRequest = table.remove(itemMoveQueue, 1)
-- TODO: Maybe try and figure out if we CAN put A into B
moveRequest.allowCombine = moveRequest.allowCombine or false
moveRequest.allowSwap = moveRequest.allowSwap or false
local success = moveRequest.where.inventory.TryPutItem(moveRequest.what, moveRequest.where.slotIndex0,
moveRequest.allowSwap, moveRequest.allowCombine, nil)
if not success then
MyModGlobal.debugPrint(string.format("Failed moving item from %s to %s", tostring(moveRequest.what),
tostring(moveRequest.where:__tostring())))
end
itemLookup[moveRequest.what] = nil
end
end
processQueue()
---@param what Barotrauma.Item
---@param where InventorySlot
---@param allowSwap? boolean
---@param allowCombine? boolean
enqueueMove = function(what, where, allowSwap, allowCombine)
allowCombine = allowCombine == true
allowSwap = allowSwap == true
if noQueue then
local success = where.inventory.TryPutItem(what, where.slotIndex0,
allowSwap, allowCombine, nil)
if not success then
MyModGlobal.debugPrint(string.format("Failed moving item from %s to %s", tostring(what),
tostring(where:__tostring())))
end
where:pretendMoved(what)
else
if #itemMoveQueue >= maxQueueSize then
MyModGlobal.debugPrint("Queue is full, skipping move")
return
end
if itemLookup[what] then
MyModGlobal.debugPrint("Item is already in the queue, skipping move")
return
end
MyModGlobal.debugPrint(string.format("Enqueuing move from %s to %s, now in queue %d/%d", tostring(what),
tostring(where:__tostring()), #itemMoveQueue, maxQueueSize))
table.insert(itemMoveQueue, {
what = what,
where = where,
allowSwap = allowSwap or false,
allowCombine = allowCombine ~= false,
})
itemLookup[what] = true
-- We will very optimistically pretend that this will 100% for sure work
where:pretendMoved(what)
end
end
end
---@class InventorySlot
---@field slot Barotrauma.ItemSlot
---@field inventory Barotrauma.ItemInventory
---@field slotIndex number
---@return Barotrauma.Item[], string? ---@return Barotrauma.Item[], string?
local function getOpenContainers() local function getOpenContainers()
@@ -23,7 +298,9 @@ end
---@return Barotrauma.Item, string? ---@return Barotrauma.Item, string?
local function getFirstOpenContainer() local function getFirstOpenContainer()
local containers, err = getOpenContainers() local containers, err = getOpenContainers()
---@diagnostic disable-next-line: return-type-mismatch
if err then return nil, err end if err then return nil, err end
---@diagnostic disable-next-line: return-type-mismatch
if #containers == 0 then return nil, "No open containers" end if #containers == 0 then return nil, "No open containers" end
return containers[1], nil return containers[1], nil
end end
@@ -32,227 +309,263 @@ end
-- And enqueueItem calls enqueueInventory -- And enqueueItem calls enqueueInventory
-- So unless we define them both before using them -- So unless we define them both before using them
-- We will get an error saying either is undefined -- We will get an error saying either is undefined
-- TODO: Rework these enqueue functions to accept a params object
-- That will house all optional parameters
-- And in that include recurse boolean
---@class ItemRefs
---@field item Barotrauma.Item
---@field inventory Barotrauma.Inventory
---@field slot Barotrauma.Inventory.ItemSlot
---@field slotIndex1 number
---@class EnqueueOptions
---@field itemQueue? Barotrauma.Item[]
---@field slotQueue? Barotrauma.Inventory.ItemSlot[]
---@field inventoryQueue? Barotrauma.Inventory[]
---@field itemPredicate? fun(item: Barotrauma.Item, itemRef: ItemRefs): boolean
---@field slotPredicate? fun(slot: Barotrauma.Inventory.ItemSlot, itemRef: ItemRefs): boolean
---@field inventoryPredicate? fun(inventory: Barotrauma.Inventory, itemRef: ItemRefs): boolean
---@field loadRefs? boolean
---@field itemRef? ItemRefs
---@field recurse? boolean
---@param options EnqueueOptions
---@return EnqueueOptions
local function ensureOptionsDefaults(options)
options = options or {}
options.itemQueue = options.itemQueue or {}
options.slotQueue = options.slotQueue or {}
options.inventoryQueue = options.inventoryQueue or {}
options.itemPredicate = options.itemPredicate or function() return true end
options.slotPredicate = options.slotPredicate or function() return true end
options.inventoryPredicate = options.inventoryPredicate or function() return true end
options.loadRefs = options.loadRefs == true
options.itemRef = options.itemRef or {}
options.recurse = options.recurse == true
return options
end
local enqueueItem local enqueueItem
local enqueueSlot local enqueueSlot
local enqueueInventory local enqueueInventory
local enqueuePlayerItems
local enqueueOpenContainers local enqueueOpenContainers
local allPlayerItems local enqueueSubmarineItems
local allSubmarineItems local enqueueAllOwnedItems
local allOwnedItems
local _
---@alias FilterPredicate fun(item: Barotrauma.Item, inventoryRef?: Barotrauma.ItemInventory, slotRef: Barotrauma.ItemInventory.Slot): boolean do
---@param item Barotrauma.Item
---@param options EnqueueOptions
---@return EnqueueOptions, string?
enqueueItem = function(item, options)
options = ensureOptionsDefaults(options)
if not item then return options, "No item" end
-- Loading refs is optional because it MAY have a performance impact local ok, stop = options.itemPredicate(item, options.itemRef)
---@param item Barotrauma.Item
---@param queue Barotrauma.Item[]
---@param predicate? FilterPredicate
---@param loadRefs? boolean
---@param itemRef? ItemRefs
---@return Barotrauma.Item[], string?
enqueueItem = function(item, queue, predicate, loadRefs, itemRef)
queue = queue or {}
predicate = predicate or function() return true end
itemRef = itemRef or {}
-- debugPrint(string.format("Enqueuing item: %s", item.Prefab.Identifier.Value))
-- local err
-- This should make it breadth first, right...?
-- No, not yet...
if not item then return queue, "No item" end
local ok, stop = predicate(item, itemRef)
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...")
if loadRefs then
itemRef.item = item
queue, _ = enqueueInventory(item.OwnInventory, queue, predicate, loadRefs, itemRef)
else
queue, _ = enqueueInventory(item.OwnInventory, queue, predicate, itemRef)
end
-- 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? FilterPredicate
---@param loadRefs? boolean
---@param itemRef? ItemRefs
---@return Barotrauma.Item[], string?
enqueueSlot = function(slot, queue, predicate, loadRefs, itemRef)
queue = queue or {}
predicate = predicate or function() return true end
itemRef = itemRef or {}
-- 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
if not slot then return queue, "No slot" end
if not slot.items then return queue, "No items" end
for _, item in ipairs(slot.items) do
-- Only the final leaf nodes decide upon the predicate
if loadRefs then
itemRef.slot = slot
queue, err = enqueueItem(item, queue, predicate, loadRefs, itemRef)
else
queue, err = enqueueItem(item, queue, predicate)
end
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? FilterPredicate
---@param loadRefs? boolean
---@param itemRef? ItemRefs
---@return Barotrauma.Item[], string?
enqueueInventory = function(inventory, queue, predicate, loadRefs, itemRef)
queue = queue or {}
predicate = predicate or function() return true end
itemRef = itemRef or {}
-- debugPrint(string.format("Enqueuing inventory with %d slots.", #inventory.slots))
local err
if not inventory then return queue, "No inventory" end
if not inventory.slots then return queue, "No slots" end
for _, slot in ipairs(inventory.slots) do
-- Only the final leaf nodes decide upon the predicate
if loadRefs then
itemRef.inventory = inventory
queue, err = enqueueSlot(slot, queue, predicate, loadRefs, itemRef)
else
queue, err = enqueueSlot(slot, queue, predicate)
end
if err then
return queue, err
end
end
-- debugPrint(string.format("Finished enqueuing inventory. Current queue size: %d", #queue))
return queue
end
local relevantPlayerInventorySlots = { 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, MyModGlobal.BAG_SLOT }
---@param queue Barotrauma.Item[]
---@param predicate? FilterPredicate
---@param loadRefs? boolean
---@return Barotrauma.Item[], string?
allPlayerItems = function(queue, predicate, loadRefs)
queue = queue or {}
predicate = predicate or function() return true end
local character = Character.Controlled
if not character then return queue, "No character" end
local inventory = character.Inventory
if not inventory then return queue, "No inventory" end
for _, slotid in ipairs(relevantPlayerInventorySlots) do
local slot = inventory.slots[slotid]
local err
if not slot then goto continue end
if #slot.items == 0 then goto continue end
queue, err = enqueueSlot(slot, queue, predicate, loadRefs)
if err then return queue, err end
::continue::
end
return queue
end
---@param queue Barotrauma.Item[]
---@param predicate? FilterPredicate
---@param loadRefs? boolean
---@return Barotrauma.Item[], string?
enqueueOpenContainers = function(queue, predicate, loadRefs)
queue = queue or {}
predicate = predicate or function() return true end
local containers, err = getOpenContainers()
if err then return queue, err end
for _, container in ipairs(containers) do
local inventories = container.OwnInventories
if not inventories then goto continue end
for containerInventory in inventories do
queue, err = enqueueInventory(containerInventory, queue, predicate, loadRefs)
if err then return queue, err end
end
::continue::
end
return queue
end
---@param queue Barotrauma.Item[]
---@param predicate? FilterPredicate
---@return Barotrauma.Item[], string?
allSubmarineItems = function(queue, predicate)
queue = queue or {}
predicate = predicate or function() return true end
-- This only exists so predicate does not explode
-- Even if its empty
local itemRef = {}
local character = Character.Controlled
if not character then return queue, "No character" end
local submarine = character.Submarine
if not submarine then return queue, "No submarine" end
for item in submarine.GetItems(false) do
-- We do NOT want to call enqueueItem here because enqueueItem
-- Is recursive
-- And this call (GetItems) already gets all items
-- So we would be doing double the work (at best case)
-- It also means we won't have refs here which sucks
local ok, stop = predicate(item, itemRef)
if ok then if ok then
queue[#queue + 1] = item options.itemQueue[#options.itemQueue + 1] = item
end end
if stop then return queue, "Stop" end if stop then return options, "Stop" end
local err
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
if options.recurse then
if options.loadRefs then
options.itemRef.item = item
options.inventoryQueue, err = enqueueInventory(item.OwnInventory, options)
else
options.inventoryQueue, err = enqueueInventory(item.OwnInventory, options)
end
end
end
return options, err
end end
return queue ---@param slot Barotrauma.Inventory.ItemSlot
end ---@param options EnqueueOptions
---@return EnqueueOptions, string?
enqueueSlot = function(slot, options)
options = ensureOptionsDefaults(options)
if not slot then return options, "No slot" end
if not slot.items then return options, "No items" end
---@param queue Barotrauma.Item[] local ok, stop = options.slotPredicate(slot, options.itemRef)
---@param predicate? FilterPredicate if ok then
---@param loadRefs? boolean options.slotQueue[#options.slotQueue + 1] = slot
---@return Barotrauma.Item[], string? end
allOwnedItems = function(queue, predicate, loadRefs) if stop then return options, "Stop" end
queue = queue or {}
predicate = predicate or function() return true end
local err for _, item in ipairs(slot.items) do
queue, err = allPlayerItems(queue, predicate, loadRefs) -- We redeclare err every iteration so it doesn't spill over
if err then return queue, err end local err
if options.loadRefs then
options.itemRef.slot = slot
options, err = enqueueItem(item, options)
else
options, err = enqueueItem(item, options)
end
if err then
return options, err
end
end
return options
end
queue, err = allSubmarineItems(queue, predicate) ---@param inventory Barotrauma.Inventory
if err then return queue, err end ---@param options EnqueueOptions
---@return EnqueueOptions, string?
enqueueInventory = function(inventory, options)
options = ensureOptionsDefaults(options)
if not inventory then return options, "No inventory" end
if not inventory.slots then return options, "No slots" end
return queue local ok, stop = options.inventoryPredicate(inventory, options.itemRef)
if ok then
options.inventoryQueue[#options.inventoryQueue + 1] = inventory
end
if stop then return options, "Stop" end
for i, slot in ipairs(inventory.slots) do
local err
if options.loadRefs then
options.itemRef.inventory = inventory
options.itemRef.slot = slot
options.itemRef.slotIndex1 = i
options, err = enqueueSlot(slot, options)
else
options, err = enqueueSlot(slot, options)
end
if err then
return options, err
end
end
return options
end
local relevantPlayerInventorySlots = {
false,
false,
false,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true,
}
---@param options EnqueueOptions
---@return EnqueueOptions, string?
enqueuePlayerItems = function(options)
options = ensureOptionsDefaults(options)
local character = Character.Controlled
if not character then return options, "No character" end
local inventory = character.Inventory
if not inventory then return options, "No inventory" end
options.loadRefs = true
local originalItemPredicate = options.itemPredicate or function() return true end
options.itemPredicate = function(item)
if not item then return false end
local parentInventory = item.ParentInventory
if not parentInventory then return false end
if not parentInventory.Equals(inventory) then return false end
return originalItemPredicate(item, options.itemRef)
end
local originalSlotPredicate = options.slotPredicate or function() return true end
options.slotPredicate = function(slot, itemRef)
if not slot then return false end
if itemRef.slotIndex1 and relevantPlayerInventorySlots[itemRef.slotIndex1] then
return originalSlotPredicate(slot, itemRef)
end
return false
end
local err
options, err = enqueueInventory(inventory, options)
if err then return options, err end
return options
end
---@param options EnqueueOptions
---@return EnqueueOptions, string?
enqueueOpenContainers = function(options)
options = ensureOptionsDefaults(options)
local containers, err = getOpenContainers()
if err then return options, err end
for _, container in ipairs(containers) do
local inventories = container.OwnInventories
if not inventories then goto continue end
for containerInventory in inventories do
options, err = enqueueInventory(containerInventory, options)
if err then return options, err end
end
::continue::
end
return options
end
---@param options EnqueueOptions
---@return EnqueueOptions, string?
enqueueSubmarineItems = function(options)
options = ensureOptionsDefaults(options)
-- This only exists so predicate does not explode
-- Even if its empty
local itemRef = {}
local character = Character.Controlled
if not character then return options, "No character" end
local submarine = character.Submarine
if not submarine then return options, "No submarine" end
for item in submarine.GetItems(false) do
-- We do NOT want to call enqueueItem here because enqueueItem
-- Is recursive
-- And this call (GetItems) already gets all items
-- So we would be doing double the work (at best case)
-- It also means we won't have refs here which sucks
local ok, stop = options.itemPredicate(item, itemRef)
if ok then
options.itemQueue[#options.itemQueue + 1] = item
end
if stop then return options, "Stop" end
end
return options
end
---@param options EnqueueOptions
---@return EnqueueOptions, string?
enqueueAllOwnedItems = function(options)
options = ensureOptionsDefaults(options)
local err
options, err = enqueuePlayerItems(options)
if err then return options, err end
options, err = enqueueSubmarineItems(options)
if err then return options, err end
return options
end
end end
-- There is actually no need to recurse deep -- There is actually no need to recurse deep
@@ -260,8 +573,8 @@ end
-- And not an item in an item in the inventory -- And not an item in an item in the inventory
-- So in theory we only need to recurse 1 deep -- So in theory we only need to recurse 1 deep
---@param inventory Barotrauma.Inventory ---@param inventory Barotrauma.Inventory
---@param slots InventorySlot[] ---@param slots? InventorySlot[]
---@param depth number ---@param depth? number
---@return InventorySlot[], string? ---@return InventorySlot[], string?
local function getMouseoverSlots(inventory, slots, depth) local function getMouseoverSlots(inventory, slots, depth)
slots = slots or {} slots = slots or {}
@@ -269,7 +582,7 @@ local function getMouseoverSlots(inventory, slots, depth)
if depth > 1 then return slots, nil end if depth > 1 then return slots, nil end
local visualSlots = inventory.visualSlots local visualSlots = inventory.visualSlots
if not visualSlots then return nil, "Inventory has no visual slots" end if not visualSlots then return slots, "Inventory has no visual slots" end
for i, visualSlot in ipairs(visualSlots) do for i, visualSlot in ipairs(visualSlots) do
local item local item
@@ -305,11 +618,8 @@ local function getMouseoverSlots(inventory, slots, depth)
::mouseover:: ::mouseover::
if visualSlot:MouseOn() then if visualSlot:MouseOn() then
slots[#slots + 1] = { local inventorySlot = MyModGlobal.InventorySlot.new(inventory, i)
inventory = inventory, slots[#slots + 1] = inventorySlot
slotIndex = i,
slot = slot
}
end end
::continue:: ::continue::
@@ -320,15 +630,17 @@ end
---@return InventorySlot[], string? ---@return InventorySlot[], string?
local function getSlotsUnderCursor() local function getSlotsUnderCursor()
local slots = {}
-- Make sure we have a controlled character -- Make sure we have a controlled character
local controlledCharacter = Character.Controlled local controlledCharacter = Character.Controlled
if not controlledCharacter then return nil, "No controlled character" end if not controlledCharacter then return slots, "No controlled character" end
local inventory = controlledCharacter.Inventory local inventory = controlledCharacter.Inventory
if not inventory then return nil, "No inventory" end if not inventory then return slots, "No inventory" end
local mouseoverSlots, err = getMouseoverSlots(inventory) local err
if err then return mouseoverSlots, err end slots, err = getMouseoverSlots(inventory, slots)
if err then return slots, err end
-- Even if we don't get them we're still fine -- Even if we don't get them we're still fine
local openContainers, _ = getOpenContainers() local openContainers, _ = getOpenContainers()
@@ -337,35 +649,30 @@ local function getSlotsUnderCursor()
for _, container in ipairs(openContainers) do for _, container in ipairs(openContainers) do
local containerInventories = container.OwnInventories local containerInventories = container.OwnInventories
for containerInventory in containerInventories do for containerInventory in containerInventories do
local slot
if not containerInventory or not containerInventory.visualSlots then if not containerInventory or not containerInventory.visualSlots then
MyModGlobal.debugPrint("Container inventory has no visual slots") MyModGlobal.debugPrint("Container inventory has no visual slots")
goto continue goto continue
end end
for i, visualSlot in ipairs(containerInventory.visualSlots) do for i, visualSlot in ipairs(containerInventory.visualSlots) do
if visualSlot:MouseOn() then if visualSlot:MouseOn() then
slot = containerInventory.slots[i] local inventorySlot = MyModGlobal.InventorySlot.new(containerInventory, i)
mouseoverSlots[#mouseoverSlots + 1] = { slots[#slots + 1] = inventorySlot
inventory = containerInventory,
slotIndex = i,
slot = slot
}
end end
end end
::continue:: ::continue::
end end
end end
return mouseoverSlots, nil return slots, nil
end end
---@return InventorySlot, string? ---@return InventorySlot, string?
local function getFirstSlotUnderCursor() local function getFirstSlotUnderCursor()
local slots, err = getSlotsUnderCursor() local slots, err = getSlotsUnderCursor()
if err then return nil, err end if err then return slots, err end
if #slots == 0 then return nil, "No slots found under cursor" end if #slots == 0 then return slots, "No slots found under cursor" end
for _, slot in ipairs(slots) do for _, slot in ipairs(slots) do
if slot.slot.items and #slot.slot.items > 0 then if slot.item then
return slot return slot
end end
end end
@@ -376,12 +683,13 @@ return {
enqueueItem = enqueueItem, enqueueItem = enqueueItem,
enqueueSlot = enqueueSlot, enqueueSlot = enqueueSlot,
enqueueInventory = enqueueInventory, enqueueInventory = enqueueInventory,
enqueueAllPlayerItems = allPlayerItems, enqueuePlayerItems = enqueuePlayerItems,
enqueueAllSubmarineItems = allSubmarineItems, enqueueSubmarineItems = enqueueSubmarineItems,
enqueueAllOwnedItems = allOwnedItems, enqueueAllOwnedItems = enqueueAllOwnedItems,
enqueueOpenContainers = enqueueOpenContainers, enqueueOpenContainers = enqueueOpenContainers,
getOpenContainers = getOpenContainers, getOpenContainers = getOpenContainers,
getFirstOpenContainer = getFirstOpenContainer, getFirstOpenContainer = getFirstOpenContainer,
getSlotsUnderCursor = getSlotsUnderCursor, getSlotsUnderCursor = getSlotsUnderCursor,
getFirstSlotUnderCursor = getFirstSlotUnderCursor, getFirstSlotUnderCursor = getFirstSlotUnderCursor,
enqueueMove = enqueueMove,
} }