Files
ATM10_CC_Codes/create_andesite_display.lua
T

384 lines
12 KiB
Lua

-- 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 = 28
local MIN_HEIGHT = 13
local CLUTCH_OUTPUT_SIDE = "left"
-- This installation drives the Clutch directly: powered means stopped.
-- Change to true if a redstone-torch inverter is installed later.
local OUTPUT_LEVEL_WHEN_RUNNING = false
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 = {}
local function setMachineRunning(running)
local level = running and OUTPUT_LEVEL_WHEN_RUNNING or not OUTPUT_LEVEL_WHEN_RUNNING
redstone.setOutput(CLUTCH_OUTPUT_SIDE, level)
end
-- Fail safe while peripherals are being discovered.
setMachineRunning(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
local name = tostring(item.name or ""):lower()
local matches = wanted[item.name]
if wanted == ZINC_ITEMS and name:find("zinc", 1, true) and name:find("nugget", 1, true) then
matches = true
elseif wanted == ALLOY_ITEMS and name:find("andesite_alloy", 1, true) then
matches = true
end
if matches 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)
local connectedVaults = {}
for _, name in ipairs(peripheral.getNames()) do
if isVault(name) then
local inventory = getInventory(name)
if inventory then
connectedVaults[#connectedVaults + 1] = inventory
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
-- This controller intentionally owns exactly two vaults. If a mod supplies
-- Zinc Nuggets under an unexpected registry name, the non-alloy vault is
-- still unambiguous and becomes the input vault.
if not savedZinc and savedAlloy and #connectedVaults == 2 then
for _, inventory in ipairs(connectedVaults) do
if inventory.name ~= savedAlloy.name then
devices.zinc = inventory.name
savedZinc = inventory
end
end
end
if not savedAlloy and savedZinc and #connectedVaults == 2 then
for _, inventory in ipairs(connectedVaults) do
if inventory.name ~= savedZinc.name then
devices.alloy = inventory.name
savedAlloy = inventory
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 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
setMachineRunning(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 text = ("%s %s / %s"):format(
label,
formatInteger(count),
formatInteger(inventory.capacity)
)
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(12, 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 FACTORY", colors.cyan)
drawInventory(3, "ZINC", data.zinc, data.zincVault,
data.zinc <= 0 and colors.orange or colors.lime)
drawInventory(4, "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, "SU: " .. formatInteger(data.usage) .. " / " .. formatInteger(data.capacity), colors.white)
local loadText = ("Load: %.1f %%"):format(data.load):gsub("%.", ",")
center(9, 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(11, 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
setMachineRunning(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)
setMachineRunning(false)
if not ok then error(reason, 0) end