Newer
Older
Radon / core / inventory / ScanInventory.lua
@Alyssa May Alyssa May on 1 Jul 2023 9 KB Parallelize purchase handling
local eventHook = require("util.eventHook")
local score = require("util.score")

local itemCache = {}
local nbtHashCache = {}

local partialObjectMatches
local function partialArrayMatches(partialArray, array)
    for i = 1, #partialArray do
        local found = false
        for j = 1, #array do
            if type(array[j]) == "table" then
                if partialObjectMatches(partialArray[i], array[j]) then
                    found = true
                    break
                end
            elseif partialArray[i] == array[j] then
                found = true
                break
            end
        end
        if not found then
            return false
        end
    end
    return true
end

function partialObjectMatches(partialObject, object)
    if type(object) ~= "table" then
        return false
    end
    if object[1] then
        return partialArrayMatches(partialObject, object)
    end
    for k,v in pairs(partialObject) do
        if type(v) == "table" then
            if not partialObjectMatches(v, object[k]) then
                return false
            end
        elseif object[k] == nil or object[k] ~= v then
            return false
        end
    end
    return true
end

local function predicateMatches(predicates, item, allowCached)
    if not allowCached or not item.cachedMeta then
        local meta = peripheral.call(item.inventory, "getItemDetail", item.slot)
        item.cachedMeta = meta
    end
    return partialObjectMatches(predicates, item.cachedMeta)
end

local function findMatchingProducts(products, item)
    local matchingProducts = {}
    for i = 1, #products do
        local product = products[i]
        if product.predicates and not product.predicatesString then
            product.predicatesString = textutils.serialize(product.predicates)
        end
        if item.name == product.modid then
            if product.predicates and item.nbt and nbtHashCache[item.name .. "." .. item.nbt] then
                for j = 1, #nbtHashCache[item.name .. "." .. item.nbt] do
                    if nbtHashCache[item.name .. "." .. item.nbt][j] == product.predicatesString then
                        table.insert(matchingProducts, product)
                        break
                    end
                end
            else
                if not product.predicates or predicateMatches(product.predicates, item, true) then
                    table.insert(matchingProducts, product)
                end
            end
        end
    end
    if item.nbt and not nbtHashCache[item.name .. "." .. item.nbt] then
        nbtHashCache[item.name .. "." .. item.nbt] = {}
        for i = 1, #matchingProducts do
            table.insert(nbtHashCache[item.name .. "." .. item.nbt], matchingProducts[i].predicatesString)
        end
    end
    return matchingProducts
end

local function getInventories()
    peripherals = peripheral.getNames()
    inventories = {}
    for i = 1, #peripherals do
        name = peripherals[i]
        local methods = peripheral.getMethods(name)
        local hasListItems = false
        local hasPushItems = false
        if methods then
            for j = 1, #methods do
                if methods[j] == "list" then
                    hasListItems = true
                end
                if methods[j] == "pushItems" then
                    hasPushItems = true
                end
                if hasListItems and hasPushItems then
                    break
                end
            end
        end
        if hasListItems and hasPushItems then
            table.insert(inventories, peripheral.wrap(name))
        end
    end
    return inventories
end

local function getInventoryItems(inventory, products)
    local inventoryName = peripheral.getName(inventory)
    local items = {}
    local slots = inventory.list()
    for slot, item in pairs(slots) do
        if item then
            item.inventory = inventoryName
            item.slot = slot
            table.insert(items, item)
            local matchingProducts = findMatchingProducts(products, item)
            for j = 1, #matchingProducts do
                local product = matchingProducts[j]
                if not product.newQty then
                    product.newQty = 0
                end
                product.newQty = product.newQty + item.count
            end
        end
    end
    return items
end

local function getAllInventoryItems(inventories, products)
    local items = {}
    local inventoryThreads = {}
    for i = 1, #inventories do
        local inventory = inventories[i]
        table.insert(inventoryThreads, function()
            local inventoryItems = getInventoryItems(inventory, products)
            for j = 1, #inventoryItems do
                table.insert(items, inventoryItems[j])
            end
        end)
    end
    parallel.waitForAll(unpack(inventoryThreads))
    return items
end

local function updateProductInventory(products, onInventoryRefresh)
    for i = 1, #products do
        products[i].newQty = 0
    end
    local inventories = getInventories()
    local originalProducts = products
    products = score.copyDeep(originalProducts)
    local items = getAllInventoryItems(inventories, products)
    itemCache = items
    if onInventoryRefresh then
        eventHook.execute(onInventoryRefresh, products, items)
    end

    for i = 1, #products do
        local product = products[i]
        product.predicatesString = nil
        product.quantity = product.newQty
        if product.bundle and #product.bundle > 0 then
            if not product.modid then
                product.quantity = nil
            end
            for _, bundledProduct in ipairs(product.bundle) do
                for _, searchProduct in ipairs(products) do
                    if searchProduct.address:lower() == bundledProduct.product:lower() or searchProduct.name:lower() == bundledProduct.product:lower() or (searchProduct.productId and searchProduct.productId:lower() == bundledProduct.product:lower()) then
                        local searchQty = searchProduct.newQty
                        if not searchQty then
                            searchQty = searchProduct.quantity
                        end
                        if not product.quantity then
                            product.quantity = searchQty
                        end
                        product.quantity = math.min(product.quantity, math.min(searchQty / bundledProduct.quantity))
                    end
                end
            end
        end
        product.newQty = nil
        product.__opaque = true
        originalProducts[i] = product
    end
end

local function getItemCache()
    return itemCache
end

local function findProductItemsFrom(product, quantity, items, cached)
    local sources = {}
    local remaining = quantity
    if product.predicates and not product.predicatesString then
        product.predicatesString = textutils.serialize(product.predicates)
    end
    for i = 1, #items do
        local item = items[i]
        local inventory = item.inventory
        local slot = item.slot
        local cacheHit = false
        if item.nbt and nbtHashCache[item.name .. "." .. item.nbt] then
            for j = 1, #nbtHashCache[item.name .. "." .. item.nbt] do
                if nbtHashCache[item.name .. "." .. item.nbt][j] == product.predicatesString then
                    cacheHit = true
                    break
                end
            end
        end
        if item.name == product.modid and (not cached or not product.predicates or cacheHit or (item.cachedMeta and partialObjectMatches(product.predicates, item.cachedMeta))) then
            if cached or product.predicates then
                item = peripheral.call(inventory, "getItemDetail", slot)
            end
            if item then
                if item.name ~= product.modid or (product.predicates and not partialObjectMatches(product.predicates, item)) then
                    item = nil
                else
                    item.inventory = inventory
                    item.slot = slot
                end
            end
            if item and item.count > 0 then
                local amount = math.min(item.count, remaining)
                table.insert(sources, {
                    inventory = item.inventory,
                    slot = item.slot,
                    amount = amount
                })
                remaining = remaining - amount
            end
        end
        if remaining <= 0 then
            break
        end
    end
    return sources, quantity - remaining
end

local function findProductItems(products, product, quantity, onInventoryRefresh)
    local sources = nil
    local amount = 0
    local items = getItemCache()
    sources, amount = findProductItemsFrom(product, quantity, items, true)
    if amount == 0 then
        updateProductInventory(products, onInventoryRefresh)
        items = getItemCache()
        sources, amount = findProductItemsFrom(product, quantity, items)
    end
    for i = 1, #products do
        products[i].predicatesString = nil
    end
    return sources, amount
end

function clearNbtCache()
    nbtHashCache = {}
end

return {
    updateProductInventory = updateProductInventory,
    getItemCache = getItemCache,
    findProductItems = findProductItems,
    clearNbtCache = clearNbtCache
}