Newer
Older
Kristify / src / libs / inv.lua
@Erb3 Erb3 on 22 Dec 2022 29 KB Add AbstractInvLib
--- 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.

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

  local itemNameNBTLUT = {}
  -- [item.name][nbt][CachedItem] -> CachedItem

  local itemSpaceLUT = {}
  -- [item.name][nbt][CachedItem] -> CachedItem

  local inventorySlotLUT = {}
  -- [inventory][slot] = CachedItem

  local inventoryLimit = {}
  -- [inventory] = number

  local emptySlotLUT = {}
  -- [inventory][slot] = true|nil

  local slotNumberLUT = {}
  -- [global slot] -> {inventory:string, slot:number}

  local inventorySlotNumberLUT = {}
  -- [inventory][slot] -> global slot:number


  local function ate(table, item) -- add to end
    table[#table + 1] = item
  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 validInventory = false
    for k, v in pairs(inventories) do
      if v == inventory then
        validInventory = true
        break
      end
    end
    assert(validInventory, "Attempted to cache invalid inventory")
    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
    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 emptySlotLUT[inventory] then
        -- There's an item in this slot, therefor this slot is not empty
        emptySlotLUT[inventory][slot] = nil
      end
      if item.count < 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

  ---Recache the inventory contents
  ---@param deep nil|boolean call getItemDetail on every slot
  function api.refreshStorage(deep)
    itemNameNBTLUT = {}
    emptySlotLUT = {}
    inventorySlotLUT = {}
    local deepCacheFunctions = {}
    for _, inventory in pairs(inventories) do
      emptySlotLUT[inventory] = {}
      for i = 1, peripheral.call(inventory, "size") do
        emptySlotLUT[inventory][i] = true
        local slotnumber = #slotNumberLUT + 1
        slotNumberLUT[slotnumber] = { inventory = inventory, slot = i }
        inventorySlotNumberLUT[inventory] = inventorySlotNumberLUT[inventory] or {}
        inventorySlotNumberLUT[inventory][i] = slotnumber
      end
      inventoryLimit[inventory] = peripheral.call(inventory, "getItemLimit", 1) -- this should make transfers from/to this inventory parallel safe.
      if not deep then
        for slot, item in pairs(peripheral.call(inventory, "list")) do
          cacheItem(item, inventory, slot)
        end
      else
        deepCacheFunctions[#deepCacheFunctions + 1] = function()
          for slot, _ in pairs(peripheral.call(inventory, "list")) do
            cacheSlot(inventory, slot)
          end
        end
      end
    end
    if deep then
      parallel.waitForAll(table.unpack(deepCacheFunctions))
    end
  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 inventory string
  ---@param slot integer
  ---@return integer|nil slot
  ---@return string|nil inventory
  ---@return integer|nil capacity
  ---@deprecated do not use
  local function getSpaceForItem(inventory, slot)
    local itemInfo = peripheral.call(inventory, "getItemDetail", slot)
    if itemInfo and itemInfo.name then
      local cachedItem = getSlotWithSpace(itemInfo.name, itemInfo.nbt)
      if cachedItem then
        return cachedItem.slot, cachedItem.inventory, cachedItem.capacity - cachedItem.item.count
      else
        return getEmptySpace()
      end
    else
      return getEmptySpace()
    end
  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 function shallowClone(t)
    local ct = {}
    for k, v in pairs(t) do
      ct[k] = v
    end
    return ct
  end

  local defaultOptions = {
    optimal = true,
    allowBadTransfers = false,
    autoDeepRefresh = false,
    itemMovedCallback = nil,
  }

  --[[
  .########..##.....##..######..##.....##
  .##.....##.##.....##.##....##.##.....##
  .##.....##.##.....##.##.......##.....##
  .########..##.....##..######..#########
  .##........##.....##.......##.##.....##
  .##........##.....##.##....##.##.....##
  .##.........#######...######..##.....##
  ]]
  ---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" and not options.optimal then
      -- 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 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
    else
      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 = 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
      parallel.waitForAll(table.unpack(transferCache))
      if badTransfer then
        -- refresh inventories
        api.refreshStorage(options.autoDeepRefresh)
        targetInventory.refreshStorage(options.autoDeepRefresh)
      end
      return actualAmountMoved
    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 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)
        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 = 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

      parallel.waitForAll(table.unpack(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
        if nbt == "NONE" then
          nbt = nil
        end
        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

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

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

  ---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
        print("item?")
        count = count + (cached.capacity - cached.item.count)
      end
    end
    return count
  end

  return api
end

return abstractInventory