_G.OS_VERSION = "0.6" Shuttingdown = false local function downloadFile(url, path) print("Downloading: " .. path) local response = http.get(url) if response then local file = fs.open(path, "w") file.write(response.readAll()) file.close() response.close() return true end return false end local function ensureDependencies(deps) local tasks = {} for path, url in pairs(deps) do if not fs.exists(path) then table.insert(tasks, function() if not downloadFile(url, path) then error("Failed to download: " .. path) end end) end end if #tasks > 0 then parallel.waitForAll(unpack(tasks)) end end -- Configuration local dependencies = { ["tendril_api.lua"] = "http://kon.homes/ccscripts/tendril_api.lua" } -- Execute ensureDependencies(dependencies) -- Now safe to require local tendril = require("tendril_api") cfg = {} local VERSION_URL = "https://api.github.com/repos/evilcarrotoverlord/Dreactor/releases" local sideConfigs = { top = "none", right = "none", front = "none", left = "none", back = "none", bottom = "none" } local numpad = { active = false, value = "", callback = nil, error = false } local tendril = require("tendril_api") local service local syncTimer = os.startTimer(1) local ID = os.getComputerID() local lastState = "" local menuMode = "MAIN" local currentScale = {} local runningTotalRF = 0 local needsFullRedraw = true local lastHourLog = os.clock() local fuelBuffer = {} local tempBuffer = {} local forceRecovery = false local lastNBTReductionLog = 0 local lastForceRecoveryLog = 0 local LOG_COOLDOWN = 10 local refueling = false local lastTemp = 0 local lastShield = 100 local lastSaturation = 100 local isFineTuning = false local adjustmentTick = 0 local SLOW_RATE = 10 local targetNBT = 2.000 local shieldTarget = 50 local criticalLogged = false local function findPeripherals(name) local found = {} local names = peripheral.getNames() for _, v in ipairs(names) do if peripheral.getType(v):find(name) then local p = peripheral.wrap(v) p.id = v table.insert(found, p) end end return found end local function findWirelessModem() return peripheral.find("modem", function(name, modem) return modem.isWireless() end) end local function getFuelPercent(info) return 100 - (math.ceil(info.fuelConversion / info.maxFuelConversion * 10000) * 0.01) end local function getReactorSafety(info) local sPct = (info.fieldStrength / info.maxFieldStrength) * 100 local ePct = (info.energySaturation / info.maxEnergySaturation) * 100 local isThermalRisk = info.temperature > 2000 and not Shuttingdown local isHardwareFailure = (sPct <= 10 or ePct <= 10) and isThermalRisk return { shield = sPct, saturation = ePct, isCritical = (info.temperature >= 10000 or isHardwareFailure or info.status == "beyond_hope"), isLowSat = (ePct < 16), isDangerSat = (ePct < 15) } end local function saveConfig(mode, totalRF, tNBT, sTarget, tendrilEnabled, code) local f = fs.open("React.conf", "w") if f then local data = { mode = mode or "Manual", totalRF = math.floor(totalRF or 0), targetNBT = tNBT or 2.000, shieldTarget = sTarget or 50, tendril = tendrilEnabled or false, tendrilcode = code or "0000", sides = sideConfigs } f.write(textutils.serialize(data)) f.close() end end local function loadConfig() if not fs.exists("React.conf") then return { tendril = false, tendrilcode = "0000", mode = "Manual", targetNBT = 2.000, shieldTarget = 50, totalRF = 0 } end local file = fs.open("React.conf", "r") local data = textutils.unserialize(file.readAll()) file.close() if type(data) == "table" then if data.sides then for k, v in pairs(data.sides) do sideConfigs[k] = v end end return data end return { tendril = false, tendrilcode = "0000", mode = "Manual" } end local function setMonitorScale(target) if target == term then return end local id = peripheral.getName(target) if not currentScale[id] or needsFullRedraw then target.setTextScale(1) local w, h = target.getSize() local desired = (w < 31 or h < 21) and 0.5 or 1 target.setTextScale(desired) currentScale[id] = desired end end local function fetchReleases() local list = {} local response = http.get(VERSION_URL) if response then local data = textutils.unserialiseJSON(response.readAll()) response.close() if data then for i, rel in ipairs(data) do local downloadUrl = nil for _, asset in ipairs(rel.assets or {}) do if asset.name == "installer.lua" then downloadUrl = asset.browser_download_url break end end table.insert(list, { tag = rel.tag_name, url = downloadUrl, isLatest = (i == 1), isCurrent = (rel.tag_name == _G.OS_VERSION) }) end end end return list end local function openUpdateMenu(target) local w, h = target.getSize() local winX, winY, winW, winH = 3, 3, w - 4, h - 5 local visibleRows = winH - 1 target.setBackgroundColor(colors.black) target.setTextColor(colors.white) target.clear() local loadMsg = "Connecting to GitHub..." target.setCursorPos(math.floor(w/2 - #loadMsg/2), math.floor(h/2)) target.write(loadMsg) local releases = fetchReleases() if #releases == 0 then needsFullRedraw = true return end local selectedIdx = 1 local scrollOffset = 0 while true do target.setBackgroundColor(colors.black) target.clear() target.setCursorPos(1, 1) target.setBackgroundColor(colors.purple) target.setTextColor(colors.black) target.clearLine() local title = " UPDATE MANAGER Current version:" .. _G.OS_VERSION target.setCursorPos(math.floor(w/2 - #title/2), 1) target.write(title) target.setCursorPos(winX, winY) target.setBackgroundColor(colors.black) target.setTextColor(colors.purple) target.write(string.char(151) .. string.rep(string.char(131), winW - 2)) target.setBackgroundColor(colors.purple) target.setTextColor(colors.black) target.write(string.char(148)) for i = 1, winH - 1 do local rowY = winY + i target.setCursorPos(winX, rowY) target.setBackgroundColor(colors.black) target.setTextColor(colors.purple) target.write(string.char(149)) local idx = i + scrollOffset if releases[idx] then local rel = releases[idx] local label = string.format("%s %s", rel.tag, (rel.isLatest and "[NEW]" or (rel.isCurrent and "[LIVE]" or ""))) local innerW = winW - 4 local padding = innerW - #label local leftPad = math.floor(padding / 2) local rightPad = padding - leftPad local centeredLabel = string.rep(" ", leftPad) .. label .. string.rep(" ", rightPad) target.setCursorPos(winX + 2, rowY) if idx == selectedIdx then target.setBackgroundColor(colors.purple) target.setTextColor(colors.black) target.write(centeredLabel) else target.setBackgroundColor(colors.black) target.setTextColor(colors.orange) target.write(centeredLabel) end end target.setCursorPos(winX + winW - 1, rowY) target.setBackgroundColor(colors.purple) target.setTextColor(colors.black) target.write(string.char(149)) end local buttonY = winY + winH target.setBackgroundColor(colors.purple) target.setCursorPos(winX, buttonY) target.setTextColor(colors.lime) target.write(" [ INSTALL ] ") target.setBackgroundColor(colors.black) local gutterStart = winX + 13 local gutterEnd = (winX + winW - 1) - 13 if gutterEnd >= gutterStart then target.setCursorPos(gutterStart, buttonY) target.write(string.rep(" ", gutterEnd - gutterStart + 1)) end target.setBackgroundColor(colors.purple) target.setCursorPos(winX + winW - 13, buttonY) target.setTextColor(colors.red) target.write(" [ CANCEL ] ") local event, p1, p2, p3 = os.pullEvent() if event == "key" then if p1 == keys.up and selectedIdx > 1 then selectedIdx = selectedIdx - 1 elseif p1 == keys.down and selectedIdx < #releases then selectedIdx = selectedIdx + 1 elseif p1 == keys.enter then break elseif p1 == keys.q then needsFullRedraw = true return end elseif event == "mouse_click" or event == "monitor_touch" then local mx, my = p2, p3 if my == buttonY then if mx >= winX and mx <= winX + 12 then break end if mx >= winX + winW - 13 and mx <= winX + winW then needsFullRedraw = true return end end if my > winY and my < buttonY then local clickedIdx = (my - winY) + scrollOffset if releases[clickedIdx] then selectedIdx = clickedIdx end end end if selectedIdx <= scrollOffset then scrollOffset = selectedIdx - 1 end if selectedIdx > scrollOffset + (visibleRows - 1) then scrollOffset = selectedIdx - (visibleRows - 1) end end local targetRel = releases[selectedIdx] if targetRel and targetRel.url then local res = http.get(targetRel.url) if res then local code = res.readAll() res.close() term.setBackgroundColor(colors.black) term.clear() term.setCursorPos(1, 1) print("Installing " .. targetRel.tag .. "...") local func = load(code, "installer", "t", _ENV) if func then pcall(func) os.reboot() end end end needsFullRedraw = true end local function formatTime(seconds) if seconds <= 0 or seconds > 10^9 then return "Infinite" end local days = math.floor(seconds / 86400) local hours = math.floor((seconds % 86400) / 3600) local mins = math.floor((seconds % 3600) / 60) return string.format("%dd:%dh:%dm", days, hours, mins) end local function formatRF(val) local units = {"rf", "Krf", "Mrf", "Grf", "Trf", "Erf"} local unit = 1 local absVal = math.abs(val) while absVal >= 1000 and unit < #units do absVal = absVal / 1000 unit = unit + 1 end local str = string.format("%.2f", (val < 0 and -absVal or absVal)) str = str:gsub("%.?0+$", "") return str .. units[unit] end local function getTempColor(temp) if temp < 2000 then return colors.lightBlue elseif temp < 4500 then return colors.yellow elseif temp < 7500 then return colors.green elseif temp < 8500 then return colors.orange else return colors.red end end local function drawVerticalText(target, x, y, text, color) target.setTextColor(color) target.setBackgroundColor(colors.black) for i = 1, #text do target.setCursorPos(x, y + (i - 1)) target.write(text:sub(i, i):upper()) end end local function smartFormat(val, precision) local fmt = "%" .. (precision + 3) .. "." .. precision .. "f" local str = string.format(fmt, val) return str .. "%" end local function inputNumpad(target, tx, ty, isClick, event, p1) if not numpad.active then return false end if event == "char" then if tonumber(p1) and #numpad.value < 9 then numpad.value = numpad.value .. p1 needsFullRedraw = true return true end elseif event == "key" then if p1 == keys.backspace then numpad.value = numpad.value:sub(1, -2) needsFullRedraw = true return true elseif p1 == keys.enter or p1 == keys.numPadEnter then if numpad.callback then numpad.callback(numpad.value) end return true end end local w, h = target.getSize() local winW, winH = 11, 8 numpad.active = true local winX, winY = math.floor((w - winW) / 2), math.floor((h - winH) / 2) local btns = { {"1", 1, 3}, {"2", 4, 3}, {"3", 7, 3}, {"4", 1, 4}, {"5", 4, 4}, {"6", 7, 4}, {"7", 1, 5}, {"8", 4, 5}, {"9", 7, 5}, {"C", 1, 6}, {"0", 4, 6}, {"<", 7, 6} } if not isClick then target.setBackgroundColor(colors.gray) for y = 0, winH do target.setCursorPos(winX, winY + y) target.write(string.rep(" ", winW)) end target.setTextColor(colors.white) target.setBackgroundColor(colors.gray) target.setCursorPos(winX + 1, winY) target.write((numpad.error and " INVALID " or " ENTRY "):sub(1, winW)) target.setBackgroundColor(colors.black) target.setCursorPos(winX + 1, winY + 1) target.write((numpad.value .. "_________"):sub(1, 9)) for i, b in ipairs(btns) do target.setCursorPos(winX + b[2], winY + b[3]) local bgColor = (i % 2 == 0) and colors.lightGray or colors.black if b[1] == "C" then bgColor = colors.red elseif b[1] == "<" then bgColor = colors.orange end target.setBackgroundColor(bgColor) target.write(" " .. b[1] .. " ") end target.setBackgroundColor(colors.green) target.setCursorPos(winX + 1, winY + 7) target.write(" OK ") target.setBackgroundColor(colors.red) target.setCursorPos(winX + 6, winY + 7) target.write("EXIT") return true end if tx < winX or tx > winX + winW or ty < winY or ty > winY + winH then return true end if tx >= winX + 1 and tx <= winX + 4 and ty == winY + 7 then if numpad.callback then numpad.callback(numpad.value) end return true elseif tx >= winX + 6 and tx <= winX + 9 and ty == winY + 7 then numpad.active = false return true end for _, b in ipairs(btns) do if tx >= winX + b[2] and tx < winX + b[2] + 3 and ty == winY + b[3] then if b[1] == "C" then numpad.value = "" elseif b[1] == "<" then numpad.value = numpad.value:sub(1, -2) elseif #numpad.value < 9 then numpad.value = numpad.value .. b[1] end return true end end return true end local function drawMiniStatus(target, info) if not info then return end local w, h = target.getSize() local s = getReactorSafety(info) local fPct = 100 - (info.fuelConversion / info.maxFuelConversion * 100) target.setCursorPos(1, h) target.setBackgroundColor(colors.purple) target.setTextColor(colors.black) target.clearLine() local lbl = w < 56 and {"shld", "sat", "fuel", "temp"} or {"Shield", "Saturation", "Fuel", "Temp"} local vals = {s.shield, s.saturation, fPct, info.temperature} local res = "" for i = 1, 4 do local unit = (i == 4 and "C" or "%") res = res .. string.format("%s:%.1f%s ", lbl[i], vals[i], unit) end target.write(res:sub(1, w)) end local function drawButton(target, x, y, width, height, text, bgColor, textColor, trimColor) bgColor, textColor, trimColor = bgColor or colors.blue, textColor or colors.gray, trimColor or colors.gray local label = string.format(" %s ", text) local pad = math.floor((width - #label) / 2) local labelStr = string.rep(" ", pad) .. label .. string.rep(" ", width - #label - pad) for i = 0, height - 1 do target.setCursorPos(x, y + i) local isTop = (i == 0) target.setBackgroundColor(bgColor) target.setTextColor(trimColor) target.write(isTop and ("\151" .. string.rep("\131", width - 2)) or "\149") if not isTop then target.setTextColor(textColor) target.write((i == height - 1 and labelStr or string.rep(" ", width)):sub(2, -2)) end target.setBackgroundColor(trimColor) target.setTextColor(bgColor) target.write(isTop and "\148" or "\149") end end local function updateRedstoneSignals(info) local safety = getReactorSafety(info) local fuelRemainingPct = 100 - ((info.fuelConversion / info.maxFuelConversion) * 100) for side, mode in pairs(sideConfigs) do if mode == "meltdown" then rs.setOutput(side, safety.isCritical) elseif mode == "fuel" then local strength = math.floor((fuelRemainingPct / 100) * 15) rs.setAnalogOutput(side, strength) else rs.setAnalogOutput(side, 0) end end end local function drawOptionsMenu(target, reactors, inGate, outGate, cfg, info) local w, h = target.getSize() local sides = {"top", "right", "front", "left", "back", "bottom"} local grid = { [1]={nil, 1, nil}, [2]={4, 3, 2}, [3]={5, 6, nil} } target.setBackgroundColor(colors.black) target.clear() target.setCursorPos(1, 1) target.setBackgroundColor(colors.purple) target.setTextColor(colors.black) target.clearLine() target.write("OPTIONS") local tCol = findWirelessModem() and (cfg.tendril and colors.green or colors.red) or colors.gray drawButton(target, w - 11, 3, 10, 3, "UPDATE", colors.black, colors.lightBlue, colors.lightBlue) drawButton(target, w - 11, 7, 10, 3, "TENDRIL", colors.black, tCol, tCol) drawButton(target, w - 11, 11, 10, 3, (cfg.tendrilcode or 0), colors.black, colors.purple, colors.purple) target.setCursorPos(w - 9, 12) target.setBackgroundColor(colors.black) target.setTextColor(colors.purple) target.write("CODE:") drawButton(target, w - 11, h - 4, 10, 3, "BACK", colors.black, colors.red, colors.red) local modeCols = { meltdown = colors.red, fuel = colors.orange } for r = 1, 3 do for c, idx in pairs(grid[r]) do local mode = sideConfigs[sides[idx]] local col = modeCols[mode] or colors.gray drawButton(target, 1 + (c-1)*8, 2 + (r-1)*6, 6, 5, mode:sub(1,4):upper(), colors.black, col, col) end end drawMiniStatus(target, info) end local function drawSetupUI(target, reactors, gates, info) if not target then return end setMonitorScale(target) local width, height = target.getSize() local hasReactor, hasInGate, hasOutGate = #reactors > 0, false, false for _, gate in ipairs(gates) do if gate.getSignalHighFlow() == 1 then hasInGate = true else hasOutGate = true end end local isFlash = (math.floor(os.clock() * 2) % 2 == 0) local function getFlashColor(isPresent, defaultColor) return isPresent and defaultColor or (isFlash and defaultColor or colors.gray) end if needsFullRedraw then target.setBackgroundColor(colors.black) target.clear() end target.setCursorPos(1, 1) target.setBackgroundColor(colors.purple) target.clearLine() local hasModem = peripheral.find("modem", function(_, v) return v.isWireless() end) ~= nil target.setCursorPos(1, 1) target.setTextColor(hasModem and colors.green or colors.red) target.write("WIRELESS MODEM") local hasMonitor = peripheral.find("monitor") ~= nil local monText = "MONITOR" target.setCursorPos(width - #monText + 1, 1) target.setTextColor(hasMonitor and colors.green or colors.red) target.write(monText) local title = "Setup incomplete" target.setCursorPos(math.floor(width/2 - #title/2), 1) target.setTextColor(colors.black) target.write(title) local items = { {hasReactor, colors.orange, "Draconic Reactor core"}, {hasInGate, colors.blue, "Input flux gate"}, {hasOutGate, colors.red, "Output flux gate"}, {hasReactor, colors.lightBlue, "Reactor Energy injector"}, {hasReactor, colors.yellow, "Reactor stabilizer"} } for i, item in ipairs(items) do target.setCursorPos(1, i + 1) target.setBackgroundColor(colors.black) target.setTextColor(item[1] and colors.green or colors.red) target.write(item[1] and "O " or "X ") target.setTextColor(item[2]) target.write(item[3]) end local rightX = width - 10 target.setTextColor(colors.purple) target.setCursorPos(1, 7) target.write(("\131"):rep(width)) for i = 8, height do target.setCursorPos(rightX - 1, i) target.write("\149") end local function writeAt(x, y, text, textColor) target.setCursorPos(x, y) target.setTextColor(textColor) target.write(text) end local cReactor = getFlashColor(hasReactor, colors.orange) local cInGate = getFlashColor(hasInGate, colors.blue) local cOutGate = getFlashColor(hasOutGate, colors.red) local cStabilizer = getFlashColor(hasReactor, colors.yellow) local cInjector = getFlashColor(hasReactor, colors.lightBlue) writeAt(rightX + 4, 11, "\143", cReactor) writeAt(rightX + 8, 11, "\143", cStabilizer) writeAt(rightX, 11, "\143", cStabilizer) writeAt(rightX + 4, 8, "\143", cStabilizer) writeAt(rightX + 4, 14, "\143", cStabilizer) writeAt(rightX + 9, 11, "\143", cOutGate) writeAt(rightX + 7, 12, "OUT", cOutGate) writeAt(rightX + 4, 16, "\143", cInjector) writeAt(rightX + 2, 17, "IN\143", cInGate) local function drawWarning(x, y, text, isPresent) target.setTextColor(isPresent and colors.black or colors.red) target.setCursorPos(x, y) target.write(text) end drawWarning(1, 8, "make sure the core has 4", hasReactor) drawWarning(1, 9, "stabilizers at least 3", hasReactor) drawWarning(1, 10, "block away from the core", hasReactor) drawWarning(1, 11, "and one injector that is", hasReactor) drawWarning(1, 12, "faced towards the core", hasReactor) drawWarning(1, 14, "no fluxgates found check", hasOutGate) drawWarning(1, 15, "if a modem is connected", hasOutGate) drawWarning(1, 16, "and if its (red)", hasOutGate) drawWarning(rightX, 2, "SET INPUT", hasInGate) drawWarning(rightX, 3, "FLUXGATE", hasInGate) drawWarning(rightX, 4, "SIGNAL HIGH", hasInGate) drawWarning(rightX, 5, "TO 1 RF/t", hasInGate) end local function logReactorEvent(info, _, disclaimer) local logs = {} if fs.exists("reactor.log") then local f = fs.open("reactor.log", "r") local content = f.readAll() f.close() for line in content:gmatch("[^\r\n]+") do logs[#logs + 1] = line end end local safety = getReactorSafety(info) local fPct = 100 - (info.fuelConversion / info.maxFuelConversion * 100) logs[#logs + 1] = string.format("[%s] %sGen: %s | NBT: %.3f | Temp: %dC | Shld: %.1f%% | Sat: %.1f%% | Fuel: %.2f%%", os.date("%H:%M:%S"), disclaimer and (disclaimer .. " | ") or "", formatRF(info.generationRate), info.fuelConversionRate / 1000, info.temperature, safety.shield, safety.saturation, fPct ) while #logs > 128 do table.remove(logs, 1) end local f = fs.open("reactor.log", "w") f.write(table.concat(logs, "\n")) f.close() end local function triggerEmergencyRedstone() local sides = rs.getSides() for _, side in ipairs(sides) do rs.setOutput(side, true) end end local function drawHorizontalBar(target, x, y, width, percent, color) percent = math.max(0, math.min(100, percent)) local fillWidth = math.ceil((width * percent) / 100) target.setCursorPos(x, y) for i = 1, width do if i <= fillWidth then target.setTextColor(color) else target.setTextColor(colors.gray) end target.write("\143") end end local function adjustFuelRate(info, outGate, targetFuelRate) local currentRate = (info.fuelConversionRate or 0) / 1000 local rateDifference = targetFuelRate - currentRate local absDiff = math.abs(rateDifference) if absDiff < 0.001 then return end local gain = 100000 local adjustment = math.floor(absDiff * gain) adjustment = math.min(adjustment, 128000) adjustment = math.max(adjustment, 500) local currentOutput = outGate.getSignalLowFlow() local newOutput = (rateDifference > 0) and (currentOutput + adjustment) or (currentOutput - adjustment) outGate.setSignalLowFlow(math.max(0, newOutput)) end local function adjustFuelForTempOnly(ri, currentTargetFuelRate) if not ri or not ri.fuelConversionRate then return currentTargetFuelRate end local temp = ri.temperature local tempDelta = math.abs(temp - 8000) local currentNBT = ri.fuelConversionRate / 1000 if tempDelta > 200 then adjustmentTick = adjustmentTick + 1 if adjustmentTick % SLOW_RATE ~= 0 then return currentTargetFuelRate end else adjustmentTick = 0 end if (lastTemp - 8000) * (temp - 8000) < 0 then isFineTuning = true end lastTemp = temp if tempDelta < 0.1 then return currentTargetFuelRate end local step = isFineTuning and 0.001 or 0.01 local direction = temp < 8000 and 1 or -1 local newTarget = math.max(0.1, currentTargetFuelRate + (step * direction)) local dynamicCap = math.max(0.1, tempDelta / 5) / 1000 return math.min(newTarget, currentNBT + dynamicCap) end function balanceReactorInput(reactor, inputGate, targetPercent) local ri = reactor.getReactorInfo() if not ri or not ri.fieldDrainRate or ri.fieldDrainRate ~= ri.fieldDrainRate then return false end if ri.status == "warming_up" or ri.status == "cold" then return false end local currentField = (ri.fieldStrength / ri.maxFieldStrength) * 100 local drain = ri.fieldDrainRate local baseInput = drain / (1 - (targetPercent / 100)) local diff = targetPercent - currentField local gain = 5000 local correction = diff * gain local finalInput = math.max(0, math.floor(baseInput + correction)) if finalInput == finalInput and finalInput ~= math.huge then inputGate.setSignalLowFlow(finalInput) return finalInput end return false end local function drawMainUI(target, info, mode, inGate, outGate) local safety = getReactorSafety(info) if not target then return end setMonitorScale(target) local w, h = target.getSize() local halfH = math.floor(h / 2) local infoX, infoY = 14, halfH + 1 local deg = string.char(176) if needsFullRedraw then target.setBackgroundColor(colors.black) target.clear() end local statusLine = " CORE: " .. info.status:upper() local versionTag = "reactor " .. _G.OS_VERSION local tempText = string.format("TEMP: %d%sC ", info.temperature, deg) target.setCursorPos(1, 1) target.setBackgroundColor(colors.purple) target.setTextColor(colors.black) target.write(statusLine .. string.rep(" ", w - #statusLine)) target.setCursorPos(math.floor(w/2 - #versionTag/2), 1) target.setTextColor(colors.orange) target.write("D") target.setTextColor(colors.black) target.write(versionTag) target.setCursorPos(w - #tempText + 1, 1) target.write(tempText) local startX, barW, startY = 2, w - 8, 2 local fPct = 100 - (info.fuelConversion / info.maxFuelConversion * 100) local gauges = { { "SHIELD STRENGTH Target: " .. shieldTarget .. "%", safety.shield, safety.shield < 20 and colors.red or colors.blue, 1 }, { "ENERGY SATURATION", safety.saturation, safety.saturation < 20 and colors.red or (safety.saturation < 80 and colors.green or colors.white), 1 }, { "FUEL CONVERSION", fPct, fPct < 20 and colors.red or (fPct < 50 and colors.orange or colors.green), 2 } } target.setBackgroundColor(colors.black) for i, g in ipairs(gauges) do local y = startY + ((i-1) * 2) target.setCursorPos(startX, y) target.setTextColor(colors.lightGray) target.write(g[1]) drawHorizontalBar(target, startX, y + 1, barW, g[2], g[3]) local val = smartFormat(g[2], g[4]) target.setCursorPos(w - #val, y + 1) target.write(val) end local modes = { Manual = { colors.black, colors.yellow, "MANUAL" }, AutoNBT = { colors.black, colors.orange, "AUTO NBT" }, Auto8k = { colors.black, colors.purple, "AUTO 8K" } } local cfg = modes[mode] or { colors.blue, colors.gray, mode:upper() } if info.status == "running" and mode ~= "Auto8k" then local startX = 2 local startY = halfH + 2 target.setCursorPos(startX, startY) target.setBackgroundColor(cfg[1]) target.setTextColor(cfg[2]) target.write("\149<<\149<>") target.setBackgroundColor(cfg[2]) target.setTextColor(cfg[1]) target.write("\149") target.setBackgroundColor(cfg[1]) target.setTextColor(cfg[2]) target.write(">>") target.setBackgroundColor(cfg[2]) target.setTextColor(cfg[1]) target.write("\149") end drawButton(target, 2, halfH + 3, 10, 2, cfg[3], cfg[1], cfg[2], cfg[2]) local chargeTxt = (info.status:find("warming") or info.status:find("online")) and "CHARGING" or "ONLINE" local isCold = info.status == "cold" drawButton(target, 2, halfH + 6, 10, 2, isCold and "CHARGE" or chargeTxt, colors.black, isCold and colors.green or colors.gray, colors.green) drawButton(target, 2, halfH + 9, 10, 2, "SHUTDOWN", colors.black, isCold and colors.gray or colors.red, colors.red) target.setBackgroundColor(colors.black) local curNBT = info.fuelConversionRate / 1000 local netGen = info.generationRate - (inGate and inGate.getSignalLowFlow() or 0) local eff = (curNBT > 0) and (info.generationRate / curNBT) or 0 local function writeStat(y, label, val, col, val2, col2) target.setCursorPos(infoX, infoY + y) target.setTextColor(colors.lightGray) target.write(label) target.setTextColor(col) target.write(val .. " ") if val2 then target.setTextColor(col2) target.write(val2) end target.write(string.rep(" ", 5)) end writeStat(0, "GENERATION:", formatRF(netGen) .. "/t", colors.green) writeStat(1, "TOTAL:", formatRF(runningTotalRF), colors.orange) writeStat(2, "RATE:", formatRF(eff) .. "/nb", colors.yellow) writeStat(3, "I/O ", "IN:" .. formatRF(inGate and inGate.getSignalLowFlow() or 0), colors.green, " OUT:" .. formatRF(outGate and outGate.getSignalLowFlow() or 0), colors.red) local tCol = mode == "Manual" and colors.lightGray or (safety.isDangerSat and colors.red or (safety.isLowSat and colors.orange or colors.purple)) local dispNBT = (mode ~= "Manual" and safety.isDangerSat) and targetNBT * 0.7 or targetNBT writeStat(4, "TARGET: ", string.format("%.3fnb/t", dispNBT), tCol, string.format("%d%sC", 8000, deg), getTempColor(8000)) writeStat(5, "CURRENT:", string.format("%.3fnb/t", curNBT), colors.orange, string.format("%d%sC", info.temperature, deg), getTempColor(info.temperature)) local tLabel, tStr, tCol = "ETA: ", "Stable", colors.yellow if info.status == "running" or info.status == "online" then if #fuelBuffer > 0 then local avg = 0 for _, v in ipairs(fuelBuffer) do avg = avg + v end avg = (avg / #fuelBuffer / 1000000) * 20 if avg > 0 then local targetAmt = info.maxFuelConversion * (fPct > 5 and 0.95 or 1.0) tLabel = fPct > 5 and "5% Fuel ETA: " or "0% Fuel ETA: " tStr = formatTime((targetAmt - info.fuelConversion) / avg) end end elseif info.temperature > 25 then tLabel, tCol = "COOL DOWN: ", colors.lightBlue if #tempBuffer >= 20 then local rate = (tempBuffer[1] - tempBuffer[#tempBuffer]) / #tempBuffer tStr = rate > 0.01 and formatTime((info.temperature - 25) / rate) or "Stable" end end _G.info.timeStr = tStr writeStat(6, tLabel, tStr, tCol) target.setTextColor(colors.purple) for i = halfH + 1, h do target.setCursorPos(13, i) target.write("\149") end target.setCursorPos(1, halfH) target.write(string.rep("\140", w)) target.setCursorPos(13, halfH) target.write("\156") end local function triggerMeltdownAlarm(reason, temp, shield, monitor) triggerEmergencyRedstone() local targets = {term} if monitor then setMonitorScale(monitor) targets[#targets + 1] = monitor end for _, t in ipairs(targets) do t.setBackgroundColor(colors.red) t.setTextColor(colors.white) t.clear() local lines = { " !!! EMERGENCY MELTDOWN DETECTED !!!", " Reason: " .. reason, " Last Temp: " .. math.floor(temp) .. "C", " Shield: " .. math.floor(shield) .. "%", " Redstone Signal: LOCKED ON" } for i, line in ipairs(lines) do t.setCursorPos(1, i + 1) t.write(line) end end while true do os.pullEvent() end end local function handleInteraction(event, p1, p2, p3, info, reactor, inGate, outGate, target, cfg) local w, h = target.getSize() local mx, my = p2, p3 if event == "mouse_click" or event == "monitor_touch" then if my == 1 then local versionTag = "Dreactor v" .. _G.OS_VERSION local startX = math.floor(w/2 - #versionTag/2) if mx >= startX and mx <= startX + #versionTag then return menuMode == "MAIN" and "OPENED_MENU" or "EXIT_MENU" end end if menuMode == "OPTIONS" then if mx >= w - 11 and mx <= w - 1 and my >= h - 4 and my <= h - 2 then return "EXIT_MENU" end if mx >= w - 11 and mx <= w - 1 and my >= 3 and my <= 5 then openUpdateMenu(target) return "REFRESH" end if mx >= w - 11 and mx <= w - 1 and my >= 7 and my <= 9 then if findWirelessModem() then cfg.tendril = not cfg.tendril saveConfig(cfg.mode, runningTotalRF, targetNBT, shieldTarget, cfg.tendril, cfg.tendrilcode) end return "REFRESH" end if mx >= w - 11 and mx <= w - 1 and my >= 11 and my <= 13 then numpad.active = true numpad.value = "" numpad.callback = function(val) local num = tonumber(val) if num then cfg.tendrilcode = num saveConfig(cfg.mode, runningTotalRF, targetNBT, shieldTarget, cfg.tendril, cfg.tendrilcode) numpad.active = false needsFullRedraw = true else numpad.error = true end end return "REFRESH" end local sides = {"top", "right", "front", "left", "back", "bottom"} local grid = {{nil, 1, nil}, {4, 3, 2}, {5, 6, nil}} for r = 1, 3 do for c = 1, 3 do local idx = grid[r][c] if idx then local gridX = 1 + (c-1)*8 local gridY = 2 + (r-1)*6 if mx >= gridX and mx < gridX + 6 and my >= gridY and my < gridY + 5 then local s = sides[idx] if sideConfigs[s] == "none" then sideConfigs[s] = "meltdown" elseif sideConfigs[s] == "meltdown" then sideConfigs[s] = "fuel" else sideConfigs[s] = "none" end saveConfig(cfg.mode, runningTotalRF, targetNBT, shieldTarget, cfg.tendril, cfg.tendrilcode) return "REFRESH" end end end end else local startX, barW, startY = 2, w - 8, 2 if my == startY + 1 and mx >= startX and mx <= (startX + barW) then local rawPercent = ((mx - startX) / (barW - 1)) * 100 local newTarget = math.floor((rawPercent / 5) + 0.5) * 5 if newTarget < 5 then shieldTarget = 5 elseif newTarget > 95 then shieldTarget = 95 else shieldTarget = newTarget end saveConfig(cfg.mode, runningTotalRF, targetNBT, shieldTarget, cfg.tendril, cfg.tendrilcode) return "REFRESH" end local halfH = math.floor(h / 2) local btnW, btnH = 10, 2 local modeY = halfH + 3 local chargeY = modeY + btnH + 1 local shutdownY = chargeY + btnH + 1 local adjY = modeY - 1 if my == adjY then local change = 0 if mx >= 2 and mx <= 4 then change = -1.0 elseif mx >= 5 and mx <= 6 then change = -0.1 elseif mx >= 7 and mx <= 8 then change = 0.1 elseif mx >= 9 and mx <= 11 then change = 1.0 end if change ~= 0 then if cfg.mode == "Manual" then if outGate then local currentOut = outGate.getSignalLowFlow() local multiplier = (math.abs(change) == 1 and 1000000 or 100000) local newVal = math.max(0, currentOut + (change > 0 and multiplier or -multiplier)) outGate.setSignalLowFlow(newVal) end elseif cfg.mode == "AutoNBT" then targetNBT = math.max(0.1, targetNBT + change) saveConfig(cfg.mode, runningTotalRF, targetNBT, shieldTarget, cfg.tendril, cfg.tendrilcode) end return "REFRESH" end end if mx >= 2 and mx <= 2 + btnW then if my >= modeY and my < (modeY + btnH) then local modes = {"Manual", "AutoNBT", "Auto8k"} local nextM = "Manual" for i, m in ipairs(modes) do if m == cfg.mode then nextM = modes[(i % 3) + 1] break end end cfg.mode = nextM saveConfig(cfg.mode, runningTotalRF, targetNBT, shieldTarget, cfg.tendril, cfg.tendrilcode) return "REFRESH" elseif my >= chargeY and my < (chargeY + btnH) then if info.status == "cold" then Shuttingdown = false inGate.setSignalLowFlow(900000) outGate.setSignalLowFlow(0) reactor.chargeReactor() elseif info.status == "warming_up" and info.temperature >= 2000 then inGate.setSignalLowFlow(900000) outGate.setSignalLowFlow(0) reactor.activateReactor() end return "REFRESH" elseif my >= shutdownY and my < (shutdownY + btnH) then if info.status ~= "cold" then inGate.setSignalLowFlow(900000) outGate.setSignalLowFlow(0) Shuttingdown = true reactor.stopReactor() end return "REFRESH" end end end end return nil end local widgets = { reactor_info = { size = {2, 2}, code = [[ local prefix = (identity or "Dreactor") .. "_" local info = _G[prefix .. "data"] or _G["data"] local rID = _G[prefix .. "id"] or _G["id"] or "???" local net = _G[prefix .. "net"] or _G["net"] or 0 local w, h = term.getSize() term.setBackgroundColor(colors.black) term.clear() term.setCursorPos(1, 1) term.setBackgroundColor(colors.purple) term.setTextColor(colors.black) local head = not info and " Connecting..." or " Dreactor:" .. rID term.write(head .. string.rep(" ", w - #head)) for i = 2, h - 1 do term.setBackgroundColor(colors.black) term.setTextColor(colors.purple) term.setCursorPos(1, i) term.write(string.char(149)) -- Left term.setCursorPos(w, i) term.setBackgroundColor(colors.purple) -- Right (Thin border) term.setTextColor(colors.black) term.write(string.char(149)) end term.setCursorPos(1, h) term.setBackgroundColor(colors.purple) term.write(string.char(138) .. string.rep(string.char(143), w - 2) .. string.char(133)) if not info then return end term.setBackgroundColor(colors.black) term.setCursorPos(2, 2) local status = (info.status or "offline"):gsub("_", " ") term.setTextColor(status == "running" and colors.lime or colors.yellow) term.write(status:upper()) local function bar(y, label, cur, max, col) local nCur = tonumber(cur) or 0 local nMax = tonumber(max) or 1 if nMax <= 0 then nMax = 1 end local pct = math.min(math.max(nCur / nMax, 0), 1) local fill = math.floor(pct * (w - 2)) term.setCursorPos(2, y) term.setBackgroundColor(colors.gray) term.write(string.rep(" ", w - 2)) term.setCursorPos(2, y) term.setBackgroundColor(col) term.write(string.rep(" ", fill)) for i = 1, #label do term.setCursorPos(2 + i, y) term.setBackgroundColor(i <= fill and col or colors.gray) term.setTextColor(colors.black) term.write(label:sub(i, i)) end end bar(3, "Shield", info.fieldStrength, info.maxFieldStrength, colors.blue) bar(4, "Saturation", info.energySaturation, info.maxEnergySaturation, colors.green) local fuelPct = 100 - ((tonumber(info.fuelConversion) or 0) / (tonumber(info.maxFuelConversion) or 1) * 100) bar(5, "Fuel", fuelPct, 100, colors.orange) term.setBackgroundColor(colors.black) term.setTextColor(colors.lightGray) local gen = net >= 1e6 and (string.format("%.1fM", net/1e6)) or (math.floor(net/1e3) .. "K") local lines = {"Temp: "..math.floor(info.temperature or 0).."C", "Gen: "..gen, " Time left", " "..(info.timeStr or _G.lastKnownETA or "CALC...")} if info.timeStr then _G.lastKnownETA = info.timeStr end for i, text in ipairs(lines) do term.setCursorPos(2, 5 + i) term.write(text) end ]] }, react_btn = { size = {2, 1}, code = [[ local id = identity or "Dreactor" local prefix = id .. "_" local info = _G[prefix .. "data"] local click = lastClick local currentMode = _G[prefix .. "mode"] or "Manual" local modes = {"Manual", "AutoNBT", "Auto8k"} local mText, mCol = "NO DATA", colors.lightGray if info then if currentMode == "Auto8k" then mText, mCol = "AUTO 8K", colors.purple elseif currentMode == "AutoNBT" then mText, mCol = "AUTO NBT", colors.orange else mText, mCol = "MANUAL", colors.yellow end end local pText, pCol, pCmd = "OFFLINE", colors.lightGray, nil if info then local status = info.status or "cold" if status == "cold" then pText, pCol, pCmd = "CHARGE", colors.green, "Shuttingdown = false; inGate.setSignalLowFlow(900000); outGate.setSignalLowFlow(0); reactor.chargeReactor()" elseif status == "warming_up" or status == "stopping" then pText, pCol, pCmd = "ACTIVATE", colors.green, "Shuttingdown = false; inGate.setSignalLowFlow(900000); outGate.setSignalLowFlow(0); reactor.activateReactor()" else pText, pCol, pCmd = "SHUTDOWN", colors.red, "inGate.setSignalLowFlow(900000); outGate.setSignalLowFlow(0); Shuttingdown = true; reactor.stopReactor()" end end local function drawWidgetButton(bx, by, bw, bh, text, stateColor) local label = " " .. text .. " " local padding = math.floor((bw - #label) / 2) local labelStr = string.rep(" ", padding) .. label .. string.rep(" ", bw - #label - padding) for i = 0, bh - 1 do term.setCursorPos(bx, by + i) term.setBackgroundColor(colors.black) term.setTextColor(stateColor) if i == 0 then term.write(string.char(151) .. string.rep(string.char(131), bw - 2)) else term.write(string.char(149) .. labelStr:sub(2, -2)) end term.setBackgroundColor(stateColor) term.setTextColor(colors.black) term.write(i == 0 and string.char(148) or string.char(149)) end end term.setBackgroundColor(colors.black) term.clear() drawWidgetButton(1, 1, 14, 2, mText, mCol) drawWidgetButton(1, 4, 14, 2, pText, pCol) if click and info then local lx, ly = click.x - x + 1, click.y - y + 1 if lx >= 1 and lx <= 14 then if ly >= 1 and ly <= 2 then local nextM = "Manual" for i, m in ipairs(modes) do if m == currentMode then nextM = modes[(i % #modes) + 1] break end end sendCallback("mode", nextM) elseif ly >= 4 and ly <= 5 and pCmd then sendCallback("run_command", pCmd) end end _G.lastClick = nil end ]] } } local function startTendril(cfg, widgets) local modem = peripheral.find("modem", function(_, v) return v.isWireless() end) if not modem or not cfg.tendril or not cfg.tendrilcode then return nil end local hostConfig = { state = { running = true, data = _G.info or {}, id = os.getComputerID(), mode = cfg.mode or "Manual", net = 0 }, mapping = { id = "id", mode = "mode", data = "data", net = "net" }, settings = { paused = false, intervals = { id = 60, mode = 1, data = 2, net = 5 } } } _G.Dreactor_host_config = hostConfig return tendril.init("Dreactor", widgets, tostring(cfg.tendrilcode), hostConfig) end local function reactorLoop(cfg) local lastTime = os.epoch("utc") while true do if _G.Dreactor_host_config and _G.Dreactor_host_config.state then cfg.mode = _G.Dreactor_host_config.state.mode end local reactors = findPeripherals("draconic_reactor") local gates = findPeripherals("flow_gate") local inGate, outGate = nil, nil cfg.targetNBT = targetNBT cfg.shieldTarget = shieldTarget for _, g in ipairs(gates) do if g.getSignalHighFlow() == 1 then inGate = g else outGate = g end end if #reactors > 0 and inGate and outGate then _G.reactor = reactors[1] _G.inGate = inGate _G.outGate = outGate balanceReactorInput(reactor, inGate, shieldTarget) local info = reactor.getReactorInfo() if info then _G.info = info updateRedstoneSignals(info) local safety = getReactorSafety(info) local isBeyondHope = info.status == "beyond_hope" if (safety.isCritical or isBeyondHope) and not criticalLogged then local reason = isBeyondHope and "STATUS: BEYOND HOPE" or "REACTOR CRITICAL STATE DETECTED" logReactorEvent(info, targetNBT, "TERMINAL: " .. reason) local monitor = findPeripherals("monitor")[1] triggerMeltdownAlarm(reason, info.temperature, safety.shield, monitor) criticalLogged = true elseif not (safety.isCritical or isBeyondHope) then criticalLogged = false end local fuelRemainingPct = 100 - ((info.fuelConversion / info.maxFuelConversion) * 100) if fuelRemainingPct <= 5 and not refueling and info.status ~= "cold" then refueling = true Shuttingdown = true reactor.stopReactor() inGate.setSignalLowFlow(1200000) outGate.setSignalLowFlow(0) logReactorEvent(info, targetNBT, "LOW FUEL: SHUTTING DOWN") end if refueling and fuelRemainingPct >= 95 then refueling = false if info.status == "cold" then reactor.chargeReactor() logReactorEvent(info, targetNBT, "FUEL DETECTED: AUTO-STARTING") end end if info.temperature >= 10000 and info.status ~= "cold" then reactor.stopReactor() inGate.setSignalLowFlow(4000000) outGate.setSignalLowFlow(0) Shuttingdown = true end if Shuttingdown then if info.temperature <= 200 then inGate.setSignalLowFlow(100) elseif info.temperature > 200 and info.temperature < 8000 then inGate.setSignalLowFlow(900000) end if info.status == "cold" then Shuttingdown = false end end if os.clock() - lastHourLog >= 3600 then logReactorEvent(info, targetNBT) lastHourLog = os.clock() end if safety.shield <= 10 or safety.saturation <= 10 then forceRecovery = true elseif safety.shield >= 30 and safety.saturation >= 30 then forceRecovery = false end if info.status == "warming_up" then inGate.setSignalLowFlow(900000) outGate.setSignalLowFlow(0) if info.temperature >= 2000 then reactor.activateReactor() end end if info.status == "running" or info.status == "online" then if forceRecovery then outGate.setSignalLowFlow(0) elseif cfg.mode == "Auto8k" then targetNBT = adjustFuelForTempOnly(info, targetNBT) adjustFuelRate(info, outGate, targetNBT) elseif cfg.mode == "AutoNBT" then adjustFuelRate(info, outGate, targetNBT) end else outGate.setSignalLowFlow(0) end table.insert(fuelBuffer, info.fuelConversionRate) if #fuelBuffer > 60 then table.remove(fuelBuffer, 1) end table.insert(tempBuffer, info.temperature) if #tempBuffer > 60 then table.remove(tempBuffer, 1) end local curEpoch = os.epoch("utc") local elapsedSeconds = (curEpoch - lastTime) / 1000 if info.status == "running" or info.status == "online" then local ticksPassed = elapsedSeconds * 20 runningTotalRF = runningTotalRF + (info.generationRate * ticksPassed) elseif fuelRemainingPct >= 99 then runningTotalRF = 0 end lastTime = curEpoch end if _G.Dreactor_host_config then _G.Dreactor_host_config.state.mode = cfg.mode end end sleep(0.5) end end local function uiLoop(cfg) local lastVisibleState = "" local lastMenuMode = "" while true do local event, p1, p2, p3 = os.pullEvent() local reactors = findPeripherals("draconic_reactor") local gates = findPeripherals("flow_gate") local monitor = findPeripherals("monitor")[1] if event == "timer" and p1 == syncTimer then syncTimer = os.startTimer(0.5) end local inGate, outGate = nil, nil for _, g in ipairs(gates) do if g.getSignalHighFlow() == 1 then inGate = g else outGate = g end end local reactor = reactors[1] local info = reactor and reactor.getReactorInfo() local isSetup = not (info and inGate and outGate) local currentState = string.format("%s%s%.0f", menuMode, (info and info.status or "OFFLINE"), (info and info.temperature or 0)) if currentState ~= lastVisibleState or event == "mouse_click" or event == "monitor_touch" or event == "timer" or needsFullRedraw then if menuMode ~= lastMenuMode or needsFullRedraw then term.setBackgroundColor(colors.black) term.clear() if monitor then monitor.setBackgroundColor(colors.black) monitor.clear() end lastMenuMode = menuMode needsFullRedraw = false end lastVisibleState = currentState local targets = monitor and {term, monitor} or {term} for _, t in ipairs(targets) do t.setCursorPos(1, 1) if not isSetup then if menuMode == "OPTIONS" then drawOptionsMenu(t, reactors, inGate, outGate, cfg, info) else drawMainUI(t, info, cfg.mode, inGate, outGate) end else drawSetupUI(t, reactors, gates, info) end if numpad.active then inputNumpad(t, 0, 0, false) end end end end end local function inputLoop(cfg) while true do local event, p1, p2, p3 = os.pullEvent() if event == "mouse_click" or event == "monitor_touch" or event == "key" or event == "char" then local reactors = findPeripherals("draconic_reactor") local gates = findPeripherals("flow_gate") local monitor = findPeripherals("monitor")[1] local target = (event == "monitor_touch") and monitor or term local handled = false if numpad.active then if event == "char" or event == "key" then handled = inputNumpad(target, 0, 0, false, event, p1) elseif event == "mouse_click" or event == "monitor_touch" then handled = inputNumpad(target, p2, p3, true) end if handled then needsFullRedraw = true end end if not handled then local inGate, outGate = nil, nil for _, g in ipairs(gates) do if g.getSignalHighFlow() == 1 then inGate = g else outGate = g end end local reactor = reactors[1] local info = reactor and reactor.getReactorInfo() if info and reactor and inGate and outGate then local result = handleInteraction(event, p1, p2, p3, info, reactor, inGate, outGate, target, cfg) if result == "OPENED_MENU" or result == "EXIT_MENU" or result == "REFRESH" then if result == "OPENED_MENU" then menuMode = "OPTIONS" elseif result == "EXIT_MENU" then menuMode = "MAIN" end needsFullRedraw = true end end end end end end local function main() cfg = loadConfig() runningTotalRF = cfg.totalRF or 0 local service = startTendril(cfg, widgets) parallel.waitForAny( function() reactorLoop(cfg) end, function() uiLoop(cfg) end, function() inputLoop(cfg) end, function() while true do if service and _G.Dreactor_host_config then local st = _G.Dreactor_host_config.state st.mode = cfg.mode st.data = _G.info or {} local input = (_G.inGate and _G.inGate.getSignalLowFlow()) or 0 local gen = (_G.info and _G.info.generationRate) or 0 st.net = gen - input os.queueEvent("ui_refresh") end sleep(0.5) end end, function() if service then service.run() else while true do sleep(100) end end end ) end main()