Files
barotrauma-localmods/CykaQuick/Lua/Cyka/utils.lua
2025-04-01 20:05:00 +02:00

629 lines
22 KiB
Lua

-- luacheck: globals Character MyModGlobal Timer _
-- luacheck: max line length 420
---@class Barotrauma.Inventory.ItemSlot
---@field items Barotrauma.Item[]
-- 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 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: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,
---@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
if not self.slot then
MyModGlobal.debugPrint("Error pretending moved but it was moved to nil slot")
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, slotIndex1=%d, slotIndex0=%d, item=%s, stackSize=%d, maxStackSize=%d)",
tostring(self.inventory), self.slotIndex1, self.slotIndex0, tostring(self.item), self.stackSize,
self.maxStackSize)
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
-- 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 = {}
local rate = 100
local perIteration = 6
local function processQueue()
-- 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, Character.Controlled, true)
if not success then
MyModGlobal.debugPrint(string.format("Failed moving item from %s to %s", tostring(moveRequest.what),
tostring(moveRequest.where)))
end
end
end
processQueue()
---@param what Barotrauma.Item
---@param where InventorySlot
---@param allowSwap? boolean
---@param allowCombine? boolean
enqueueMove = function(what, where, allowSwap, allowCombine)
MyModGlobal.debugPrint(string.format("Enqueuing move from %s to %s", tostring(what), tostring(where)))
table.insert(itemMoveQueue, {
what = what,
where = where,
allowSwap = allowSwap or false,
allowCombine = allowCombine ~= false,
})
-- We will very optimistically pretend that this will 100% for sure work
where:pretendMoved(what)
end
end
---@return Barotrauma.Item[], string?
local function getOpenContainers()
local controlledCharacter = Character.Controlled
if not controlledCharacter then return {}, "No controlled character" end
local selectedItem = controlledCharacter.SelectedItem
if not selectedItem then return {}, "No selected item" end
return { selectedItem }, nil
end
---@return Barotrauma.Item, string?
local function getFirstOpenContainer()
local containers, err = getOpenContainers()
---@diagnostic disable-next-line: return-type-mismatch
if err then return nil, err end
---@diagnostic disable-next-line: return-type-mismatch
if #containers == 0 then return nil, "No open containers" end
return containers[1], nil
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
-- 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 enqueueSlot
local enqueueInventory
local enqueuePlayerItems
local enqueueOpenContainers
local enqueueSubmarineItems
local enqueueAllOwnedItems
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
local ok, stop = options.itemPredicate(item, options.itemRef)
if ok then
options.itemQueue[#options.itemQueue + 1] = item
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
---@param slot Barotrauma.Inventory.ItemSlot
---@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
local ok, stop = options.slotPredicate(slot, options.itemRef)
if ok then
options.slotQueue[#options.slotQueue + 1] = slot
end
if stop then return options, "Stop" end
for _, item in ipairs(slot.items) do
-- We redeclare err every iteration so it doesn't spill over
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
---@param inventory Barotrauma.Inventory
---@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
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
-- There is actually no need to recurse deep
-- Because we can only have an item in the inventory open
-- And not an item in an item in the inventory
-- So in theory we only need to recurse 1 deep
---@param inventory Barotrauma.Inventory
---@param slots? InventorySlot[]
---@param depth? number
---@return InventorySlot[], string?
local function getMouseoverSlots(inventory, slots, depth)
slots = slots or {}
depth = depth or 0
if depth > 1 then return slots, nil end
local visualSlots = inventory.visualSlots
if not visualSlots then return slots, "Inventory has no visual slots" end
for i, visualSlot in ipairs(visualSlots) do
local item
local itemInventory
-- local err
local slot = inventory.slots[i]
if not slot then
-- MyModGlobal.debugPrint("Slot is not a valid slot")
goto continue
end
if #slot.items == 0 then
goto mouseover
end
item = slot.items[1]
if not item then
goto mouseover
end
itemInventory = item.OwnInventory
if not itemInventory then
goto mouseover
end
-- print("Before: " .. #slots)--
getMouseoverSlots(itemInventory, slots, depth + 1)
-- if err then
-- MyModGlobal.debugPrint(string.format("Error getting mouseover slots: %s", err))
-- end
-- print("After: " .. #slots)
::mouseover::
if visualSlot:MouseOn() then
local inventorySlot = MyModGlobal.InventorySlot.new(inventory, i)
slots[#slots + 1] = inventorySlot
end
::continue::
end
return slots, nil
end
---@return InventorySlot[], string?
local function getSlotsUnderCursor()
local slots = {}
-- Make sure we have a controlled character
local controlledCharacter = Character.Controlled
if not controlledCharacter then return slots, "No controlled character" end
local inventory = controlledCharacter.Inventory
if not inventory then return slots, "No inventory" end
local err
slots, err = getMouseoverSlots(inventory, slots)
if err then return slots, err end
-- Even if we don't get them we're still fine
local openContainers, _ = getOpenContainers()
-- if err then return mouseoverSlots, err end
for _, container in ipairs(openContainers) do
local containerInventories = container.OwnInventories
for containerInventory in containerInventories do
if not containerInventory or not containerInventory.visualSlots then
MyModGlobal.debugPrint("Container inventory has no visual slots")
goto continue
end
for i, visualSlot in ipairs(containerInventory.visualSlots) do
if visualSlot:MouseOn() then
local inventorySlot = MyModGlobal.InventorySlot.new(containerInventory, i)
slots[#slots + 1] = inventorySlot
end
end
::continue::
end
end
return slots, nil
end
---@return InventorySlot, string?
local function getFirstSlotUnderCursor()
local slots, err = getSlotsUnderCursor()
if err then return slots, err end
if #slots == 0 then return slots, "No slots found under cursor" end
for _, slot in ipairs(slots) do
if slot.slot.items and #slot.slot.items > 0 then
return slot
end
end
return slots[1]
end
return {
enqueueItem = enqueueItem,
enqueueSlot = enqueueSlot,
enqueueInventory = enqueueInventory,
enqueuePlayerItems = enqueuePlayerItems,
enqueueSubmarineItems = enqueueSubmarineItems,
enqueueAllOwnedItems = enqueueAllOwnedItems,
enqueueOpenContainers = enqueueOpenContainers,
getOpenContainers = getOpenContainers,
getFirstOpenContainer = getFirstOpenContainer,
getSlotsUnderCursor = getSlotsUnderCursor,
getFirstSlotUnderCursor = getFirstSlotUnderCursor,
enqueueMove = enqueueMove,
}