576 lines
19 KiB
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,
|
|
}
|