-- 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, }