-- Inventory Manager: Touch UI on monitor -- Main computer (networked). Computer 1 sits next to dropper_0 and auto-dispenses. -- -- Modular architecture: -- manager/config.lua — configuration constants & data tables -- manager/state.lua — shared mutable state, cache, flags -- manager/operations.lua — peripheral helpers & all inventory operations -- manager/display.lua — dashboard rendering & touch handlers -- inventoryManager.lua — this file: orchestrator, main(), network handler ------------------------------------------------- -- Resolve base directory for portable path resolution ------------------------------------------------- local _baseDir = fs.getDir(shell.getRunningProgram()) local function _path(rel) return fs.combine(_baseDir, rel) end -- Persistent config path: survives Opus package updates by storing -- user data in usr/config/inventory-manager/ instead of the package dir. local _PERSIST_DIR = "usr/config/inventory-manager" local function _configPath(rel) if fs.isDir(_PERSIST_DIR) or fs.isDir("packages/inventory-manager") then if not fs.isDir(_PERSIST_DIR) then fs.makeDir(_PERSIST_DIR) end return fs.combine(_PERSIST_DIR, rel) end return _path(rel) end -- Write crash info to a file so we can always read it local function _crashLog(err) local f = fs.open(_path(".crash.log"), "w") if f then f.write(tostring(err) .. "\n" .. (debug and debug.traceback and debug.traceback() or "")) f.close() end end local ok, err = xpcall(function() -- Override dofile to load modules into our _ENV so they inherit -- Opus's require/package (CC:Tweaked dofile uses _G instead). local _ccDofile = dofile local function dofile(path) -- luacheck: ignore local fn, err = loadfile(path, nil, _ENV) if fn then return fn() else error(err, 2) end end ------------------------------------------------- -- Structured logging & shared UI helpers ------------------------------------------------- local log = dofile(_path("lib/log.lua")) local ui = dofile(_path("lib/ui.lua")) local itemDB = dofile(_path("lib/itemDB.lua")) itemDB.init(_configPath(".item_names.db")) ------------------------------------------------- -- Load modules (factory pattern → shared context) ------------------------------------------------- local cfg = dofile(_path("manager/config.lua"))(log, _path) local state = dofile(_path("manager/state.lua"))() -- Shared context table (Lua tables are by-reference, so all -- modules see the same mutable cache/activity/etc) local ctx = { log = log, ui = ui, cfg = cfg, state = state, -- Filled during init: networkModem = nil, networkModemName = nil, craftTurtleName = nil, } local ops = dofile(_path("manager/operations.lua"))(ctx) ctx.ops = ops local display = dofile(_path("manager/display.lua"))(ctx) ctx.display = display -- Recursive crafting engine local craftEngine = dofile(_path("lib/craft.lua")) craftEngine.init(cfg.recipeBook, ops.getItemTotal) ctx.craftEngine = craftEngine ctx.itemDB = itemDB -- Convenience aliases local cache = state.cache local activity = state.activity ------------------------------------------------- -- Idempotent command tracking ------------------------------------------------- local processedCmdIds = {} local CMD_TTL_MS = 300000 -- 5 minutes local function isCommandDuplicate(commandId) if not commandId then return false end local entry = processedCmdIds[commandId] return entry ~= nil and (os.epoch("utc") - entry) < CMD_TTL_MS end local function recordCommandId(commandId) if not commandId then return end processedCmdIds[commandId] = os.epoch("utc") end local function cleanupCommandIds() local cutoff = os.epoch("utc") - CMD_TTL_MS for id, ts in pairs(processedCmdIds) do if ts < cutoff then processedCmdIds[id] = nil end end end ------------------------------------------------- -- Network broadcast (sends state to client displays) ------------------------------------------------- local function broadcastState() if not ctx.networkModem then return end state.ensureItemList() local payload = { type = "state", stateVersion = state.stateVersion, cache = { itemList = cache.itemList, grandTotal = cache.grandTotal, chestCount = cache.chestCount, totalSlots = cache.totalSlots, usedSlots = cache.usedSlots, freeSlots = cache.freeSlots, usedRatio = cache.usedRatio, dropperOk = cache.dropperOk, barrelOk = cache.barrelOk, furnaceCount = cache.furnaceCount, furnaceStatus = cache.furnaceStatus, droppers = cache.droppers, }, activity = activity, alerts = state.activeAlerts, smeltingPaused = state.smeltingPaused, disabledRecipes = state.disabledRecipes, craftTurtleOk = ctx.craftTurtleName and peripheral.isPresent(ctx.craftTurtleName), } -- Keep ctx in sync so display.lua can check ctx.craftTurtleOk directly ctx.craftTurtleOk = payload.craftTurtleOk payload.smeltable = cfg.SMELTABLE payload.craftable = cfg.CRAFTABLE state.configDirty = false ctx.networkModem.transmit(cfg.BROADCAST_CHANNEL, cfg.ORDER_CHANNEL, payload) state.lastBroadcastVersion = state.stateVersion end ------------------------------------------------- -- Main ------------------------------------------------- local function main() print("=================================") print(" Inventory Manager v3 (Modular)") print("=================================") print("") -- Peripheral detection if peripheral.isPresent(cfg.DROPPER_NAME) then log.info("INIT", "Dropper: %s", cfg.DROPPER_NAME) else log.warn("INIT", "Dropper not found: %s", cfg.DROPPER_NAME) end if peripheral.isPresent(cfg.BARREL_NAME) then log.info("INIT", "Barrel: %s", cfg.BARREL_NAME) else log.warn("INIT", "Barrel not found: %s", cfg.BARREL_NAME) end -- Monitors if display.setupMonitor() then log.info("INIT", "Monitor: %s", display.monName or cfg.MONITOR_SIDE) else log.warn("INIT", "No monitor on %s", cfg.MONITOR_SIDE) end if display.setupSmelterMonitor() then log.info("INIT", "Smelter monitor: %s", display.smelterMonName or cfg.SMELTER_MONITOR_SIDE) else log.warn("INIT", "No smelter monitor on %s", cfg.SMELTER_MONITOR_SIDE) end -- Find wired modem for client/turtle communication for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == "modem" then local m = peripheral.wrap(name) -- Prefer wired modem (has getNameLocal); skip wireless if m.isWireless and not m.isWireless() then ctx.networkModem = m ctx.networkModemName = name ctx.networkModem.open(cfg.ORDER_CHANNEL) ctx.networkModem.open(cfg.CRAFT_REPLY_CHANNEL) ctx.networkModem.open(cfg.SYSTEM_CHANNEL) break elseif not ctx.networkModem then -- Fallback: use wireless if no wired available ctx.networkModem = m ctx.networkModemName = name end end end if ctx.networkModem then -- Ensure channels are open even on fallback modem ctx.networkModem.open(cfg.ORDER_CHANNEL) ctx.networkModem.open(cfg.CRAFT_REPLY_CHANNEL) ctx.networkModem.open(cfg.SYSTEM_CHANNEL) log.info("INIT", "Network modem: %s (wired=%s)", ctx.networkModemName, tostring(ctx.networkModem.isWireless and not ctx.networkModem.isWireless())) else log.warn("INIT", "No modem found for client sync") end -- Detect crafting turtle on network -- The actual craft message goes on CRAFT_CHANNEL which only the crafting -- turtle listens to, so we just need any turtle name for presence checks. for _, name in ipairs(peripheral.getNames()) do if name:match("^turtle_") then ctx.craftTurtleName = name break end end if ctx.craftTurtleName then log.info("INIT", "Crafting turtle: %s", ctx.craftTurtleName) else log.warn("INIT", "No crafting turtle found") end -- Load recipe toggles from disk ops.loadDisabledRecipes() if state.smeltingPaused then log.info("INIT", "Smelting is PAUSED (toggle on smelter monitor)") end local enabledCount = 0 local totalRecipeCount = 0 for _ in pairs(cfg.SMELTABLE) do totalRecipeCount = totalRecipeCount + 1 end for k in pairs(cfg.SMELTABLE) do if not state.disabledRecipes[k] then enabledCount = enabledCount + 1 end end log.info("INIT", "%d/%d smelting recipes enabled", enabledCount, totalRecipeCount) do local cc, sc = cfg.recipeBook.count() log.info("INIT", "Recipe book: %d crafting, %d smelting", cc, sc) end -- Compost peripherals if peripheral.isPresent(cfg.COMPOST_DROPPER) then log.info("INIT", "Compost dropper: %s", cfg.COMPOST_DROPPER) else log.warn("INIT", "Compost dropper not found: %s", cfg.COMPOST_DROPPER) end if peripheral.isPresent(cfg.COMPOST_HOPPER) then log.info("INIT", "Compost hopper: %s", cfg.COMPOST_HOPPER) else log.warn("INIT", "Compost hopper not found: %s", cfg.COMPOST_HOPPER) end log.info("INIT", "Tracking %d low-stock alerts", #cfg.LOW_STOCK_ALERTS) print("") print("Console shows log. Use the monitors to interact.") print("") -- Try loading cached inventory from disk for instant startup local cacheLoaded = ops.loadCacheFromDisk() if cacheLoaded then log.info("INIT", "Loaded cached inventory (%d types)", #cache.itemList) log.info("INIT", "Background refresh starting...") else -- No cache: do full scan with boot progress bar log.info("INIT", "No cache found. Scanning inventories...") if display.mon then local w, h = display.mon.getSize() local buf = window.create(display.mon, 1, 1, w, h, false) local function drawBoot(current, total, chestName) buf.setBackgroundColor(colors.black) buf.clear() buf.setBackgroundColor(colors.blue) buf.setCursorPos(1, 1) buf.write(string.rep(" ", w)) local title = " INVENTORY MANAGER " buf.setCursorPos(math.floor((w - #title) / 2) + 1, 1) buf.setTextColor(colors.white) buf.write(title) local midY = math.floor(h / 2) buf.setBackgroundColor(colors.black) buf.setTextColor(colors.lightGray) local label = "Scanning inventories..." buf.setCursorPos(math.floor((w - #label) / 2) + 1, midY - 2) buf.write(label) local short = chestName or "" if #short > w - 4 then short = ".." .. short:sub(-(w - 6)) end buf.setTextColor(colors.gray) buf.setCursorPos(math.floor((w - #short) / 2) + 1, midY - 1) buf.write(short) local barW = math.min(w - 8, 40) local barX = math.floor((w - barW) / 2) + 1 local ratio = total > 0 and (current / total) or 0 local filled = math.floor(ratio * barW) buf.setCursorPos(barX, midY + 1) buf.setBackgroundColor(colors.lime) buf.write(string.rep(" ", filled)) buf.setBackgroundColor(colors.gray) buf.write(string.rep(" ", barW - filled)) buf.setBackgroundColor(colors.black) buf.setTextColor(colors.white) local pct = string.format("%d/%d (%d%%)", current, total, math.floor(ratio * 100)) buf.setCursorPos(math.floor((w - #pct) / 2) + 1, midY + 3) buf.write(pct) buf.setCursorPos(1, h) buf.setBackgroundColor(colors.blue) buf.write(string.rep(" ", w)) buf.setVisible(true) buf.setVisible(false) end ops.refreshCache(drawBoot) -- Destroy boot window so it doesn't intercept future monitor writes buf.setVisible(true) buf.clear() buf = nil else ops.refreshCache() end log.info("INIT", "Done. Found %d item types.", #cache.itemList) end print("") ----------------------------------------------- -- Resilient task wrapper: if a task crashes, it -- restarts after a brief delay instead of killing -- all other parallel tasks. ----------------------------------------------- local function resilient(name, fn) return function() while true do local ok, err = pcall(fn) if not ok then log.error("TASK", "%s crashed: %s", name, tostring(err)) sleep(5) log.info("TASK", "%s restarting...", name) else -- Task returned normally (shouldn't happen) log.warn("TASK", "%s exited unexpectedly, restarting...", name) sleep(1) end end end end ----------------------------------------------- -- Parallel tasks ----------------------------------------------- parallel.waitForAny( -- Task 1: Background inventory scanner resilient("Scanner", function() if cacheLoaded then pcall(ops.refreshCache) pcall(ops.checkAlerts) state.needsRedraw = true state.smelterNeedsRedraw = true log.info("INIT", "Background refresh complete. %d types.", #cache.itemList) end while true do sleep(cfg.SCAN_INTERVAL) pcall(ops.refreshCache) pcall(ops.checkAlerts) pcall(function() itemDB.flush() end) pcall(function() cfg.recipeBook.flush() end) state.needsRedraw = true state.smelterNeedsRedraw = true end end), -- Task 2: Barrel auto-sort resilient("Barrel-sort", function() while true do pcall(ops.sortBarrel) sleep(cfg.POLL_INTERVAL) end end), -- Task 3: Auto-smelt resilient("Auto-smelt", function() while true do local ok, didWork = pcall(ops.autoSmelt) if ok and didWork then activity.smelting = true state.needsRedraw = true state.smelterNeedsRedraw = true end activity.smelting = false pcall(ops.refreshFurnaceStatus) state.needsRedraw = true state.smelterNeedsRedraw = true sleep(cfg.SMELT_INTERVAL) end end), -- Task 4: Defrag (consolidate partial stacks) resilient("Defrag", function() sleep(10) while true do activity.defragging = true state.needsRedraw = true pcall(ops.defragInventory) activity.defragging = false state.needsRedraw = true sleep(cfg.DEFRAG_INTERVAL) end end), -- Task 5: Auto-compost resilient("Auto-compost", function() while true do activity.composting = true state.needsRedraw = true pcall(ops.autoCompost) activity.composting = false state.needsRedraw = true pcall(ops.checkAlerts) sleep(cfg.COMPOST_INTERVAL) end end), -- Task 5b: Auto-discard excess stock resilient("Auto-discard", function() if #cfg.TRASH_DROPPERS == 0 then while true do sleep(3600) end end sleep(8) -- let initial scan finish first log.info("DISCARD", "Auto-discard active with %d trash dropper(s)", #cfg.TRASH_DROPPERS) while true do activity.discarding = true state.needsRedraw = true pcall(ops.discardExcess) activity.discarding = false state.needsRedraw = true sleep(cfg.DISCARD_INTERVAL) end end), -- Task 5c: Auto-craft excess items into target products resilient("Auto-craft", function() sleep(12) -- let initial scan + discard settle first log.info("AUTOCRAFT", "Auto-craft active (%d explicit rule(s), smart=%s)", #cfg.AUTO_CRAFT_RULES, tostring(cfg.AUTO_CRAFT_FROM_EXCESS)) while true do if ctx.craftTurtleName then activity.autocrafting = true state.needsRedraw = true pcall(ops.autoCraft) activity.autocrafting = false state.needsRedraw = true end sleep(cfg.AUTO_CRAFT_INTERVAL) end end), -- Task 5d: Collection hopper emptying (egg spawner, etc.) resilient("Hopper-collect", function() if #cfg.COLLECTION_HOPPERS == 0 then while true do sleep(3600) end end sleep(3) log.info("COLLECT", "Hopper collection active for %d hopper(s)", #cfg.COLLECTION_HOPPERS) while true do pcall(ops.collectHoppers) sleep(cfg.COLLECTION_INTERVAL) end end), -- Task 6: Low-stock alert checker resilient("Alert-checker", function() sleep(5) pcall(ops.checkAlerts) state.needsRedraw = true while true do sleep(cfg.ALERT_INTERVAL) pcall(ops.checkAlerts) state.needsRedraw = true end end), -- Task 7: Main dashboard redraw (event-driven, polls 0.1s) resilient("Dashboard", function() state.needsRedraw = true while true do if state.needsRedraw then state.needsRedraw = false local dok, derr = pcall(display.drawDashboard) if not dok then log.error("DRAW", "Dashboard: %s", tostring(derr)) end end if state.statusTimer > 0 then state.statusTimer = state.statusTimer - 0.1 if state.statusTimer <= 0 then state.statusTimer = 0 state.needsRedraw = true end end if #state.activeAlerts > 0 then state.needsRedraw = true end sleep(0.1) end end), -- Task 8: Smelter dashboard redraw resilient("Smelter-dashboard", function() state.smelterNeedsRedraw = true while true do if state.smelterNeedsRedraw then state.smelterNeedsRedraw = false local sok, serr = pcall(display.drawSmelterDashboard) if not sok then log.error("DRAW", "Smelter: %s", tostring(serr)) end end sleep(0.1) end end), -- Task 9: Touch event listener (both monitors) resilient("Touch-listener", function() while true do local event, side, x, y = os.pullEvent("monitor_touch") if display.smelterMonName and side == display.smelterMonName then log.debug("TOUCH", "x=%d y=%d", x, y) display.handleSmelterTouch(x, y) else log.debug("TOUCH", "x=%d y=%d", x, y) display.handleTouch(x, y) end end end), -- Task 10: Network state broadcast (skips if nothing changed) resilient("Broadcast", function() while true do if state.stateVersion ~= state.lastBroadcastVersion then pcall(broadcastState) end sleep(cfg.BROADCAST_INTERVAL) end end), -- Task 11: Peripheral detach handler resilient("Detach-handler", function() while true do local event, name = os.pullEvent("peripheral_detach") if name then ops.invalidateWrapCache(name) ops.invalidatePeripheralCaches() log.info("DETACH", "%s", name) if name == ctx.craftTurtleName then ctx.craftTurtleName = nil log.warn("DETACH", "Crafting turtle disconnected") end end end end), -- Task 11b: Peripheral attach handler (auto-detect crafting turtle) resilient("Attach-handler", function() while true do local event, name = os.pullEvent("peripheral_attach") if name and name:match("^turtle_") and not ctx.craftTurtleName then ctx.craftTurtleName = name log.info("ATTACH", "Crafting turtle detected: %s", name) pcall(broadcastState) end end end), -- Task 12: Supply chest (builder / manifest-based stocking) resilient("Supply-chest", function() if cfg.SUPPLY_CHEST == "" or #cfg.SUPPLY_MANIFEST == 0 then while true do sleep(3600) end end log.info("SUPPLY", "Stocking %s with %d item types", cfg.SUPPLY_CHEST, #cfg.SUPPLY_MANIFEST) while true do pcall(ops.supplyChest) sleep(cfg.SUPPLY_INTERVAL) end end), -- Task 13: Network order/command listener resilient("Network-listener", function() if not ctx.networkModem then while true do sleep(3600) end end while true do local event, side, channel, replyChannel, message, distance = os.pullEvent("modem_message") if channel == cfg.ORDER_CHANNEL and type(message) == "table" then if isCommandDuplicate(message.commandId) then log.debug("NET", "Duplicate command skipped: %s", tostring(message.commandId)) else recordCommandId(message.commandId) cleanupCommandIds() local handlerOk, handlerErr = pcall(function() if message.type == "order" and message.itemName and message.amount then log.info("NET", "Order: %s x%d", message.itemName, message.amount) local pok, success = pcall(ops.orderItem, message.itemName, message.amount, message.dropperName) if not pok then log.error("NET", "orderItem crashed: %s", tostring(success)) success = false state.statusMessage = "Order error" state.statusColor = colors.red state.statusTimer = 5 state.needsRedraw = true end pcall(function() ctx.networkModem.transmit(replyChannel, cfg.ORDER_CHANNEL, { type = "order_result", commandId = message.commandId, success = success, message = state.statusMessage, color = state.statusColor, }) end) pcall(broadcastState) elseif message.type == "scan" then log.info("NET", "Scan request from client") state.configDirty = true pcall(ops.refreshCache) pcall(ops.checkAlerts) state.needsRedraw = true state.smelterNeedsRedraw = true pcall(broadcastState) elseif message.type == "toggle_pause" then state.smeltingPaused = not state.smeltingPaused log.info("NET", "Smelting %s", state.smeltingPaused and "PAUSED" or "RESUMED") ops.saveDisabledRecipes() state.bumpStateVersion() state.smelterNeedsRedraw = true state.needsRedraw = true pcall(broadcastState) elseif message.type == "toggle_recipe" and message.recipe then if state.disabledRecipes[message.recipe] then state.disabledRecipes[message.recipe] = nil else state.disabledRecipes[message.recipe] = true end log.info("NET", "Recipe toggle: %s", message.recipe) ops.saveDisabledRecipes() state.configDirty = true state.bumpStateVersion() state.smelterNeedsRedraw = true pcall(broadcastState) elseif message.type == "enable_all" then state.disabledRecipes = {} log.info("NET", "All recipes enabled") ops.saveDisabledRecipes() state.configDirty = true state.bumpStateVersion() state.smelterNeedsRedraw = true pcall(broadcastState) elseif message.type == "disable_all" then for inputName in pairs(cfg.SMELTABLE) do state.disabledRecipes[inputName] = true end log.info("NET", "All recipes disabled") ops.saveDisabledRecipes() state.configDirty = true state.bumpStateVersion() state.smelterNeedsRedraw = true pcall(broadcastState) elseif message.type == "sort_barrel" and message.barrelName then log.info("NET", "Sort barrel: %s", message.barrelName) pcall(ops.sortBarrel, message.barrelName) pcall(broadcastState) elseif message.type == "register_droppers" and message.clientId and message.droppers then local cid = tostring(message.clientId) state.clientDroppers[cid] = message.droppers log.info("NET", "Client %s registered %d dropper(s)", cid, #message.droppers) local seenNames = {} local merged = {} for _, d in ipairs(cache.droppers or {}) do if not d.clientId then table.insert(merged, d) seenNames[d.name] = true end end for clientId, clientList in pairs(state.clientDroppers) do for _, d in ipairs(clientList) do if not seenNames[d.name] then table.insert(merged, { name = d.name, isDefault = false, clientId = clientId }) seenNames[d.name] = true end end end cache.droppers = merged state.bumpStateVersion() pcall(broadcastState) elseif message.type == "reboot" then local target = message.target or "all" log.info("NET", "Reboot command received, target: %s", target) ctx.networkModem.transmit(cfg.SYSTEM_CHANNEL, cfg.ORDER_CHANNEL, { type = "reboot", target = target, }) if target == "all" or target == "manager" then log.info("NET", "Manager rebooting in 1s...") sleep(1) os.reboot() end elseif message.type == "craft" and message.recipeIdx then log.info("NET", "Craft request: recipe #%d", message.recipeIdx) local pok, ok, err = pcall(ops.craftItem, message.recipeIdx) if not pok then log.error("NET", "craftItem crashed: %s", tostring(ok)) err = tostring(ok) ok = false end pcall(function() ctx.networkModem.transmit(replyChannel, cfg.ORDER_CHANNEL, { type = "craft_result", commandId = message.commandId, success = ok, error = err, }) end) state.smelterNeedsRedraw = true state.needsRedraw = true pcall(broadcastState) elseif message.type == "recursive_craft" and message.itemName and message.count then log.info("NET", "Recursive craft: %s x%d", message.itemName, message.count) local pok, ok, craftErr = pcall(ops.recursiveCraft, message.itemName, message.count) if not pok then log.error("NET", "recursiveCraft crashed: %s", tostring(ok)) craftErr = tostring(ok) ok = false end pcall(function() ctx.networkModem.transmit(replyChannel, cfg.ORDER_CHANNEL, { type = "recursive_craft_result", commandId = message.commandId, success = ok, error = craftErr, }) end) state.smelterNeedsRedraw = true state.needsRedraw = true pcall(broadcastState) elseif message.type == "sync_disabled_recipes" then if message.disabledRecipes then state.disabledRecipes = message.disabledRecipes end if message.smeltingPaused ~= nil then state.smeltingPaused = message.smeltingPaused end ops.saveDisabledRecipes() log.info("NET", "Synced smelting state from client") state.configDirty = true state.bumpStateVersion() state.smelterNeedsRedraw = true state.needsRedraw = true pcall(broadcastState) elseif message.type == "learn_crafting_recipe" and message.output and message.count and message.grid then cfg.recipeBook.learnCraftingRecipe(message.output, message.count, message.grid) cfg.refreshRecipes() cfg.recipeBook.flush() log.info("NET", "Learned crafting recipe: %s", message.output) state.configDirty = true state.bumpStateVersion() pcall(broadcastState) elseif message.type == "learn_smelting_recipe" and message.input and message.result then cfg.recipeBook.learnSmeltingRecipe(message.input, message.result, message.furnaces) cfg.refreshRecipes() cfg.recipeBook.flush() log.info("NET", "Learned smelting recipe: %s -> %s", message.input, message.result) state.configDirty = true state.bumpStateVersion() pcall(broadcastState) elseif message.type == "forget_recipe" and message.recipe then local forgot = cfg.recipeBook.forgetCraftingRecipe(message.recipe) or cfg.recipeBook.forgetSmeltingRecipe(message.recipe) if forgot then cfg.refreshRecipes() cfg.recipeBook.flush() log.info("NET", "Forgot recipe: %s", message.recipe) state.configDirty = true state.bumpStateVersion() end pcall(broadcastState) elseif message.type == "find_item" and message.items then -- Return chest+slot locations for the first matching item -- message.items = list of item names to search (in priority order) -- message.limit = max items to return info for (default 64) local limit = message.limit or 64 local results = {} for _, itemName in ipairs(message.items) do if cache.catalogue[itemName] then for _, source in ipairs(cache.catalogue[itemName]) do local chest = ops.wrapCached(source.chest) if chest then for slot, slotItem in pairs(chest.list()) do if slotItem.name == itemName then table.insert(results, { chest = source.chest, slot = slot, name = itemName, count = slotItem.count, }) if #results >= limit then break end end end end if #results >= limit then break end end end if #results > 0 then break end -- found fuel, stop searching end log.info("NET", "find_item: found %d source(s)", #results) pcall(function() ctx.networkModem.transmit(replyChannel, cfg.ORDER_CHANNEL, { type = "find_item_result", commandId = message.commandId, results = results, }) end) end end) -- pcall handler if not handlerOk then log.error("NET", "Handler error: %s", tostring(handlerErr)) end end -- idempotency else end end end) ) end main() end, function(e) return tostring(e) .. "\n" .. debug.traceback() end) if not ok then _crashLog(err) printError(tostring(err)) print("") print("Crash log saved to: " .. _path(".crash.log")) print("") print("Press any key to exit...") os.pullEvent("char") end