Files

696 lines
24 KiB
Lua

-- luacheck: globals Character MyModGlobal Timer _
-- luacheck: max line length 420
---@class Barotrauma.Inventory.ItemSlot
---@field items Barotrauma.Item[]
---@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
---@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.item 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,
}