Task 12 (supply chest) and Task 13 (network) returned immediately when not configured, which caused parallel.waitForAny to exit and the entire program to silently stop after cache build.
715 lines
29 KiB
Lua
715 lines
29 KiB
Lua
-- 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
|
|
|
|
-- 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(_path(".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),
|
|
}
|
|
|
|
if state.configDirty then
|
|
payload.smeltable = cfg.SMELTABLE
|
|
payload.craftable = cfg.CRAFTABLE
|
|
state.configDirty = false
|
|
end
|
|
|
|
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 modem for client communication
|
|
for _, name in ipairs(peripheral.getNames()) do
|
|
if peripheral.getType(name) == "modem" then
|
|
ctx.networkModem = peripheral.wrap(name)
|
|
ctx.networkModemName = name
|
|
ctx.networkModem.open(cfg.ORDER_CHANNEL)
|
|
ctx.networkModem.open(cfg.CRAFT_REPLY_CHANNEL)
|
|
ctx.networkModem.open(cfg.SYSTEM_CHANNEL)
|
|
break
|
|
end
|
|
end
|
|
if ctx.networkModem then
|
|
log.info("INIT", "Network modem: %s", ctx.networkModemName)
|
|
else
|
|
log.warn("INIT", "No modem found for client sync")
|
|
end
|
|
|
|
-- Detect crafting turtle on network
|
|
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)
|
|
else
|
|
ops.refreshCache()
|
|
end
|
|
log.info("INIT", "Done. Found %d item types.", #cache.itemList)
|
|
end
|
|
print("")
|
|
|
|
-----------------------------------------------
|
|
-- Parallel tasks
|
|
-----------------------------------------------
|
|
|
|
parallel.waitForAny(
|
|
-- Task 1: Background inventory 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
|
|
function()
|
|
while true do
|
|
pcall(ops.sortBarrel)
|
|
sleep(cfg.POLL_INTERVAL)
|
|
end
|
|
end,
|
|
|
|
-- Task 3: 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)
|
|
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
|
|
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 6: Low-stock 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)
|
|
function()
|
|
state.needsRedraw = true
|
|
while true do
|
|
if state.needsRedraw then
|
|
state.needsRedraw = false
|
|
pcall(display.drawDashboard)
|
|
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
|
|
function()
|
|
state.smelterNeedsRedraw = true
|
|
while true do
|
|
if state.smelterNeedsRedraw then
|
|
state.smelterNeedsRedraw = false
|
|
pcall(display.drawSmelterDashboard)
|
|
end
|
|
sleep(0.1)
|
|
end
|
|
end,
|
|
|
|
-- Task 9: Touch event listener (both monitors)
|
|
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)
|
|
function()
|
|
while true do
|
|
if state.stateVersion ~= state.lastBroadcastVersion then
|
|
pcall(broadcastState)
|
|
end
|
|
sleep(cfg.BROADCAST_INTERVAL)
|
|
end
|
|
end,
|
|
|
|
-- Task 11: Peripheral 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)
|
|
end
|
|
end
|
|
end,
|
|
|
|
-- Task 12: Supply chest (builder / manifest-based stocking)
|
|
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
|
|
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 == "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)
|
|
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 |