--- Inventory Abstraction Library -- Inventory Peripheral API compatible library that caches the contents of chests, and allows for very fast transfers of items between AbstractInventory objects. -- Transfers can occur from slot to slot, or by item name and nbt data. -- This can also transfer to / from normal inventories, just pass in the peripheral name. -- Use {optimal=false} to transfer to / from non-inventory peripherals. -- Now you can wrap arbratrary slot ranges -- To do so, rather than passing in the inventory name when constructing (or adding/removing inventories) -- you simply pass in a table of the following format -- {name: string, minSlot: integer?, maxSlot: integer?, slots: integer[]?} -- If slots is provided that overwrites anything in minSlot and maxSlot -- minSlot defaults to 1, and maxSlot defaults to the inventory size -- Transfers with this inventory are parallel safe iff -- * assumeLimits = true -- * The limits of the abstractInventorys involved have already been cached -- * refreshStorage() will do this -- * The transfer is to an abstractInventory, or to an un-optimized peripheral -- Copyright 2022 Mason Gulu -- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. local expect = require("cc.expect").expect local abstractInventory ---@class Item This is pulled directly from list(), or from getItemDetail(), so it may have more fields ---@field name string Name of this item ---@field nbt string|nil ---@field count number ---@class TransferOptions ---@field optimal boolean|nil Try to optimize item movements, true default ---@field allowBadTransfers boolean|nil Recover from item transfers not going as planned (probably caused by someone tampering with the inventory) ---@field autoDeepRefresh boolean|nil Whether to do a deep refresh upon a bad transfer (requires bad transfers to be allowed) ---@field itemMovedCallback nil|fun(): nil Function called anytime an item is moved ---@class CachedItem ---@field item Item|nil If an item is in this slot, this field will be an Item ---@field inventory string Inventory peripheral name ---@field slot number Slot in inventory this CachedItem represents ---@field globalSlot number Global slot of this CachedItem, spans across all wrapped inventories ---@field capacity number ---Wrap inventories and create an abstractInventory ---@param inventories table<integer,string|{name: string, minSlot: integer?, maxSlot: integer?, slots: integer[]?}> Table of inventory peripheral names to wrap ---@param assumeLimits nil|boolean Default true, assume the limit of each slot is the same, saves a TON of time ---@return AbstractInventory function abstractInventory(inventories, assumeLimits) expect(1, inventories, "table") expect(2, assumeLimits, "nil", "boolean") ---@class AbstractInventory local api = {} api.assumeLimits = assumeLimits if api.assumeLimits == nil then api.assumeLimits = true end ---@type table<string,table<string,CachedItem>> local itemNameNBTLUT = {} -- [item.name][nbt][CachedItem] -> CachedItem ---@type table<string,table<string,CachedItem>> local itemSpaceLUT = {} -- [item.name][nbt][CachedItem] -> CachedItem ---@type table<string,table<integer,CachedItem>> local inventorySlotLUT = {} -- [inventory][slot] = CachedItem ---@type table<string,integer> local inventoryLimit = {} -- [inventory] = number ---@type table<string,table<integer,boolean|nil>> local emptySlotLUT = {} -- [inventory][slot] = true|nil ---@type table<integer,{inventory:string, slot:integer}> local slotNumberLUT = {} -- [global slot] -> {inventory:string, slot:number} ---@type table<string,table<integer,integer>> local inventorySlotNumberLUT = {} -- [inventory][slot] -> global slot:number ---@type table<string,table<string,boolean>> local tagLUT = {} -- [tag] -> string[] ---@type table<string,table<string,table>> local deepItemLUT = {} -- [name][nbt] -> ItemInfo local executeLimit = 128 -- limit of functions to run in parallel ---Execute a table of functions in batches ---@param func function[] local function batchExecute(func) local batches = math.ceil(#func / executeLimit) for batch = 1, batches do local start = ((batch - 1) * executeLimit) + 1 local batch_end = math.min(start + executeLimit - 1, #func) parallel.waitForAll(table.unpack(func, start, batch_end)) end end local function ate(table, item) -- add to end table[#table + 1] = item end local function shallowClone(t) local ct = {} for k, v in pairs(t) do ct[k] = v end return ct end local function removeSlotFromEmptySlots(inventory, slot) emptySlotLUT[inventory] = emptySlotLUT[inventory] or {} emptySlotLUT[inventory][slot] = nil if not next(emptySlotLUT[inventory]) then emptySlotLUT[inventory] = nil end end ---Cache a given item, ensuring that whatever was in the slot beforehand is wiped properly ---And the caches are managed correctly. ---@param item table|nil ---@param inventory string ---@param slot number ---@return CachedItem local function cacheItem(item, inventory, slot) expect(1, item, "table", "nil") expect(2, inventory, "string") expect(3, slot, "number") local nbt = (item and item.nbt) or "NONE" if item and item.name == "" then item = nil end inventorySlotLUT[inventory] = inventorySlotLUT[inventory] or {} if inventorySlotLUT[inventory][slot] then local oldCache = inventorySlotLUT[inventory][slot] local oldItem = oldCache.item if oldItem and oldItem.name then -- There was an item in this slot before, clean up the caches local oldNBT = oldItem.nbt or "NONE" if itemNameNBTLUT[oldItem.name] and itemNameNBTLUT[oldItem.name][oldNBT] then itemNameNBTLUT[oldItem.name][oldNBT][oldCache] = nil end if itemSpaceLUT[oldItem.name] and itemSpaceLUT[oldItem.name][oldNBT] then itemSpaceLUT[oldItem.name][oldNBT][oldCache] = nil end end end removeSlotFromEmptySlots(inventory, slot) if not inventorySlotLUT[inventory][slot] then inventorySlotLUT[inventory][slot] = { item = item, inventory = inventory, slot = slot, globalSlot = inventorySlotNumberLUT[inventory][slot] } end if not inventorySlotLUT[inventory][slot].capacity then if api.assumeLimits and inventoryLimit[inventory] then inventorySlotLUT[inventory][slot].capacity = inventoryLimit[inventory] else inventorySlotLUT[inventory][slot].capacity = peripheral.call(inventory, "getItemLimit", slot) end inventoryLimit[inventory] = inventorySlotLUT[inventory][slot].capacity end ---@type CachedItem local cachedItem = inventorySlotLUT[inventory][slot] cachedItem.item = item if item and item.name then itemNameNBTLUT[item.name] = itemNameNBTLUT[item.name] or {} itemNameNBTLUT[item.name][nbt] = itemNameNBTLUT[item.name][nbt] or {} itemNameNBTLUT[item.name][nbt][cachedItem] = cachedItem if item.tags then for k, v in pairs(item.tags) do tagLUT[k] = tagLUT[k] or {} tagLUT[k][item.name] = true end end if emptySlotLUT[inventory] then -- There's an item in this slot, therefor this slot is not empty emptySlotLUT[inventory][slot] = nil end if item.count < (item.maxCount or cachedItem.capacity) then -- There's space left in this slot, add it to the cache itemSpaceLUT[item.name] = itemSpaceLUT[item.name] or {} itemSpaceLUT[item.name][nbt] = itemSpaceLUT[item.name][nbt] or {} itemSpaceLUT[item.name][nbt][cachedItem] = cachedItem end else -- There is no item in this slot, this slot is empty emptySlotLUT[inventory] = emptySlotLUT[inventory] or {} emptySlotLUT[inventory][slot] = true end return cachedItem end ---Cache what's in a given slot ---@param inventory string ---@param slot number ---@return CachedItem local function cacheSlot(inventory, slot) return cacheItem(peripheral.call(inventory, "getItemDetail", slot), inventory, slot) end ---Refresh a CachedItem ---@param item CachedItem local function refreshItem(item) cacheSlot(item.inventory, item.slot) end local function refreshInventory(inventory, deep) local deepCacheFunctions = {} local inventoryName, slots, minSlot, maxSlot if type(inventory) == "table" then inventoryName = inventory.name slots = inventory.slots minSlot = inventory.minSlot or 1 maxSlot = inventory.maxSlot or assert(peripheral.call(inventoryName, "size"), ("%s is not a valid inventory."):format(inventoryName)) else inventoryName = inventory minSlot = 1 maxSlot = assert(peripheral.call(inventoryName, "size"), ("%s is not a valid inventory."):format(inventoryName)) end if not slots then slots = {} for i = minSlot, maxSlot do slots[#slots + 1] = i end end emptySlotLUT[inventoryName] = {} for _, i in ipairs(slots) do emptySlotLUT[inventoryName][i] = true local slotnumber = #slotNumberLUT + 1 slotNumberLUT[slotnumber] = { inventory = inventoryName, slot = i } inventorySlotNumberLUT[inventoryName] = inventorySlotNumberLUT[inventoryName] or {} inventorySlotNumberLUT[inventoryName][i] = slotnumber end inventoryLimit[inventoryName] = peripheral.call(inventoryName, "getItemLimit", 1) -- this should make transfers from/to this inventory parallel safe. local listings = peripheral.call(inventoryName, "list") if not deep then for _, i in ipairs(slots) do if listings[i] then cacheItem(listings[i], inventoryName, i) end end else for _, i in ipairs(slots) do local listing = listings[i] if listing then deepCacheFunctions[#deepCacheFunctions + 1] = function() deepItemLUT[listing.name] = deepItemLUT[listing.name] or {} if deepItemLUT[listing.name][listing.nbt or "NONE"] then local item = shallowClone(deepItemLUT[listing.name][listing.nbt or "NONE"]) item.count = listing.count cacheItem(item, inventoryName, i) else local item = peripheral.call(inventoryName, "getItemDetail", i) cacheItem(item, inventoryName, i) deepItemLUT[item.name][item.nbt or "NONE"] = item end end end end end return deepCacheFunctions end ---Recache the inventory contents ---@param deep nil|boolean call getItemDetail on every slot function api.refreshStorage(deep) if type(deep) == "nil" then deep = true end itemNameNBTLUT, itemSpaceLUT, inventorySlotLUT, inventoryLimit, emptySlotLUT, slotNumberLUT, inventorySlotNumberLUT, tagLUT, deepItemLUT = {}, {}, {}, {}, {}, {}, {}, {}, {} local inventoryRefreshers = {} local deepCacheFunctions = {} for _, inventory in pairs(inventories) do table.insert(inventoryRefreshers, function() for k, v in ipairs(refreshInventory(inventory, deep) or {}) do deepCacheFunctions[#deepCacheFunctions + 1] = v end end) end batchExecute(inventoryRefreshers) batchExecute(deepCacheFunctions) end ---Get an inventory slot for a given item ---@param name string ---@param nbt nil|string ---@return nil|CachedItem local function getItem(name, nbt) nbt = nbt or "NONE" if not (itemNameNBTLUT[name] and itemNameNBTLUT[name][nbt]) then return end ---@type CachedItem local cached = next(itemNameNBTLUT[name][nbt]) return cached end ---@return string|nil inventory ---@return integer|nil slot local function getEmptySlot() local inv = next(emptySlotLUT) if not inv then return end local slot = next(emptySlotLUT[inv]) if not slot then emptySlotLUT[inv] = nil return getEmptySlot() end return inv, slot end ---Get an inventory slot that has space for a given item ---@param name string ---@param nbt nil|string ---@return nil|CachedItem local function getSlotWithSpace(name, nbt) nbt = nbt or "NONE" if not (itemSpaceLUT[name] and itemSpaceLUT[name][nbt]) then return end ---@type CachedItem local cached = next(itemSpaceLUT[name][nbt]) return cached end ---@return integer|nil slot ---@return string|nil inventory ---@return integer capacity local function getEmptySpace() local inv, freeSlot = getEmptySlot() local space if inv and freeSlot and inventorySlotLUT[inv] and inventorySlotLUT[inv][freeSlot] then space = inventorySlotLUT[inv][freeSlot].capacity else space = 64 -- maybe???? might be a bad assumption end return freeSlot, inv, space end ---@param name string ---@param nbt string|nil ---@return CachedItem|nil function api._getSlotFor(name, nbt) return getSlotWithSpace(name, nbt) end ---@return integer|nil slot ---@return string|nil inventory ---@return integer capacity function api._getEmptySpace() return getEmptySpace() end ---@param item table|nil ---@param inventory string ---@param slot integer ---@return CachedItem function api._updateItem(item, inventory, slot) return cacheItem(item, inventory, slot) end ---@return CachedItem|nil function api._getItem(name, nbt) if not (itemNameNBTLUT[name] and itemNameNBTLUT[name][nbt]) then return end return next(itemNameNBTLUT[name][nbt]) end ---@param slot integer ---@return CachedItem local function getGlobalSlot(slot) local slotInfo = slotNumberLUT[slot] inventorySlotLUT[slotInfo.inventory] = inventorySlotLUT[slotInfo.inventory] or {} if not inventorySlotLUT[slotInfo.inventory][slotInfo.slot] then cacheSlot(slotInfo.inventory, slotInfo.slot) end return inventorySlotLUT[slotInfo.inventory][slotInfo.slot] end ---@param slot integer ---@return CachedItem|nil function api._getGlobalSlot(slot) return getGlobalSlot(slot) end function api._getLookupSlot(slot) return slotNumberLUT[slot] end local defaultOptions = { optimal = true, allowBadTransfers = false, autoDeepRefresh = false, itemMovedCallback = nil, } local function pushItemsUnoptimal(targetInventory, name, amount, toSlot, nbt, options) -- This is to a normal inventory local totalMoved = 0 local rep = true while totalMoved < amount and rep do local item if type(name) == "number" then -- perform lookup item = getGlobalSlot(name) else item = getItem(name, nbt) end if not item then return totalMoved -- no items to move end local itemCount = item.item.count rep = (itemCount - totalMoved) < amount local expectedMove = math.min(amount - totalMoved, 64) local remainingItems = math.max(0, itemCount - expectedMove) item.item.count = remainingItems if item.count == 0 then cacheItem(nil, item.inventory, item.slot) else cacheItem(item.item, item.inventory, item.slot) end local amountMoved = peripheral.call(item.inventory, "pushItems", targetInventory, item.slot, amount - totalMoved, toSlot) totalMoved = totalMoved + amountMoved refreshItem(item) if options.itemMovedCallback then options.itemMovedCallback() end if amountMoved < itemCount then return totalMoved -- target slot full end end return totalMoved end local function pushItemsOptimal(targetInventory, name, amount, toSlot, nbt, options) if type(targetInventory) == "string" then -- We'll see if this is a good optimization or not targetInventory = abstractInventory({ targetInventory }) targetInventory.refreshStorage() end local theoreticalAmountMoved = 0 local actualAmountMoved = 0 local transferCache = {} local totalTime = 0 local badTransfer while theoreticalAmountMoved < amount do local t0 = os.clock() -- find the cachedItem item in self ---@type CachedItem|nil local cachedItem if type(name) == "number" then cachedItem = getGlobalSlot(name) if not (cachedItem and cachedItem.item) then -- this slot is empty break end else cachedItem = getItem(name, nbt) if not (cachedItem and cachedItem.item) then -- no slots with this item break end end -- check how many items there are available to move local itemsToMove = cachedItem.item.count -- ask the other inventory for a slot with space local destinationInfo if toSlot then destinationInfo = targetInventory._getGlobalSlot(toSlot) if not destinationInfo then local info = targetInventory._getLookupSlot(toSlot) destinationInfo = cacheItem(nil, info.inventory, info.slot) end else destinationInfo = targetInventory._getSlotFor(cachedItem.item.name, nbt) if not destinationInfo then local slot, inventory, capacity = targetInventory._getEmptySpace() if not (slot and inventory) then break end destinationInfo = targetInventory._updateItem(nil, inventory, slot) end end -- determine the amount of items that should get moved local slotCapacity = cachedItem.item.maxCount or destinationInfo.capacity if destinationInfo.item then slotCapacity = slotCapacity - destinationInfo.item.count end itemsToMove = math.min(itemsToMove, slotCapacity, amount - theoreticalAmountMoved) if destinationInfo.item and (destinationInfo.item.name ~= cachedItem.item.name) then itemsToMove = 0 end if itemsToMove == 0 then break end -- queue a transfer of that item local fromInv, toInv, fromSlot, limit, slot = cachedItem.inventory, destinationInfo.inventory, cachedItem.slot, itemsToMove, destinationInfo.slot if limit ~= 0 then ate(transferCache, function() local itemsMoved = peripheral.call(fromInv, "pushItems", toInv, fromSlot, limit, slot) if options.itemMovedCallback then options.itemMovedCallback() end actualAmountMoved = actualAmountMoved + itemsMoved if not options.allowBadTransfers then assert(itemsToMove == itemsMoved, ("Expected to move %u items, moved %u"):format(itemsToMove, itemsMoved)) elseif not itemsToMove == itemsMoved then badTransfer = true end end) end -- update our cache of that item to include the predicted transfer local updatedItem = shallowClone(cachedItem.item) updatedItem.count = updatedItem.count - itemsToMove -- update the other inventory's cache to include the predicted transfer if not destinationInfo.item then destinationInfo.item = shallowClone(cachedItem.item) destinationInfo.item.count = 0 end destinationInfo.item.count = destinationInfo.item.count + itemsToMove if updatedItem.count == 0 then cacheItem(nil, cachedItem.inventory, cachedItem.slot) else cacheItem(updatedItem, cachedItem.inventory, cachedItem.slot) end targetInventory._updateItem(destinationInfo.item, destinationInfo.inventory, destinationInfo.slot) --- Timing stuff local dt = os.clock() - t0 totalTime = totalTime + dt theoreticalAmountMoved = theoreticalAmountMoved + itemsToMove end -- execute the inventory transfers -- return amount of items moved batchExecute(transferCache) if badTransfer then -- refresh inventories api.refreshStorage(options.autoDeepRefresh) targetInventory.refreshStorage(options.autoDeepRefresh) end return actualAmountMoved end --[[ .########..##.....##..######..##.....## .##.....##.##.....##.##....##.##.....## .##.....##.##.....##.##.......##.....## .########..##.....##..######..######### .##........##.....##.......##.##.....## .##........##.....##.##....##.##.....## .##.........#######...######..##.....## ]] ---Push items to an inventory ---@param targetInventory string|AbstractInventory ---@param name string|number ---@param amount nil|number ---@param toSlot nil|number ---@param nbt nil|string ---@param options nil|TransferOptions ---@return integer count function api.pushItems(targetInventory, name, amount, toSlot, nbt, options) expect(1, targetInventory, "string", "table") expect(2, name, "string", "number") expect(3, amount, "nil", "number") expect(4, toSlot, "nil", "number") expect(5, nbt, "nil", "string") expect(6, options, "nil", "table") amount = amount or 64 options = options or defaultOptions for k, v in pairs(defaultOptions) do if options[k] == nil then options[k] = v end end if type(targetInventory) == "string" then local test = peripheral.wrap(targetInventory) if not (test and test.size) then options.optimal = false end end if type(targetInventory) == "string" and not options.optimal then return pushItemsUnoptimal(targetInventory, name, amount, toSlot, nbt, options) else return pushItemsOptimal(targetInventory, name, amount, toSlot, nbt, options) end error("Invalid targetInventory") end --[[ .########..##.....##.##.......##...... .##.....##.##.....##.##.......##...... .##.....##.##.....##.##.......##...... .########..##.....##.##.......##...... .##........##.....##.##.......##...... .##........##.....##.##.......##...... .##.........#######..########.######## ]] ---Pull items from an inventory ---@param fromInventory string|AbstractInventory ---@param fromSlot string|number ---@param amount nil|number ---@param toSlot nil|number ---@param nbt nil|string ---@param options nil|TransferOptions ---@return integer count function api.pullItems(fromInventory, fromSlot, amount, toSlot, nbt, options) expect(1, fromInventory, "table", "string") expect(2, fromSlot, "number", "string") expect(3, amount, "nil", "number") expect(4, toSlot, "nil", "number") expect(5, nbt, "nil", "string") expect(6, options, "nil", "table") options = options or defaultOptions for k, v in pairs(defaultOptions) do if options[k] == nil then options[k] = v end end local rep, itemsPulled = false, 0 amount = amount or 64 nbt = nbt or "NONE" if type(fromInventory) == "string" then local test = peripheral.wrap(fromInventory) if not (test and test.size) then options.optimal = false end end if options.optimal == nil then options.optimal = true end if type(fromInventory) == "string" and not options.optimal then assert(type(fromSlot) == "number", "Must pull from a slot #") while itemsPulled < amount do local freeSlot, freeInventory, space freeSlot, freeInventory, space = getEmptySpace() if not (freeSlot and freeInventory) then return itemsPulled end local limit = math.min(amount - itemsPulled, space) cacheItem({ name = "UNKNOWN", count = math.huge }, freeInventory, freeSlot) local moved = peripheral.call(freeInventory, "pullItems", fromInventory, fromSlot, limit, freeSlot) cacheSlot(freeInventory, freeSlot) if options.itemMovedCallback then options.itemMovedCallback() end itemsPulled = itemsPulled + moved if moved < limit then -- there's no more items to pull return itemsPulled end end return itemsPulled else local theoreticalAmountMoved = 0 local actualAmountMoved = 0 local transferCache = {} local badTransfer while theoreticalAmountMoved < amount do if type(fromInventory) == "string" then fromInventory = abstractInventory({ fromInventory }) fromInventory.refreshStorage() end -- find the cachedItem item in fromInventory ---@type CachedItem|nil local cachedItem if type(fromSlot) == "number" then cachedItem = fromInventory._getGlobalSlot(fromSlot) if not (cachedItem and cachedItem.item) then -- this slot is empty break end else cachedItem = fromInventory._getItem(fromSlot, nbt) if not (cachedItem and cachedItem.item) then -- no slots with this item break end end -- check how many items there are available to move local itemsToMove = cachedItem.item.count -- find where the item will be put local destinationInfo if toSlot then destinationInfo = getGlobalSlot(toSlot) if not destinationInfo then local info = slotNumberLUT[toSlot] destinationInfo = cacheItem(nil, info.inventory, info.slot) end else destinationInfo = getSlotWithSpace(cachedItem.item.name, nbt) if not destinationInfo then local slot, inventory, capacity = getEmptySpace() if not (slot and inventory) then break end destinationInfo = cacheItem(nil, inventory, slot) end end local slotCapacity = cachedItem.item.maxCount or destinationInfo.capacity or 64 if destinationInfo.item then slotCapacity = slotCapacity - destinationInfo.item.count end itemsToMove = math.min(itemsToMove, slotCapacity, amount - theoreticalAmountMoved) if destinationInfo.item and (destinationInfo.item.name ~= cachedItem.item.name) then itemsToMove = 0 end if itemsToMove == 0 then break end -- queue a transfer of that item local toInv, fromInv, fslot, limit, tslot = destinationInfo.inventory, cachedItem.inventory, cachedItem.slot, itemsToMove, destinationInfo.slot if limit ~= 0 then ate(transferCache, function() local itemsMoved = peripheral.call(toInv, "pullItems", fromInv, fslot, limit, tslot) if options.itemMovedCallback then options.itemMovedCallback() end actualAmountMoved = actualAmountMoved + itemsMoved if not options.allowBadTransfers then assert(itemsToMove == itemsMoved, ("Expected to move %u items, moved %u"):format(itemsToMove, itemsMoved)) elseif not itemsToMove == itemsMoved then badTransfer = true end end) end theoreticalAmountMoved = theoreticalAmountMoved + itemsToMove -- update our cache to include the predicted transfer if not destinationInfo.item then destinationInfo.item = shallowClone(cachedItem.item) destinationInfo.item.count = 0 end destinationInfo.item.count = destinationInfo.item.count + itemsToMove cacheItem(destinationInfo.item, destinationInfo.inventory, destinationInfo.slot) -- update the other inventory's cache of that item to include the predicted transfer local updatedItem = shallowClone(cachedItem.item) updatedItem.count = updatedItem.count - itemsToMove if updatedItem.count == 0 then fromInventory._updateItem(nil, cachedItem.inventory, cachedItem.slot) else fromInventory._updateItem(updatedItem, cachedItem.inventory, cachedItem.slot) end end batchExecute(transferCache) if badTransfer then -- refresh inventories api.refreshStorage(options.autoDeepRefresh) fromInventory.refreshStorage(options.autoDeepRefresh) end return actualAmountMoved end error("Invalid inventory") end ---Get the amount of this item in storage ---@param item string ---@param nbt nil|string ---@return integer function api.getCount(item, nbt) expect(1, item, "string") expect(2, nbt, "nil", "string") nbt = nbt or "NONE" if not (itemNameNBTLUT[item] and itemNameNBTLUT[item][nbt]) then return 0 end local totalCount = 0 for k, v in pairs(itemNameNBTLUT[item][nbt]) do totalCount = totalCount + v.item.count end return totalCount end ---Get a list of all items in this storage ---@return table list CachedItem[] function api.listItems() local t = {} for name, nbtt in pairs(itemNameNBTLUT) do for nbt, cachedItem in pairs(nbtt) do ate(t, cachedItem) end end return t end ---Get a list of all item names in this storage ---@return table function api.listNames() local t = {} for k, v in pairs(itemNameNBTLUT) do t[#t + 1] = k end return t end ---Get a list of all item NBT hashes in this storage ---@param name string ---@return table function api.listNBT(name) local t = {} for k, v in pairs(itemNameNBTLUT[name] or {}) do t[#t + 1] = k end return t end ---Rearrange items to make the most efficient use of space function api.defrag() local f = {} for _, name in pairs(api.listNames()) do local nbt = "NONE" table.insert(f, function() local optimal = false while not optimal do local item = getSlotWithSpace(name, nbt) if not item then optimal = true break end local count = item.item.count cacheItem(nil, item.inventory, item.slot) while count > 0 do local toItem = getSlotWithSpace(name, nbt) if toItem then local toMove = math.min(count, (toItem.item.maxCount or toItem.capacity) - toItem.item.count) toItem.item.count = toItem.item.count + toMove cacheItem(toItem, toItem.inventory, toItem.slot) peripheral.call(item.inventory, "pushItems", toItem.inventory, item.slot, toMove, toItem.slot) refreshItem(item) refreshItem(toItem) count = count - toMove else optimal = true break end end end end) end batchExecute(f) api.refreshStorage(true) -- this messes with the cache in some way I currently cannot figure out. end ---Get a CachedItem by name/nbt ---@param name string ---@param nbt nil|string ---@return CachedItem|nil function api.getItem(name, nbt) expect(1, name, "string") expect(2, nbt, "nil", "string") return getItem(name, nbt) -- this can be nil end ---Get a CachedItem by slot ---@param slot integer ---@return CachedItem function api.getSlot(slot) expect(1, slot, "number") return getGlobalSlot(slot) end ---Change the max number of functions to run in parallel ---@param n integer function api.setBatchLimit(n) expect(1, n, "number") assert(n > 0, "Attempt to set negative/0 batch limit.") if n < 10 then print(string.format("Warning, setting a very low batch limit (%u), abstractInvLib will be very slow."):format(n)) end if n > 250 then error(string.format("Attempt to set batch limit to %u, the event queue is 256 elements long. This is very likely to result in dropped events." , n), 2) end if n > 150 then print(string.format("Warning, the event queue is 256 elements long and the batch limit is %u. It is possible that you may have dropped events" , n)) end executeLimit = n end ---Get an inventory peripheral compatible list of items in this storage ---@return table function api.list() local t = {} for itemName, nbtTable in pairs(itemNameNBTLUT) do for nbt, cachedItems in pairs(nbtTable) do for item, _ in pairs(cachedItems) do t[inventorySlotNumberLUT[item.inventory][item.slot]] = item.item end end end return t end ---Get a list of item name indexed counts of each item ---@return table function api.listItemAmounts() local t = {} for _, itemName in ipairs(api.listNames()) do t[itemName] = 0 for _, nbt in ipairs(api.listNBT(itemName)) do t[itemName] = t[itemName] + api.getCount(itemName, nbt) end end return t end ---Get a list of items with the given tag ---@return string[] function api.getTag(tag) local t = {} for k, v in pairs(tagLUT[tag] or {}) do table.insert(t, k) end return t end ---Get the slot usage of this inventory ---@return {free: integer, used:integer, total:integer} function api.getUsage() local ret = {} ret.total = api.size() ret.used = 0 for i, _ in pairs(api.list()) do ret.used = ret.used + 1 end ret.free = ret.total - ret.used return ret end ---Get the amount of slots in this inventory ---@return integer function api.size() return #slotNumberLUT end ---Get item information from a slot ---@param slot integer ---@return Item function api.getItemDetail(slot) expect(1, slot, "number") local item = getGlobalSlot(slot) if item.item == nil then refreshItem(item) end return item.item end ---Get maximum number of items that can be in a slot ---@param slot integer ---@return integer function api.getItemLimit(slot) expect(1, slot, "number") local item = getGlobalSlot(slot) return item.limit end ---pull all items from an inventory ---@param inventory string|AbstractInventory ---@return integer moved total items moved function api.pullAll(inventory) if type(inventory) == "string" then inventory = abstractInventory({ inventory }) inventory.refreshStorage() end local moved = 0 for k, _ in pairs(inventory.list()) do moved = moved + api.pullItems(inventory, k) end return moved end local function getItemIndex(t, item) for k, v in ipairs(t) do if v == item then return k end end end ---Add an inventory to the storage object ---@param inventory string ---@return boolean success function api.addInventory(inventory) expect(1, inventory, "table") if getItemIndex(inventories, inventory) then return false end table.insert(inventories, inventory) api.refreshStorage(true) return true end ---Remove an inventory from the storage object ---@param inventory string ---@return boolean success function api.removeInventory(inventory) expect(1, inventory, "string") local index = getItemIndex(inventories, inventory) if not index then return false end table.remove(inventories, index) api.refreshStorage(true) return true end ---Get the number of free slots in this inventory ---@return integer function api.freeSpace() local count = 0 for _, inventorySlots in pairs(emptySlotLUT) do for _, _ in pairs(inventorySlots) do count = count + 1 end end return count end ---Get the number of items of this type you could store in this inventory ---@param name string ---@param nbt string|nil ---@return integer count function api.totalSpaceForItem(name, nbt) expect(1, name, "string") expect(2, nbt, "string", "nil") local count = 0 for inventory, inventorySlots in pairs(emptySlotLUT) do for _, _ in pairs(inventorySlots) do count = count + (inventoryLimit[inventory] or 64) end end nbt = nbt or "NONE" if itemSpaceLUT[name] and itemSpaceLUT[name][nbt] then for _, cached in pairs(itemSpaceLUT[name][nbt]) do count = count + (cached.capacity - cached.item.count) end end return count end api.refreshStorage(true) return api end return abstractInventory