-- Andesite Alloy production controller for CC:Tweaked. -- Two Create Item Vaults are discovered by their contents and remembered. local REFRESH_SECONDS = 1 local MIN_WIDTH = 42 local MIN_HEIGHT = 15 local CLUTCH_OUTPUT_SIDE = "left" local STATE_FILE = "create_andesite_state.txt" local DEVICE_FILE = "create_andesite_devices.txt" local ZINC_ITEMS = { ["create:zinc_nugget"] = true, } local ALLOY_ITEMS = { ["create:andesite_alloy"] = true, } local button = { x1 = 0, x2 = 0, y = 0 } local function loadText(path) if not fs.exists(path) then return nil end local handle = fs.open(path, "r") if not handle then return nil end local value = handle.readAll() handle.close() return value end local function saveText(path, value) local handle = fs.open(path, "w") if not handle then return false end handle.write(value) handle.close() return true end local requestedRunning = loadText(STATE_FILE) == "RUNNING" local devices = textutils.unserialize(loadText(DEVICE_FILE) or "") or {} local capacityCache = {} -- Fail safe while peripherals are being discovered. redstone.setOutput(CLUTCH_OUTPUT_SIDE, false) local monitor = peripheral.find("monitor") if not monitor then error("Kein Monitor gefunden. Monitor per Wired Modem verbinden.", 0) end local function fitMonitor() local chosen = 0.5 for step = 10, 1, -1 do local scale = step / 2 local ok = pcall(monitor.setTextScale, scale) if ok then local width, height = monitor.getSize() if width >= MIN_WIDTH and height >= MIN_HEIGHT then chosen = scale break end end end monitor.setTextScale(chosen) monitor.setCursorBlink(false) end fitMonitor() local function center(y, text, foreground, background) local width = monitor.getSize() text = tostring(text or "") if #text > width then text = text:sub(1, width) end local x = math.max(1, math.floor((width - #text) / 2) + 1) monitor.setBackgroundColor(background or colors.black) monitor.setTextColor(foreground or colors.white) monitor.setCursorPos(1, y) monitor.write((" "):rep(width)) monitor.setCursorPos(x, y) monitor.write(text) end local function formatInteger(value) value = math.floor(math.abs(tonumber(value) or 0) + 0.5) local digits, parts = tostring(value), {} while #digits > 3 do table.insert(parts, 1, digits:sub(-3)) digits = digits:sub(1, -4) end table.insert(parts, 1, digits) return table.concat(parts, ".") end local function getValueIgnoringCase(data, wantedKeys) if type(data) ~= "table" then return nil end for key, value in pairs(data) do local lowered = tostring(key):lower() for _, wanted in ipairs(wantedKeys) do if lowered == wanted:lower() then return value end end end return nil end local function isVault(name) return peripheral.isPresent(name) and peripheral.hasType(name, "inventory") and peripheral.hasType(name, "create:item_vault") end local function getInventory(name) if not name or not isVault(name) then return nil end local okList, listed = pcall(peripheral.call, name, "list") local okSize, size = pcall(peripheral.call, name, "size") if not okList or not okSize or type(listed) ~= "table" then return nil end local result = { name = name, list = listed, size = tonumber(size) or 0, capacity = 0, total = 0, } local cached = capacityCache[name] if cached and cached.size == result.size then result.capacity = cached.capacity else for slot = 1, result.size do local ok, limit = pcall(peripheral.call, name, "getItemLimit", slot) result.capacity = result.capacity + (ok and tonumber(limit) or 64) end capacityCache[name] = { size = result.size, capacity = result.capacity } end for _, item in pairs(listed) do result.total = result.total + (tonumber(item.count) or 0) end return result end local function countItems(inventory, wanted) if not inventory then return 0 end local count = 0 for _, item in pairs(inventory.list) do if wanted[item.name] then count = count + (tonumber(item.count) or 0) end end return count end local function containsItems(inventory, wanted) return countItems(inventory, wanted) > 0 end local function discoverVaults() local oldZinc, oldAlloy = devices.zinc, devices.alloy local savedZinc = getInventory(devices.zinc) local savedAlloy = getInventory(devices.alloy) for _, name in ipairs(peripheral.getNames()) do if isVault(name) then local inventory = getInventory(name) if inventory then if containsItems(inventory, ZINC_ITEMS) then devices.zinc = name savedZinc = inventory end if containsItems(inventory, ALLOY_ITEMS) then devices.alloy = name savedAlloy = inventory end end end end if devices.zinc ~= oldZinc or devices.alloy ~= oldAlloy then saveText(DEVICE_FILE, textutils.serialize(devices)) end return savedZinc or getInventory(devices.zinc), savedAlloy or getInventory(devices.alloy) end local function discoverStressometer() for _, name in ipairs(peripheral.getNames()) do if peripheral.hasType(name, "block_reader") then local ok, data = pcall(peripheral.call, name, "getBlockData") if ok and type(data) == "table" and tonumber(getValueIgnoringCase(data, { "Speed" })) then local network = getValueIgnoringCase(data, { "Network" }) local speed = math.abs(tonumber(getValueIgnoringCase(data, { "Speed" })) or 0) if type(network) == "table" then local capacity = tonumber(getValueIgnoringCase(network, { "Capacity" })) or 0 local usage = tonumber(getValueIgnoringCase(network, { "Stress" })) or 0 local load = capacity > 0 and usage / capacity * 100 or 0 return speed, capacity, usage, load end return speed, nil, nil, nil end end end return nil, nil, nil, nil end local function percentage(value, maximum) if not maximum or maximum <= 0 then return 0 end return math.min(100, value / maximum * 100) end local function evaluate() local zincVault, alloyVault = discoverVaults() local zinc = countItems(zincVault, ZINC_ITEMS) local alloy = countItems(alloyVault, ALLOY_ITEMS) local speed, capacity, usage, load = discoverStressometer() local missing = not zincVault or not alloyVault local noMaterial = zincVault and zinc <= 0 local outputFull = alloyVault and alloyVault.capacity > 0 and alloyVault.total >= alloyVault.capacity local overstressed = capacity and usage and usage > capacity local allowed = not missing and not noMaterial and not outputFull and not overstressed local effectiveRunning = requestedRunning and allowed redstone.setOutput(CLUTCH_OUTPUT_SIDE, effectiveRunning) local status, color = "STOPPED", colors.red if requestedRunning then if missing then status, color = "NO VAULT DATA", colors.red elseif noMaterial then status, color = "WAITING: NO ZINC", colors.orange elseif outputFull then status, color = "STANDBY: OUTPUT FULL", colors.orange elseif overstressed then status, color = "STOPPED: OVERSTRESSED", colors.red elseif not speed or speed == 0 then status, color = "STARTING / NO ROTATION", colors.orange else status, color = "RUNNING", colors.lime end end return { zincVault = zincVault, alloyVault = alloyVault, zinc = zinc, alloy = alloy, speed = speed, capacity = capacity, usage = usage, load = load, status = status, statusColor = color, effectiveRunning = effectiveRunning, } end local function drawInventory(y, label, count, inventory, color) if not inventory then center(y, label .. ": NO DATA", colors.red) return end local fill = percentage(inventory.total, inventory.capacity) local fillText = ("%.1f"):format(fill):gsub("%.", ",") local text = ("%s: %s / %s %s%%"):format( label, formatInteger(count), formatInteger(inventory.capacity), fillText ) center(y, text, color or colors.white) end local function drawButton(height, data) local width = monitor.getSize() local label = requestedRunning and " CONTROL: ENABLED " or " CONTROL: DISABLED " local x = math.max(1, math.floor((width - #label) / 2) + 1) local y = math.max(14, height - 1) button.x1 = x button.x2 = math.min(width, x + #label - 1) button.y = y monitor.setCursorPos(1, y) monitor.setBackgroundColor(colors.black) monitor.write((" "):rep(width)) monitor.setCursorPos(x, y) monitor.setBackgroundColor(requestedRunning and (data.effectiveRunning and colors.lime or colors.orange) or colors.red) monitor.setTextColor(colors.black) monitor.write(label) monitor.setBackgroundColor(colors.black) end local function render() local _, height = monitor.getSize() local data = evaluate() monitor.setBackgroundColor(colors.black) monitor.clear() center(1, "ANDESITE ALLOY FACTORY", colors.cyan) drawInventory(3, "ZINC NUGGETS", data.zinc, data.zincVault, data.zinc <= 0 and colors.orange or colors.lime) drawInventory(4, "ANDESITE ALLOY", data.alloy, data.alloyVault, data.alloyVault and data.alloyVault.total >= data.alloyVault.capacity and colors.orange or colors.lime) center(6, "MACHINE SHAFT", colors.cyan) if data.speed ~= nil then center(7, ("Speed: %.1f RPM"):format(data.speed), data.speed > 0 and colors.lime or colors.orange) else center(7, "Speed: NO DATA", colors.red) end if data.capacity then center(8, "Capacity: " .. formatInteger(data.capacity) .. " SU", colors.white) center(9, "Usage: " .. formatInteger(data.usage) .. " SU", colors.white) local loadText = ("Load: %.1f %%"):format(data.load):gsub("%.", ",") center(10, loadText, data.load >= 90 and colors.red or data.load >= 70 and colors.orange or colors.lime) else center(8, "Shaft network offline", colors.gray) end center(12, data.status, data.statusColor) drawButton(height, data) center(height, "Touch = Anlage freigeben/stoppen", colors.gray) end local function safeRender() local ok, reason = pcall(render) if not ok then redstone.setOutput(CLUTCH_OUTPUT_SIDE, false) monitor.setBackgroundColor(colors.black) monitor.clear() center(1, "CONTROLLER ERROR", colors.red) center(3, tostring(reason), colors.orange) end end local function toggleControl() requestedRunning = not requestedRunning saveText(STATE_FILE, requestedRunning and "RUNNING" or "STOPPED") end safeRender() local refreshTimer = os.startTimer(REFRESH_SECONDS) local function run() while true do local event, first, second, third = os.pullEventRaw() if event == "terminate" then return elseif event == "timer" and first == refreshTimer then safeRender() refreshTimer = os.startTimer(REFRESH_SECONDS) elseif event == "monitor_touch" then local x, y = second, third if y == button.y and x >= button.x1 and x <= button.x2 then toggleControl() safeRender() end end end end local ok, reason = pcall(run) redstone.setOutput(CLUTCH_OUTPUT_SIDE, false) if not ok then error(reason, 0) end