Files
barotrauma-localmods/CykaQuick/Lua/Cyka/utils.lua

576 lines
19 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: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,
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 predicate? fun(slot: InventorySlot): boolean
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,
-- hash = function(self)
-- return string.format("%s:%d:%d", tostring(self.inventory), self.slotIndex1, self.slotIndex0)
-- end
}
---@class ItemMoveRequest
---@field A InventorySlot
---@field B InventorySlot
---@field allowSwap boolean
---@field allowCombine boolean
local enqueueMove
do
-- A bit of cheeky scoping
local enabled = true
---@type ItemMoveRequest[]
local itemMoveQueue = {}
local rate = 10
local function processQueue()
MyModGlobal.debugPrint("Processing queue")
Timer.Wait(processQueue, rate)
if not enabled then return end
if #itemMoveQueue == 0 then return end
---@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.B.inventory.TryPutItem(moveRequest.A.item, moveRequest.B.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.A),
tostring(moveRequest.B)))
end
end
processQueue()
---@param A InventorySlot
---@param B InventorySlot
---@param allowSwap boolean
---@param allowCombine boolean
enqueueMove = function(A, B, allowSwap, allowCombine)
MyModGlobal.debugPrint(string.format("Enqueuing move from %s to %s", tostring(A), tostring(B)))
table.insert(itemMoveQueue, {
A = A,
B = B,
allowSwap = allowSwap,
allowCombine = allowCombine,
})
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 or false
options.itemRef = options.itemRef or nil
options.recurse = options.recurse or 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.itemQueue, "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.itemQueue, "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
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 true
end
options.slotPredicate = function(slot, itemRef)
if not slot then return false end
if itemRef.slotIndex1 and relevantPlayerInventorySlots[itemRef.slotIndex1] then
return true
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,
}