diff --git a/README.md b/README.md index d890f57..e50eb8a 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,32 @@ Controller-Koordinaten `11266/64/10810`, `11260/64/10810` und `11254/64/10810` z Fehlt ein Reader, bleibt dadurch exakt die betroffene Boiler-Zeile auf `MISSING / NO READER`. Jeder Block Reader muss direkt auf den Controller-Tank seines Boiler-Multiblocks zeigen. +## Andesite-Alloy-Produktion + +Der erste lokale Create-Produktionscontroller ueberwacht einen Zink-Nugget-Vault, einen +Andesite-Alloy-Vault und die per Clutch getrennte Maschinenwelle. Die beiden Vaults muessen +jeweils direkt ueber einen aktivierten Wired Modem Full Block als Inventory-Peripheral mit dem +Computer verbunden sein. Ein Advanced-Peripherals Block Reader zeigt auf das Stressometer der +Maschinenwelle. Der Redstone-Ausgang `left` steuert die Clutch ueber einen Fail-safe-Inverter. + +Installation und erster Start: + +```lua +wget https://git.peli-server.de/Dystroyer8/ATM10_CC_Codes/raw/branch/main/create_andesite_display.lua create_andesite_display.lua +create_andesite_display +``` + +Der Controller erkennt die Vaults anhand von `create:zinc_nugget` und +`create:andesite_alloy` und speichert ihre Peripheral-Namen. Die Freigabe wird ebenfalls +gespeichert. Fehlendes Zink, ein voller Ausgang, fehlende Vault-Daten oder eine ueberlastete +Welle schalten die Maschinenwelle ab; nach Behebung wird eine weiterhin freigegebene Anlage +automatisch fortgesetzt. Fuer den automatischen Start: + +```lua +wget https://git.peli-server.de/Dystroyer8/ATM10_CC_Codes/raw/branch/main/create_andesite_startup.lua startup.lua +reboot +``` + ## Struktur ```text diff --git a/create_andesite_display.lua b/create_andesite_display.lua new file mode 100644 index 0000000..8c1f00c --- /dev/null +++ b/create_andesite_display.lua @@ -0,0 +1,354 @@ +-- 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 diff --git a/create_andesite_startup.lua b/create_andesite_startup.lua new file mode 100644 index 0000000..f44cf63 --- /dev/null +++ b/create_andesite_startup.lua @@ -0,0 +1,8 @@ +-- Startup wrapper for the local Andesite Alloy production controller. + +if not fs.exists("create_andesite_display.lua") then + printError("create_andesite_display.lua fehlt") + return +end + +shell.run("create_andesite_display.lua") diff --git a/create_peripheral_debug.lua b/create_peripheral_debug.lua index 90ee7a5..c411280 100644 --- a/create_peripheral_debug.lua +++ b/create_peripheral_debug.lua @@ -27,8 +27,10 @@ for _, name in ipairs(names) do writeLine("Methods: " .. table.concat(methods, ", ")) local hasBlockData = false + local hasInventoryList = false for _, method in ipairs(methods) do if method == "getBlockData" then hasBlockData = true end + if method == "list" then hasInventoryList = true end end if hasBlockData then @@ -36,6 +38,13 @@ for _, name in ipairs(names) do writeLine("getBlockData OK: " .. tostring(ok)) writeLine(ok and textutils.serialize(data) or tostring(data)) end + if hasInventoryList then + local okSize, size = pcall(peripheral.call, name, "size") + local okList, items = pcall(peripheral.call, name, "list") + writeLine("Inventory size: " .. (okSize and tostring(size) or "ERROR")) + writeLine("Inventory list OK: " .. tostring(okList)) + writeLine(okList and textutils.serialize(items) or tostring(items)) + end writeLine("") end diff --git a/manifest.lua b/manifest.lua index 7df3138..a48a828 100644 --- a/manifest.lua +++ b/manifest.lua @@ -5,6 +5,8 @@ return { files = { "startup.lua", "create_boiler_display.lua", + "create_andesite_display.lua", + "create_andesite_startup.lua", "create_peripheral_debug.lua", "core/constants.lua", "core/log.lua",