-- ATM10 Create factory logistics test controller -- Phase 1: Stock Ticker requests and live stock display, no redstone control. local REFRESH_SECONDS = 1.0 local CONFIRM_SECONDS = 6.0 local JOB_TIMEOUT_SECONDS = 180.0 local LOG_FILE = "factory_test_log.txt" local actions = { { id = "crush_asurine", label = "64 ASURINE CRUSHEN", lockKey = "crushing", address = "crushing", item = "create:asurine", inputCount = 64, completion = { mode = "any_increase", outputs = { "alltheores:zinc_clump", "alltheores:zinc_nugget" }, }, }, { id = "strip_oak", label = "64 STRIPPED OAK", lockKey = "saw:2", address = "saw:2", item = "minecraft:oak_log", inputCount = 64, completion = { mode = "expected", output = "minecraft:stripped_oak_log", outputPerInput = 1, }, }, { id = "planks_oak", label = "64 OAK PLANKS", lockKey = "saw:2", address = "saw:2", item = "minecraft:stripped_oak_log", -- Create yields 6 planks per stripped log. 11 inputs produce 66 planks. inputCount = 11, completion = { mode = "expected", output = "minecraft:oak_planks", outputPerInput = 6, }, }, { id = "shafts", label = "64 SHAFTS", lockKey = "saw:1", address = "saw:1", item = "create:andesite_alloy", -- Create yields 6 shafts per Andesite Alloy. 11 inputs produce 66 shafts. inputCount = 11, completion = { mode = "expected", output = "create:shaft", outputPerInput = 6, }, }, } local function findPeripheral(wantedType) for _, name in ipairs(peripheral.getNames()) do local peripheralType = peripheral.getType(name) if peripheralType == wantedType then return name end end return nil end local tickerName = findPeripheral("Create_StockTicker") local monitorName = findPeripheral("monitor") if not tickerName then error("Kein Create Stock Ticker am Wired-Modem-Netz gefunden", 0) end if not monitorName then error("Kein Monitor am Wired-Modem-Netz gefunden", 0) end local monitor = peripheral.wrap(monitorName) monitor.setTextScale(0.5) local stock = {} local stockEntries = {} local stockPage = 1 local stockPageCount = 1 local stockPageButtons = {} local buttons = {} local messages = {} local jobs = {} local armedAction = nil local armedUntil = 0 local statusText = "Bereit - nur Testbetrieb, keine Redstone-Steuerung" local statusColor = colors.lime local function formatNumber(value) local text = tostring(math.floor(tonumber(value) or 0)) local sign, digits = text:match("^([%-]?)(%d+)$") if not digits then return text end local formatted = digits:reverse():gsub("(%d%d%d)", "%1."):reverse():gsub("^%.", "") return sign .. formatted end local function timeText() return textutils.formatTime(os.time(), true) end local function prettyItemName(itemId) local path = tostring(itemId):match("^[^:]+:(.+)$") or tostring(itemId) path = path:gsub("_", " ") return (path:gsub("(%a)([%w']*)", function(first, rest) return first:upper() .. rest:lower() end)) end local function appendLog(message) local line = "[" .. timeText() .. "] " .. message messages[#messages + 1] = line while #messages > 4 do table.remove(messages, 1) end local file = fs.open(LOG_FILE, "a") if file then file.writeLine(line) file.close() end end local function refreshStock() local ok, result = pcall(peripheral.call, tickerName, "stock", true) if not ok then statusText = "Stock-Ticker-Fehler: " .. tostring(result) statusColor = colors.red return false end local nextStock = {} local detailsByName = {} for _, entry in pairs(result or {}) do if type(entry) == "table" and entry.name then nextStock[entry.name] = (nextStock[entry.name] or 0) + (tonumber(entry.count) or 0) detailsByName[entry.name] = entry end end stock = nextStock local nextEntries = {} for itemId, count in pairs(nextStock) do local details = detailsByName[itemId] or {} local displayName = details.displayName if type(displayName) ~= "string" or displayName == "" then displayName = prettyItemName(itemId) end nextEntries[#nextEntries + 1] = { id = itemId, label = displayName, count = count, } end table.sort(nextEntries, function(a, b) local aLabel = a.label:lower() local bLabel = b.label:lower() if aLabel == bLabel then return a.id < b.id end return aLabel < bLabel end) stockEntries = nextEntries return true end local function jobComplete(job) if job.completion.mode == "expected" then local current = stock[job.completion.output] or 0 return current >= job.baseline[job.completion.output] + job.expectedOutput end if job.completion.mode == "any_increase" then for _, output in ipairs(job.completion.outputs) do if (stock[output] or 0) > (job.baseline[output] or 0) then return true end end end return false end local function updateJobs() for lockKey, job in pairs(jobs) do if jobComplete(job) then statusText = "FERTIG: " .. job.label statusColor = colors.lime appendLog(statusText) jobs[lockKey] = nil elseif os.clock() - job.startedAt > JOB_TIMEOUT_SECONDS and not job.timedOut then job.timedOut = true statusText = "TIMEOUT: " .. job.label .. " - Anlage pruefen" statusColor = colors.red appendLog(statusText) end end end local function writeAt(x, y, text, foreground, background) local width, height = monitor.getSize() if y < 1 or y > height or x > width then return end monitor.setCursorPos(math.max(1, x), y) monitor.setTextColor(foreground or colors.white) monitor.setBackgroundColor(background or colors.black) monitor.write(tostring(text):sub(1, math.max(0, width - x + 1))) end local function center(y, text, foreground, background) local width = monitor.getSize() local x = math.max(1, math.floor((width - #text) / 2) + 1) writeAt(x, y, text, foreground, background) end local function drawStockPage(width) local itemsPerPage = 10 stockPageCount = math.max(1, math.ceil(#stockEntries / itemsPerPage)) stockPage = math.max(1, math.min(stockPage, stockPageCount)) stockPageButtons = {} writeAt(3, 3, "<", stockPage > 1 and colors.yellow or colors.gray) center(3, "LAGERBESTAND " .. tostring(stockPage) .. "/" .. tostring(stockPageCount), colors.cyan) writeAt(width - 2, 3, ">", stockPage < stockPageCount and colors.yellow or colors.gray) stockPageButtons[#stockPageButtons + 1] = { direction = -1, x1 = 1, x2 = math.floor(width / 3), y = 3, enabled = stockPage > 1, } stockPageButtons[#stockPageButtons + 1] = { direction = 1, x1 = math.ceil(width * 2 / 3), x2 = width, y = 3, enabled = stockPage < stockPageCount, } local columnWidth = math.floor((width - 6) / 2) local leftX = 3 local rightX = leftX + columnWidth + 2 local firstIndex = (stockPage - 1) * itemsPerPage + 1 for position = 1, itemsPerPage do local entry = stockEntries[firstIndex + position - 1] if entry then local column = (position - 1) % 2 local row = 4 + math.floor((position - 1) / 2) local x = column == 0 and leftX or rightX local maxLabelLength = math.max(8, columnWidth - 14) local label = entry.label if #label > maxLabelLength then label = label:sub(1, maxLabelLength - 1) .. "~" end writeAt(x, row, label .. ": " .. formatNumber(entry.count), colors.lime) end end end local function drawButton(action, x1, y1, x2, y2) local isArmed = armedAction == action.id and os.clock() <= armedUntil local available = stock[action.item] or 0 local activeJob = jobs[action.lockKey] local enabled = available >= action.inputCount and activeJob == nil local background = colors.gray local foreground = colors.lightGray if activeJob then background = activeJob.timedOut and colors.red or colors.orange foreground = activeJob.timedOut and colors.white or colors.black elseif enabled then background = isArmed and colors.orange or colors.green foreground = isArmed and colors.black or colors.white end for y = y1, y2 do writeAt(x1, y, string.rep(" ", x2 - x1 + 1), foreground, background) end local label = action.label if activeJob then label = (activeJob.timedOut and "TIMEOUT: " or "IN ARBEIT: ") .. activeJob.label elseif isArmed then label = "NOCHMALS: " .. label elseif not enabled then label = "ZU WENIG INPUT: " .. label end local labelX = math.max(x1, math.floor((x1 + x2 - #label) / 2)) writeAt(labelX, math.floor((y1 + y2) / 2), label, foreground, background) buttons[#buttons + 1] = { action = action, x1 = x1, y1 = y1, x2 = x2, y2 = y2, enabled = enabled, activeJob = activeJob, } end local function render() local width, height = monitor.getSize() monitor.setBackgroundColor(colors.black) monitor.setTextColor(colors.white) monitor.clear() buttons = {} center(1, "CREATE FACTORY - LOGISTIKTEST", colors.cyan) center(2, "Ticker: " .. tickerName .. " | Monitor: " .. monitorName, colors.gray) drawStockPage(width) local statusRow = 10 center(statusRow, statusText, statusColor) local buttonTop = statusRow + 2 local gap = 2 local buttonWidth = math.floor((width - 6 - gap) / 2) local xLeft = 3 local xRight = xLeft + buttonWidth + gap local buttonHeight = 2 drawButton(actions[1], xLeft, buttonTop, xLeft + buttonWidth - 1, buttonTop + buttonHeight - 1) drawButton(actions[2], xRight, buttonTop, xRight + buttonWidth - 1, buttonTop + buttonHeight - 1) drawButton(actions[3], xLeft, buttonTop + 3, xLeft + buttonWidth - 1, buttonTop + 4) drawButton(actions[4], xRight, buttonTop + 3, xRight + buttonWidth - 1, buttonTop + 4) local logTitleRow = math.min(height - 4, buttonTop + 6) writeAt(2, logTitleRow, "Letzte Ereignisse:", colors.yellow) for index, message in ipairs(messages) do writeAt(2, logTitleRow + index, message, colors.lightGray) end writeAt(2, height, "Jede Bestellung muss innerhalb 6 Sekunden zweimal beruehrt werden.", colors.gray) end local function submit(action) local filter = { name = action.item, _requestCount = action.inputCount, } local baseline = {} if action.completion.mode == "expected" then baseline[action.completion.output] = stock[action.completion.output] or 0 else for _, output in ipairs(action.completion.outputs) do baseline[output] = stock[output] or 0 end end local ok, sent = pcall( peripheral.call, tickerName, "requestFiltered", action.address, filter ) if not ok then statusText = "BESTELLFEHLER: " .. tostring(sent) statusColor = colors.red appendLog(statusText) return end local amount = tonumber(sent) or 0 if amount <= 0 then statusText = "NICHT GESENDET: " .. action.label statusColor = colors.orange appendLog(statusText) refreshStock() return end local expectedOutput = 0 if action.completion.mode == "expected" then expectedOutput = amount * action.completion.outputPerInput end jobs[action.lockKey] = { label = action.label, actionId = action.id, address = action.address, inputItem = action.item, inputSent = amount, completion = action.completion, baseline = baseline, expectedOutput = expectedOutput, startedAt = os.clock(), timedOut = false, } statusText = "GESTARTET: " .. action.label .. " (" .. formatNumber(amount) .. " Input)" statusColor = colors.orange appendLog(statusText) refreshStock() end local function handleTouch(x, y) for _, pageButton in ipairs(stockPageButtons) do if y == pageButton.y and x >= pageButton.x1 and x <= pageButton.x2 then if pageButton.enabled then stockPage = stockPage + pageButton.direction statusText = "Lagerseite " .. tostring(stockPage) .. "/" .. tostring(stockPageCount) statusColor = colors.cyan end return end end for _, button in ipairs(buttons) do if x >= button.x1 and x <= button.x2 and y >= button.y1 and y <= button.y2 then if button.activeJob then statusText = "Auftrag laeuft bereits: " .. button.activeJob.label statusColor = button.activeJob.timedOut and colors.red or colors.orange return end if not button.enabled then statusText = "Zu wenig Rohstoff fuer diesen Auftrag" statusColor = colors.orange appendLog(statusText) return end local now = os.clock() if armedAction == button.action.id and now <= armedUntil then local selected = button.action armedAction = nil armedUntil = 0 submit(selected) else armedAction = button.action.id armedUntil = now + CONFIRM_SECONDS statusText = "Zur Bestaetigung denselben Knopf erneut beruehren" statusColor = colors.orange end return end end end appendLog("Testcontroller gestartet auf Computer " .. tostring(os.getComputerID())) refreshStock() render() local refreshTimer = os.startTimer(REFRESH_SECONDS) while true do local event, parameter1, parameter2, parameter3 = os.pullEvent() if event == "monitor_touch" and parameter1 == monitorName then handleTouch(parameter2, parameter3) render() elseif event == "timer" and parameter1 == refreshTimer then if armedAction and os.clock() > armedUntil then armedAction = nil armedUntil = 0 statusText = "Bestaetigung abgelaufen" statusColor = colors.gray end refreshStock() updateJobs() render() refreshTimer = os.startTimer(REFRESH_SECONDS) elseif event == "peripheral_detach" or event == "peripheral" then refreshStock() render() end end