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