268 lines
9.0 KiB
Lua
268 lines
9.0 KiB
Lua
-- Create boiler status board for CC:Tweaked.
|
|
-- All Create values are read through Advanced Peripherals Block Readers.
|
|
|
|
local REFRESH_SECONDS = 1
|
|
local MIN_WIDTH = 38
|
|
local MIN_HEIGHT = 14
|
|
local CLUTCH_OUTPUT_SIDE = "left"
|
|
local STATE_FILE = "create_boiler_state.txt"
|
|
|
|
local BOILERS = {
|
|
{ label = "BOILER 1", position = { 11266, 64, 10810 } },
|
|
{ label = "BOILER 2", position = { 11260, 64, 10810 } },
|
|
{ label = "BOILER 3", position = { 11252, 64, 10810 } },
|
|
}
|
|
|
|
local clutchButton = { x1 = 0, x2 = 0, y = 0 }
|
|
|
|
local function loadProductionState()
|
|
if not fs.exists(STATE_FILE) then return false end
|
|
local handle = fs.open(STATE_FILE, "r")
|
|
if not handle then return false end
|
|
local saved = handle.readAll()
|
|
handle.close()
|
|
return saved == "RUNNING"
|
|
end
|
|
|
|
local function saveProductionState(running)
|
|
local handle = fs.open(STATE_FILE, "w")
|
|
if not handle then return false end
|
|
handle.write(running and "RUNNING" or "STOPPED")
|
|
handle.close()
|
|
return true
|
|
end
|
|
|
|
local productionRunning = loadProductionState()
|
|
redstone.setOutput(CLUTCH_OUTPUT_SIDE, productionRunning)
|
|
|
|
local monitor = peripheral.find("monitor")
|
|
if not monitor then
|
|
error("Kein Monitor gefunden. Monitor direkt oder 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)
|
|
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 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 samePosition(a, b)
|
|
if type(a) ~= "table" or type(b) ~= "table" then return true end
|
|
return tonumber(a[1]) == tonumber(b[1])
|
|
and tonumber(a[2]) == tonumber(b[2])
|
|
and tonumber(a[3]) == tonumber(b[3])
|
|
end
|
|
|
|
local function positionKey(position)
|
|
if type(position) ~= "table" then return nil end
|
|
local x, y, z = tonumber(position[1]), tonumber(position[2]), tonumber(position[3])
|
|
if not x or not y or not z then return nil end
|
|
return ("%d,%d,%d"):format(x, y, z)
|
|
end
|
|
|
|
local function discoverReaders()
|
|
local boilers, networks = {}, {}
|
|
local names = peripheral.getNames()
|
|
table.sort(names)
|
|
|
|
for _, name in ipairs(names) do
|
|
if peripheral.hasType(name, "block_reader") then
|
|
local ok, data = pcall(peripheral.call, name, "getBlockData")
|
|
if ok and type(data) == "table" then
|
|
local boiler = getValueIgnoringCase(data, { "Boiler" })
|
|
local network = getValueIgnoringCase(data, { "Network" })
|
|
local speed = getValueIgnoringCase(data, { "Speed" })
|
|
|
|
if type(boiler) == "table" then
|
|
local position = getValueIgnoringCase(data, { "LastKnownPos", "Controller" })
|
|
local key = positionKey(position)
|
|
if key then boilers[key] = { name = name, data = data, boiler = boiler } end
|
|
elseif type(network) == "table" and tonumber(speed) then
|
|
networks[#networks + 1] = { name = name, data = data, network = network }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return boilers, networks
|
|
end
|
|
|
|
local function readBoiler(entry)
|
|
if not entry then return "MISSING", "NO READER", colors.red end
|
|
|
|
local controller = getValueIgnoringCase(entry.data, { "Controller" })
|
|
local readerPos = getValueIgnoringCase(entry.data, { "LastKnownPos" })
|
|
if not samePosition(controller, readerPos) then
|
|
return "READER", "WRONG TANK", colors.red
|
|
end
|
|
|
|
local boiler = entry.boiler
|
|
local passive = tonumber(getValueIgnoringCase(boiler, { "PassiveHeat", "passive" })) or 0
|
|
local heat = tonumber(getValueIgnoringCase(boiler, { "ActiveHeat", "Heat", "actualHeat" })) or 0
|
|
local supply = tonumber(getValueIgnoringCase(boiler, { "Supply", "waterSupply" })) or 0
|
|
local engines = tonumber(getValueIgnoringCase(boiler, { "Engines", "attachedEngines" })) or 0
|
|
|
|
-- A heat source alone does not make the boiler operational. Create reports
|
|
-- PassiveHeat even when a campfire boiler has no water and is IDLE.
|
|
if supply <= 0 then return "IDLE", "NO WATER", colors.red end
|
|
if engines <= 0 then return "IDLE", "NO ENGINE", colors.red end
|
|
|
|
if passive > 0 then return "ONLINE", "CAMPFIRE", colors.lime end
|
|
if heat >= 18 then return "ONLINE", "SUPERHEATED", colors.lime end
|
|
if heat > 0 then return "ONLINE", "HEATED", colors.lime end
|
|
return "IDLE", "NO HEAT", colors.orange
|
|
end
|
|
|
|
local function readMainShaft(entry)
|
|
if not entry then return nil, nil, nil, nil end
|
|
local speed = math.abs(tonumber(getValueIgnoringCase(entry.data, { "Speed" })) or 0)
|
|
local capacity = tonumber(getValueIgnoringCase(entry.network, { "Capacity" })) or 0
|
|
local usage = tonumber(getValueIgnoringCase(entry.network, { "Stress" })) or 0
|
|
local load = capacity > 0 and usage / capacity * 100 or 0
|
|
return speed, capacity, usage, load
|
|
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 drawClutchButton(height)
|
|
local width = monitor.getSize()
|
|
-- With the external redstone-torch inverter, output ON means the clutch is
|
|
-- unpowered and production runs. Output OFF is the fail-safe stopped state.
|
|
local label = productionRunning and " PRODUCTION: RUNNING " or " PRODUCTION: STOPPED "
|
|
local x = math.max(1, math.floor((width - #label) / 2) + 1)
|
|
local y = math.max(13, height - 1)
|
|
|
|
clutchButton.x1 = x
|
|
clutchButton.x2 = math.min(width, x + #label - 1)
|
|
clutchButton.y = y
|
|
|
|
monitor.setCursorPos(1, y)
|
|
monitor.setBackgroundColor(colors.black)
|
|
monitor.write((" "):rep(width))
|
|
monitor.setCursorPos(x, y)
|
|
monitor.setBackgroundColor(productionRunning and colors.lime or colors.red)
|
|
monitor.setTextColor(colors.black)
|
|
monitor.write(label)
|
|
monitor.setBackgroundColor(colors.black)
|
|
end
|
|
|
|
local function toggleProduction()
|
|
productionRunning = not productionRunning
|
|
redstone.setOutput(CLUTCH_OUTPUT_SIDE, productionRunning)
|
|
saveProductionState(productionRunning)
|
|
end
|
|
|
|
local function render()
|
|
local _, height = monitor.getSize()
|
|
local boilers, networks = discoverReaders()
|
|
local speed, capacity, usage, load = readMainShaft(networks[1])
|
|
|
|
monitor.setBackgroundColor(colors.black)
|
|
monitor.clear()
|
|
center(1, "CREATE KRAFTWERK", colors.cyan)
|
|
|
|
for index, configuredBoiler in ipairs(BOILERS) do
|
|
local reader = boilers[positionKey(configuredBoiler.position)]
|
|
local state, heatMode, color = readBoiler(reader)
|
|
center(2 + index, ("%s %-7s %s"):format(configuredBoiler.label, state, heatMode), color)
|
|
end
|
|
|
|
center(7, "MAIN SHAFT", colors.cyan)
|
|
if speed then
|
|
center(8, ("Speed: %.1f RPM"):format(speed), speed > 0 and colors.lime or colors.orange)
|
|
center(9, "Capacity: " .. formatInteger(capacity) .. " SU", colors.white)
|
|
center(10, "Usage: " .. formatInteger(usage) .. " SU", colors.white)
|
|
local loadText = ("Load: %.1f %%"):format(load):gsub("%.", ",")
|
|
center(11, loadText, load >= 90 and colors.red or load >= 70 and colors.orange or colors.lime)
|
|
else
|
|
center(8, "Kein Gauge-Reader gefunden", colors.red)
|
|
center(9, "Reader auf Speed-/Stressometer", colors.orange)
|
|
end
|
|
|
|
drawClutchButton(height)
|
|
center(height, "Touch = Produktion umschalten", colors.gray)
|
|
monitor.setTextColor(colors.white)
|
|
monitor.setBackgroundColor(colors.black)
|
|
term.setCursorPos(1, 1)
|
|
end
|
|
|
|
local function safeRender()
|
|
local ok, reason = pcall(render)
|
|
if not ok then
|
|
monitor.setBackgroundColor(colors.black)
|
|
monitor.clear()
|
|
center(1, "ANZEIGEFEHLER", colors.red)
|
|
center(3, tostring(reason), colors.orange)
|
|
end
|
|
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 == clutchButton.y and x >= clutchButton.x1 and x <= clutchButton.x2 then
|
|
toggleProduction()
|
|
safeRender()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local ok, reason = pcall(run)
|
|
redstone.setOutput(CLUTCH_OUTPUT_SIDE, false)
|
|
if not ok then error(reason, 0) end
|