diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..995a49e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +reactorConfigSerialized.txt +commit.txt +/overrides +/defaults +/state +.DS_Store \ No newline at end of file diff --git a/install.lua b/install.lua new file mode 100644 index 0000000..90761e4 --- /dev/null +++ b/install.lua @@ -0,0 +1,40 @@ +-- This file is in pastebin + +local GITHUB_CONSTANTS = { + OWNER = "Kasra-G", + REPO = "ReactorController", + BRANCH = "development", +} + +local function downloadGitHubFileByPath(filepath, tempFoldername) + if tempFoldername == nil then + tempFoldername = "" + end + + local endpoint = "https://raw.githubusercontent.com/"..GITHUB_CONSTANTS.OWNER.."/"..GITHUB_CONSTANTS.REPO.."/refs/heads/"..GITHUB_CONSTANTS.BRANCH.."/"..filepath + local response = http.get(endpoint) + local contents = response.readAll() + local file = fs.open(fs.combine(tempFoldername, filepath), "w") + file.write(contents) + file.close() + print("File", filepath, "downloaded!") +end + +--- Download the update script and reboot +local function install() + local updateScriptPath = "src/scripts/update.lua" + local success, err = pcall(function() downloadGitHubFileByPath(updateScriptPath) end) + if not success then + error("Failed to install the script with error", err) + end + shell.run(updateScriptPath) + local success = _G.UpdateScript.performUpdate() + if not success then + error("Failed to install the script! Do you have internet access?") + end + print("Files downloaded successfully.") + sleep(1) + os.reboot() +end + +install() \ No newline at end of file diff --git a/installer.lua b/installer.lua deleted file mode 100644 index f67c17b..0000000 --- a/installer.lua +++ /dev/null @@ -1,13 +0,0 @@ ---pastebin run kSkwEchg ---Github: https://github.com/Kasra-G/ReactorController#readme - ---Overwrite startup file -local file = fs.open("startup", "w") -file.writeLine("shell.run(\"update_reactor.lua\")") -file.writeLine("while (true) do") -file.writeLine(" shell.run(\"reactorController.lua\")") -file.writeLine(" sleep(2)") -file.writeLine("end") -file.close() -shell.run("pastebin get w6vVtrLb update_reactor.lua") -shell.run("reboot") diff --git a/reactorController.lua b/reactorController.lua deleted file mode 100644 index 503ae74..0000000 --- a/reactorController.lua +++ /dev/null @@ -1,946 +0,0 @@ -local version = "0.51" -local tag = "reactorConfig" ---[[ -Program made by DrunkenKas - See github: https://github.com/Kasra-G/ReactorController/#readme - -The MIT License (MIT) - -Copyright (c) 2021 Kasra Ghaffari - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -]] - -dofile("/usr/apis/touchpoint.lua") - -local reactorVersion, reactor -local mon, monSide -local sizex, sizey, dim, oo, offy -local btnOn, btnOff, invalidDim -local minb, maxb -local rod, rfLost -local storedLastTick, storedThisTick, lastRFT = 0,0,0 -local fuelTemp, caseTemp, fuelUsage, waste, capacity = 0,0,0,0,1 -local t -local displayingGraphMenu = false - -local secondsToAverage = 2 - -local averageStoredThisTick = 0 -local averageLastRFT = 0 -local averageRod = 0 -local averageFuelUsage = 0 -local averageWaste = 0 -local averageFuelTemp = 0 -local averageCaseTemp = 0 -local averageRfLost = 0 - --- table of which graphs to draw -local graphsToDraw = {} - --- table of all the graphs -local graphs = -{ - "Energy Buffer", - "Control Level", - "Temperatures", -} - --- marks the offsets for each graph position --- { XOffset, } -local XOffs = -{ - { 4, true}, - {27, true}, - {50, true}, - {73, true}, - {96, true}, -} - --- Draw a box with no fill -local function drawBox(size, xoff, yoff, color) - if (monSide == nil) then - return - end - local x,y = mon.getCursorPos() - mon.setBackgroundColor(color) - local horizLine = string.rep(" ", size[1]) - mon.setCursorPos(xoff + 1, yoff + 1) - mon.write(horizLine) - mon.setCursorPos(xoff + 1, yoff + size[2]) - mon.write(horizLine) - - -- Draw vertical lines - for i=0, size[2] - 1 do - mon.setCursorPos(xoff + 1, yoff + i + 1) - mon.write(" ") - mon.setCursorPos(xoff + size[1], yoff + i +1) - mon.write(" ") - end - mon.setCursorPos(x,y) - mon.setBackgroundColor(colors.black) -end - ---Draw a filled box -local function drawFilledBox(size, xoff, yoff, colorOut, colorIn) - if (monSide == nil) then - return - end - local horizLine = string.rep(" ", size[1] - 2) - drawBox(size, xoff, yoff, colorOut) - local x,y = mon.getCursorPos() - mon.setBackgroundColor(colorIn) - for i=2, size[2] - 1 do - mon.setCursorPos(xoff + 2, yoff + i) - mon.write(horizLine) - end - mon.setBackgroundColor(colors.black) - mon.setCursorPos(x,y) -end - ---Draws text on the screen -local function drawText(text, x1, y1, backColor, textColor) - if (monSide == nil) then - return - end - local x, y = mon.getCursorPos() - mon.setCursorPos(x1, y1) - mon.setBackgroundColor(backColor) - mon.setTextColor(textColor) - mon.write(text) - mon.setTextColor(colors.white) - mon.setBackgroundColor(colors.black) - mon.setCursorPos(x,y) -end - ---Helper method for adding buttons -local function addButt(name, callBack, size, xoff, yoff, color1, color2) - t:add(name, callBack, - xoff + 1, yoff + 1, - size[1] + xoff, size[2] + yoff, - color1, color2) -end - -local function minAdd10() - minb = math.min(maxb - 10, minb + 10) -end -local function minSub10() - minb = math.max(0, minb - 10) -end -local function maxAdd10() - maxb = math.min(100, maxb + 10) -end -local function maxSub10() - maxb = math.max(minb + 10, maxb - 10) -end - -local function turnOff() - if (btnOn) then - t:toggleButton("Off") - t:toggleButton("On") - btnOff = true - btnOn = false - reactor.setActive(false) - end -end - -local function turnOn() - if (btnOff) then - t:toggleButton("Off") - t:toggleButton("On") - btnOff = false - btnOn = true - reactor.setActive(true) - end -end - ---adds buttons -local function addButtons() - if (sizey == 24) then - oo = 1 - end - addButt("On", turnOn, {8, 3}, dim + 7, 3 + oo, - colors.red, colors.lime) - addButt("Off", turnOff, {8, 3}, dim + 19, 3 + oo, - colors.red, colors.lime) - if (btnOn) then - t:toggleButton("On", true) - else - t:toggleButton("Off", true) - end - if (sizey > 24) then - addButt("+ 10", minAdd10, {8, 3}, dim + 7, 14 + oo, - colors.purple, colors.pink) - addButt(" + 10 ", maxAdd10, {8, 3}, dim + 19, 14 + oo, - colors.magenta, colors.pink) - addButt("- 10", minSub10, {8, 3}, dim + 7, 18 + oo, - colors.purple, colors.pink) - addButt(" - 10 ", maxSub10, {8, 3}, dim + 19, 18 + oo, - colors.magenta, colors.pink) - end -end - ---Resets the monitor -local function resetMon() - if (monSide == nil) then - return - end - mon.setBackgroundColor(colors.black) - mon.clear() - mon.setTextScale(0.5) - mon.setCursorPos(1,1) -end - -local function getPercPower() - return averageStoredThisTick / capacity * 100 -end - -local function rnd(num, dig) - return math.floor(10 ^ dig * num) / (10 ^ dig) -end - -local function getEfficiency() - return averageLastRFT / averageFuelUsage -end - -local function format(num) - if (num >= 1000000000) then - return string.format("%7.3f G", num / 1000000000) - elseif (num >= 1000000) then - return string.format("%7.3f M", num / 1000000) - elseif (num >= 1000) then - return string.format("%7.3f K", num / 1000) - elseif (num >= 1) then - return string.format("%7.3f ", num) - elseif (num >= .001) then - return string.format("%7.3f m", num * 1000) - elseif (num >= .000001) then - return string.format("%7.3f u", num * 1000000) - else - return string.format("%7.3f ", 0) - end -end - - -local function getAvailableXOff() - for i,v in pairs(XOffs) do - if (v[2] and v[1] < dim) then - v[2] = false - return v[1] - end - end - return -1 -end - -local function getXOff(num) - for i,v in pairs(XOffs) do - if (v[1] == num) then - return v - end - end - return nil -end - -local function enableGraph(name) - if (graphsToDraw[name] ~= nil) then - return - end - local e = getAvailableXOff() - if (e ~= -1) then - graphsToDraw[name] = e - if (displayingGraphMenu) then - t:toggleButton(name) - end - end -end - -local function disableGraph(name) - if (graphsToDraw[name] == nil) then - return - end - if (displayingGraphMenu) then - t:toggleButton(name) - end - getXOff(graphsToDraw[name])[2] = true - graphsToDraw[name] = nil -end - -local function toggleGraph(name) - if (graphsToDraw[name] == nil) then - enableGraph(name) - else - disableGraph(name) - end -end - -local function addGraphButtons() - offy = oo - 14 - for i,v in pairs(graphs) do - addButt(v, function() toggleGraph(v) end, {20, 3}, - dim + 7, offy + i * 3 - 1, - colors.red, colors.lime) - if (graphsToDraw[v] ~= nil) then - t:toggleButton(v, true) - end - end -end - -local function drawGraphButtons() - drawBox({sizex - dim - 3, oo - offy - 1}, - dim + 2, offy, colors.orange) - drawText(" Graph Controls ", - dim + 7, offy + 1, - colors.black, colors.orange) -end - -local function drawEnergyBuffer(xoff) - local srf = sizey - 9 - local off = xoff - local right = off + 19 < dim - local poff = right and off + 15 or off - 6 - - drawBox({15, srf + 2}, off - 1, 4, colors.gray) - local pwr = math.floor(getPercPower() / 100 - * (srf)) - drawFilledBox({13, srf}, off, 5, - colors.red, colors.red) - local rndpw = rnd(getPercPower(), 2) - local color = (rndpw < maxb and rndpw > minb) and colors.green - or (rndpw >= maxb and colors.orange or colors.blue) - if (pwr > 0) then - drawFilledBox({13, pwr + 1}, off, srf + 4 - pwr, - color, color) - end - --drawPoint(off + 14, srf + 5 - pwr, pwr > 0 and color or colors.red) - drawText(string.format(right and "%.2f%%" or "%5.2f%%", rndpw), poff, srf + 5 - pwr, - colors.black, color) - drawText("Energy Buffer", off + 1, 4, - colors.black, colors.orange) - drawText(format(averageStoredThisTick).."RF", off + 1, srf + 5 - pwr, - pwr > 0 and color or colors.red, colors.black) -end - -local function drawControlLevel(xoff) - local srf = sizey - 9 - local off = xoff - drawBox({15, srf + 2}, off - 1, 4, colors.gray) - drawFilledBox({13, srf}, off, 5, - colors.yellow, colors.yellow) - local rodTr = math.floor(averageRod / 100 - * (srf)) - drawText("Control Level", off + 1, 4, - colors.black, colors.orange) - if (rodTr > 0) then - drawFilledBox({9, rodTr}, off + 2, 5, - colors.white, colors.white) - end - drawText(string.format("%6.2f%%", averageRod), off + 4, rodTr > 0 and rodTr + 5 or 6, - rodTr > 0 and colors.white or colors.yellow, colors.black) - -end - -local function drawTemperatures(xoff) - local srf = sizey - 9 - local off = xoff - drawBox({15, srf + 2}, off, 4, colors.gray) - --drawFilledBox({12, srf}, off, 5, - -- colors.red, colors.red) - - local tempUnit = (reactorVersion == "Bigger Reactors") and "K" or "C" - local tempFormat = "%4s"..tempUnit - - local fuelRnd = math.floor(averageFuelTemp) - local caseRnd = math.floor(averageCaseTemp) - local fuelTr = math.floor(fuelRnd / 2000 - * (srf)) - local caseTr = math.floor(caseRnd / 2000 - * (srf)) - drawText(" Case ", off + 2, 5, - colors.gray, colors.lightBlue) - drawText(" Fuel ", off + 9, 5, - colors.gray, colors.magenta) - if (fuelTr > 0) then - fuelTr = math.min(fuelTr, srf) - drawFilledBox({6, fuelTr}, off + 8, srf + 5 - fuelTr, - colors.magenta, colors.magenta) - - drawText(string.format(tempFormat, fuelRnd..""), - off + 10, srf + 6 - fuelTr, - colors.magenta, colors.black) - else - drawText(string.format(tempFormat, fuelRnd..""), - off + 10, srf + 5, - colors.black, colors.magenta) - end - - if (caseTr > 0) then - caseTr = math.min(caseTr, srf) - drawFilledBox({6, caseTr}, off + 1, srf + 5 - caseTr, - colors.lightBlue, colors.lightBlue) - drawText(string.format(tempFormat, caseRnd..""), - off + 3, srf + 6 - caseTr, - colors.lightBlue, colors.black) - else - drawText(string.format(tempFormat, caseRnd..""), - off + 3, srf + 5, - colors.black, colors.lightBlue) - end - - drawText("Temperatures", off + 2, 4, - colors.black, colors.orange) - drawBox({1, srf}, off + 7, 5, - colors.gray) -end - -local function drawGraph(name, offset) - if (name == "Energy Buffer") then - drawEnergyBuffer(offset) - elseif (name == "Control Level") then - drawControlLevel(offset) - elseif (name == "Temperatures") then - drawTemperatures(offset) - end -end - -local function drawGraphs() - for i,v in pairs(graphsToDraw) do - if (v + 15 < dim) then - drawGraph(i,v) - end - end -end - -local function drawStatus() - if (dim <= -1) then - return - end - drawBox({dim, sizey - 2}, - 1, 1, colors.lightBlue) - drawText(" Reactor Graphs ", dim - 18, 2, - colors.black, colors.lightBlue) - drawGraphs() -end - -local function drawControls() - if (sizey == 24) then - drawBox({sizex - dim - 3, 9}, dim + 2, oo, - colors.cyan) - drawText(" Reactor Controls ", dim + 7, oo + 1, - colors.black, colors.cyan) - drawText("Reactor "..(btnOn and "Online" or "Offline"), - dim + 10, 3 + oo, - colors.black, btnOn and colors.green or colors.red) - return - end - - drawBox({sizex - dim - 3, 23}, dim + 2, oo, - colors.cyan) - drawText(" Reactor Controls ", dim + 7, oo + 1, - colors.black, colors.cyan) - drawFilledBox({20, 3}, dim + 7, 8 + oo, - colors.red, colors.red) - drawFilledBox({(maxb - minb) / 5, 3}, - dim + 7 + minb / 5, 8 + oo, - colors.green, colors.green) - drawText(string.format("%3s", minb.."%"), dim + 6 + minb / 5, 12 + oo, - colors.black, colors.purple) - drawText(maxb.."%", dim + 8 + maxb / 5, 12 + oo, - colors.black, colors.magenta) - drawText("Buffer Target Range", dim + 8, 8 + oo, - colors.black, colors.orange) - drawText("Min", dim + 10, 14 + oo, - colors.black, colors.purple) - drawText("Max", dim + 22, 14 + oo, - colors.black, colors.magenta) - drawText("Reactor ".. (btnOn and "Online" or "Offline"), - dim + 10, 3 + oo, - colors.black, btnOn and colors.green or colors.red) -end - -local function drawStatistics() - local oS = sizey - 13 - drawBox({sizex - dim - 3, sizey - oS - 1}, dim + 2, oS, - colors.blue) - drawText(" Reactor Statistics ", dim + 7, oS + 1, - colors.black, colors.blue) - - --statistics - drawText("Generating : " - ..format(averageLastRFT).."RF/t", dim + 5, oS + 3, - colors.black, colors.green) - drawText("RF Drain " - ..(averageStoredThisTick <= averageLastRFT and "> " or ": ") - ..format(averageRfLost) - .."RF/t", dim + 5, oS + 5, - colors.black, colors.red) - drawText("Efficiency : " - ..format(getEfficiency()).."RF/B", - dim + 5, oS + 7, - colors.black, colors.green) - drawText("Fuel Usage : " - ..format(averageFuelUsage) - .."B/t", dim + 5, oS + 9, - colors.black, colors.green) - drawText("Waste : " - ..string.format("%7d mB", waste), - dim + 5, oS + 11, - colors.black, colors.green) -end - ---Draw a scene -local function drawScene() - if (monSide == nil) then - return - end - if (invalidDim) then - mon.write("Invalid Monitor Dimensions") - return - end - - if (displayingGraphMenu) then - drawGraphButtons() - end - drawControls() - drawStatus() - drawStatistics() - t:draw() -end - ---returns the side that a given peripheral type is connected to -local function getPeripheral(name) - for i,v in pairs(peripheral.getNames()) do - if (peripheral.getType(v) == name) then - return v - end - end - return "" -end - ---Creates all the buttons and determines monitor size -local function initMon() - monSide = getPeripheral("monitor") - if (monSide == nil or monSide == "") then - monSide = nil - return - end - - mon = peripheral.wrap(monSide) - - if mon == nil then - monSide = nil - return - end - - resetMon() - t = touchpoint.new(monSide) - sizex, sizey = mon.getSize() - oo = sizey - 37 - dim = sizex - 33 - - if (sizex == 36) then - dim = -1 - end - if (pcall(addGraphButtons)) then - displayingGraphMenu = true - else - t = touchpoint.new(monSide) - displayingGraphMenu = false - end - local rtn = pcall(addButtons) - if (not rtn) then - t = touchpoint.new(monSide) - invalidDim = true - else - invalidDim = false - end -end - -local function setRods(level) - level = math.max(level, 0) - level = math.min(level, 100) - reactor.setAllControlRodLevels(level) -end - -local function lerp(start, finish, t) - -- Ensure t is in the range [0, 1] - t = math.max(0, math.min(1, t)) - - -- Calculate the linear interpolation - return (1 - t) * start + t * finish -end - --- Function to calculate the average of an array of values -local function calculateAverage(array) - local sum = 0 - for _, value in ipairs(array) do - sum = sum + value - end - return sum / #array -end - --- Define PID controller parameters -local pid = { - setpointRFT = 0, -- Target RFT - setpointRF = 0, -- Target RF - Kp = -.08, -- Proportional gain - Ki = -.0015, -- Integral gain - Kd = -.01, -- Derivative gain - integral = 0, -- Integral term accumulator - lastError = 0, -- Last error for derivative term -} - -local function iteratePID(pid, error) - -- Proportional term - local P = pid.Kp * error - - -- Integral term - pid.integral = pid.integral + pid.Ki * error - pid.integral = math.max(math.min(100, pid.integral), -100) - - -- Derivative term - local derivative = pid.Kd * (error - pid.lastError) - - -- Calculate control rod level - local rodLevel = math.max(math.min(P + pid.integral + derivative, 100), 0) - - -- Update PID controller state - pid.lastError = error - return rodLevel -end - -local function updateRods() - if (not btnOn) then - return - end - local currentRF = storedThisTick - local diffb = maxb - minb - local minRF = minb / 100 * capacity - local diffRF = diffb / 100 * capacity - local diffr = diffb / 100 - local targetRFT = rfLost - local currentRFT = lastRFT - local targetRF = diffRF / 2 + minRF - - pid.setpointRFT = targetRFT - pid.setpointRF = targetRF / capacity * 1000 - - local errorRFT = pid.setpointRFT - currentRFT - local errorRF = pid.setpointRF - currentRF / capacity * 1000 - - local W_RFT = lerp(1, 0, (math.abs(targetRF - currentRF) / capacity / (diffr / 4))) - W_RFT = math.max(math.min(W_RFT, 1), 0) - - local W_RF = (1 - W_RFT) -- Adjust the weight for energy error - - -- Combine the errors with weights - local combinedError = W_RFT * errorRFT + W_RF * errorRF - local error = combinedError - local rftRodLevel = iteratePID(pid, error) - - -- Set control rod levels - setRods(rftRodLevel) -end - --- Saves the configuration of the reactor controller -local function saveToConfig() - local file = fs.open(tag.."Serialized.txt", "w") - local configs = { - maxb = maxb, - minb = minb, - rod = rod, - btnOn = btnOn, - graphsToDraw = graphsToDraw, - XOffs = XOffs, - } - local serialized = textutils.serialize(configs) - file.write(serialized) - file.close() -end - -local storedThisTickValues = {} -local lastRFTValues = {} -local rodValues = {} -local fuelUsageValues = {} -local wasteValues = {} -local fuelTempValues = {} -local caseTempValues = {} -local rfLostValues = {} - -local function updateStats() - storedLastTick = storedThisTick - if (reactorVersion == "Big Reactors") then - storedThisTick = reactor.getEnergyStored() - lastRFT = reactor.getEnergyProducedLastTick() - rod = reactor.getControlRodLevel(0) - fuelUsage = reactor.getFuelConsumedLastTick() / 1000 - waste = reactor.getWasteAmount() - fuelTemp = reactor.getFuelTemperature() - caseTemp = reactor.getCasingTemperature() - -- Big Reactors doesn't give us a way to directly query RF capacity through CC APIs - capacity = math.max(capacity, reactor.getEnergyStored) - elseif (reactorVersion == "Extreme Reactors") then - local bat = reactor.getEnergyStats() - local fuel = reactor.getFuelStats() - - storedThisTick = bat.energyStored - lastRFT = bat.energyProducedLastTick - capacity = bat.energyCapacity - rod = reactor.getControlRodLevel(0) - fuelUsage = fuel.fuelConsumedLastTick / 1000 - waste = reactor.getWasteAmount() - fuelTemp = reactor.getFuelTemperature() - caseTemp = reactor.getCasingTemperature() - elseif (reactorVersion == "Bigger Reactors") then - storedThisTick = reactor.battery().stored() - lastRFT = reactor.battery().producedLastTick() - capacity = reactor.battery().capacity() - rod = reactor.getControlRod(0).level() - fuelUsage = reactor.fuelTank().burnedLastTick() / 1000 - waste = reactor.fuelTank().waste() - fuelTemp = reactor.fuelTemperature() - caseTemp = reactor.casingTemperature() - end - rfLost = lastRFT + storedLastTick - storedThisTick - -- Add the values to the arrays - table.insert(storedThisTickValues, storedThisTick) - table.insert(lastRFTValues, lastRFT) - table.insert(rodValues, rod) - table.insert(fuelUsageValues, fuelUsage) - table.insert(wasteValues, waste) - table.insert(fuelTempValues, fuelTemp) - table.insert(caseTempValues, caseTemp) - table.insert(rfLostValues, rfLost) - - local maxIterations = 20 * secondsToAverage - while #storedThisTickValues > maxIterations do - table.remove(storedThisTickValues, 1) - table.remove(lastRFTValues, 1) - table.remove(rodValues, 1) - table.remove(fuelUsageValues, 1) - table.remove(wasteValues, 1) - table.remove(fuelTempValues, 1) - table.remove(caseTempValues, 1) - table.remove(rfLostValues, 1) - end - - -- Calculate running averages - averageStoredThisTick = calculateAverage(storedThisTickValues) - averageLastRFT = calculateAverage(lastRFTValues) - averageRod = calculateAverage(rodValues) - averageFuelUsage = calculateAverage(fuelUsageValues) - averageWaste = calculateAverage(wasteValues) - averageFuelTemp = calculateAverage(fuelTempValues) - averageCaseTemp = calculateAverage(caseTempValues) - averageRfLost = calculateAverage(rfLostValues) -end - ---Initialize variables from either a config file or the defaults -local function loadFromConfig() - invalidDim = false - local legacyConfigExists = fs.exists(tag..".txt") - local newConfigExists = fs.exists(tag.."Serialized.txt") - if (newConfigExists) then - local file = fs.open(tag.."Serialized.txt", "r") - print("Config file "..tag.."Serialized.txt found! Using configurated settings") - - local serialized = file.readAll() - local deserialized = textutils.unserialise(serialized) - - maxb = deserialized.maxb - minb = deserialized.minb - rod = deserialized.rod - btnOn = deserialized.btnOn - graphsToDraw = deserialized.graphsToDraw - XOffs = deserialized.XOffs - elseif (legacyConfigExists) then - local file = fs.open(tag..".txt", "r") - local calibrated = file.readLine() == "true" - - --read calibration information - if (calibrated) then - _ = tonumber(file.readLine()) - _ = tonumber(file.readLine()) - end - maxb = tonumber(file.readLine()) - minb = tonumber(file.readLine()) - rod = tonumber(file.readLine()) - btnOn = file.readLine() == "true" - - --read Graph data - for i in pairs(XOffs) do - local graph = file.readLine() - local v1 = tonumber(file.readLine()) - local v2 = true - if (graph ~= "nil") then - v2 = false - graphsToDraw[graph] = v1 - end - - XOffs[i] = {v1, v2} - - end - file.close() - else - print("Config file not found, generating default settings!") - - maxb = 70 - minb = 30 - rod = 80 - btnOn = false - if (monSide == nil) then - btnOn = true - end - sizex, sizey = 100, 52 - dim = sizex - 33 - oo = sizey - 37 - enableGraph("Energy Buffer") - enableGraph("Control Level") - enableGraph("Temperatures") - end - btnOff = not btnOn - reactor.setActive(btnOn) -end - -local function startTimer(ticksToUpdate, callback) - local timeToUpdate = ticksToUpdate * 0.05 - local id = os.startTimer(timeToUpdate) - local fun = function(event) - if (event[1] == "timer" and event[2] == id) then - id = os.startTimer(timeToUpdate) - callback() - end - end - return fun -end - - --- Main loop, handles all the events -local function loop() - local ticksToUpdateStats = 1 - local ticksToRedraw = 4 - - local hasClicked = false - - local updateStatsTick = startTimer( - ticksToUpdateStats, - function() - updateStats() - updateRods() - end - ) - local redrawTick = startTimer( - ticksToRedraw, - function() - if (not hasClicked) then - resetMon() - drawScene() - end - hasClicked = false - end - ) - local handleResize = function(event) - if (event[1] == "monitor_resize") then - initMon() - end - end - local handleClick = function(event) - if (event[1] == "button_click") then - t.buttonList[event[2]].func() - saveToConfig() - resetMon() - drawScene() - hasClicked = true - end - end - while (true) do - local event = (monSide == nil) and { os.pullEvent() } or { t:handleEvents() } - - updateStatsTick(event) - redrawTick(event) - handleResize(event) - handleClick(event) - end -end - -local function detectReactor() - -- Bigger Reactors V1. - local reactor_bigger_v1 = getPeripheral("bigger-reactor") - reactor = reactor_bigger_v1 ~= nil and peripheral.wrap(reactor_bigger_v1) - if (reactor ~= nil) then - reactorVersion = "Bigger Reactors" - return true - end - - -- Bigger Reactors V2 - local reactor_bigger_v2 = getPeripheral("BiggerReactors_Reactor") - reactor = reactor_bigger_v2 ~= nil and peripheral.wrap(reactor_bigger_v2) - if (reactor ~= nil) then - reactorVersion = "Bigger Reactors" - return true - end - - -- Big Reactors or Extreme Reactors - local reactor_extreme_or_big = getPeripheral("BigReactors-Reactor") - reactor = reactor_extreme_or_big ~= nil and peripheral.wrap(reactor_extreme_or_big) - if (reactor ~= nil) then - reactorVersion = (reactor.mbIsConnected ~= nil) and "Extreme Reactors" or "Big Reactors" - return true - end - return false -end - ---Entry point -local function main() - term.setBackgroundColor(colors.black) - term.clear() - term.setCursorPos(1,1) - - local reactorDetected = false - while (not reactorDetected) do - reactorDetected = detectReactor() - if (not reactorDetected) then - print("Reactor not detected! Trying again...") - sleep(1) - end - end - - print("Reactor detected! Proceeding with initialization ") - - print("Loading config...") - loadFromConfig() - print("Initializing monitor if connected...") - initMon() - print("Writing config to disk...") - saveToConfig() - print("Reactor initialization done! Starting controller") - sleep(2) - - term.clear() - term.setCursorPos(1,1) - print("Reactor Controller Version "..version) - print("Reactor Mod: "..reactorVersion) - --main loop - - loop() -end - -main() - -print("script exited") -sleep(1) diff --git a/src/classes/deque.lua b/src/classes/deque.lua new file mode 100644 index 0000000..cad5fac --- /dev/null +++ b/src/classes/deque.lua @@ -0,0 +1,76 @@ +---@class Deque +---@field first number +---@field last number +---@field sum number +---@field size number +---@field list table +local Deque = { + sum = 0, + size = 0, + first = 0, + last = -1, + + ---@param self Deque + ---@param value number + pushleft = function(self, value) + local first = self.first - 1 + self.first = first + self.list[first] = value + self.size = self.size + 1 + self.sum = self.sum + value + end, + + ---@param self Deque + ---@param value number + pushright = function (self, value) + local last = self.last + 1 + self.last = last + self.list[last] = value + self.size = self.size + 1 + self.sum = self.sum + value + end, + + ---@param self Deque + ---@return number + popleft = function (self) + local first = self.first + if first > self.last then error("list is empty") end + + local value = self.list[first] + self.list[first] = nil -- to allow garbage collection + self.first = first + 1 + self.size = self.size - 1 + self.sum = self.sum - value + return value + end, + + ---@param self Deque + ---@return number + popright = function (self) + local last = self.last + if self.first > last then error("list is empty") end + local value = self.list[last] + self.list[last] = nil -- to allow garbage collection + self.last = last - 1 + self.size = self.size - 1 + self.sum = self.sum - value + return value + end, + + ---@param self Deque + ---@return number + average = function (self) + return self.sum / self.size + end +} + +---@return Deque +local function new() + local dequeInstance = {list = {}} + setmetatable(dequeInstance, {__index = Deque}) + return dequeInstance +end + +_G.Deque = { + new = new +} diff --git a/src/classes/energybuffer.lua b/src/classes/energybuffer.lua new file mode 100644 index 0000000..e047e21 --- /dev/null +++ b/src/classes/energybuffer.lua @@ -0,0 +1,89 @@ +---@class EnergyBuffer +---@field id string +---@field energyStoredLastTick number +---@field energyStoredThisTick number +---@field energyStoredLastTickValues Deque +---@field energyStoredThisTickValues Deque +---@field averageEnergyStoredLastTick number +---@field averageEnergyStoredThisTick number +---@field lastUpdatedTick number +---@field capacity number +---@field getEnergyStored function +---@field getEnergyCapacity function +local EnergyBuffer = { + updateAverages = function (self) + self.energyStoredThisTickValues:pushleft(self.energyStoredThisTick) + self.energyStoredLastTickValues:pushleft(self.energyStoredLastTick) + + local ticksToAverage = 20 * _G.SECONDS_TO_AVERAGE + while self.energyStoredThisTickValues.size > ticksToAverage do + self.energyStoredLastTickValues:popright() + self.energyStoredThisTickValues:popright() + end + + self.averageEnergyStoredLastTick = self.energyStoredLastTickValues:average() + self.averageEnergyStoredThisTick = self.energyStoredThisTickValues:average() + end, + + ---@param self EnergyBuffer + ---@param currentTickNumber number + update = function(self, currentTickNumber) + if self.lastUpdatedTick >= currentTickNumber then + return + elseif self.lastUpdatedTick < currentTickNumber - 1 then + -- We missed the last tick - we don't know what it is! just set it to 0 for now... + self.energyStoredLastTick = 0 + end + local newEnergyStored = self:getEnergyStored() + self.capacity = self:getEnergyCapacity() + self.energyStoredLastTick = self.energyStoredThisTick + self.energyStoredThisTick = newEnergyStored + + self:updateAverages() + + self.lastUpdatedTick = currentTickNumber + end, + +} + +---@return EnergyBuffer +local function new(id, energyStoredFunction, energyCapacityFunction) + local energyBufferInstance = { + id = id, + energyStoredLastTick = 0, + energyStoredThisTick = 0, + energyStoredLastTickValues = Deque.new(), + energyStoredThisTickValues = Deque.new(), + lastUpdatedTick = 0, + capacity = 0, + getEnergyStored = energyStoredFunction, + getEnergyCapacity = energyCapacityFunction, + } + setmetatable(energyBufferInstance, {__index = EnergyBuffer}) + local currentTickNumber = math.floor(os.clock() * 20) + energyBufferInstance:update(currentTickNumber) + return energyBufferInstance +end + +local function newReactorEnergyBuffer(id) + local energyPeripheral = peripheral.wrap(id) + return new( + id, + function() return energyPeripheral.getEnergyStats().energyStored end, + function() return energyPeripheral.getEnergyStats().energyCapacity end + ) +end + +local function newForgeEnergyBuffer(id) + local energyPeripheral = peripheral.wrap(id) + return new( + id, + energyPeripheral.getEnergy, + energyPeripheral.getEnergyCapacity + ) +end + +_G.EnergyBuffer = { + newForgeEnergyBuffer = newForgeEnergyBuffer, + newReactorEnergyBuffer = newReactorEnergyBuffer, +} \ No newline at end of file diff --git a/src/classes/graph.lua b/src/classes/graph.lua new file mode 100644 index 0000000..da71268 --- /dev/null +++ b/src/classes/graph.lua @@ -0,0 +1,92 @@ +---@class Graph +local Graph = { + ---@type string + id = nil, + ---@type string + name = nil, + ---@type Vector2 + offset = nil, + ---@type Vector2 + size = nil, + ---@type function + drawCallback = nil, + ---@param self Graph + ---@param mon table + ---@param statistics ReactorStatistics + draw = function(self, mon, statistics) + self.drawCallback(mon, self.offset, self.size, statistics) + end, +} + +---comment +---@param id string +---@param name string +---@param offset Vector2 +---@param size Vector2 +---@param drawCallback function +---@return Graph +local function newGraph(id, name, offset, size, drawCallback) + + local graphInstance = { + id = id, + name = name, + offset = offset, + size = size, + drawCallback = drawCallback, + } + setmetatable(graphInstance, {__index = Graph}) + return graphInstance +end + +_G.Graph = { + new = newGraph +} + +---@param mon table +---@param offset Vector2 +---@param size Vector2 +local function drawControlGraph(mon, offset, size, averageRod) + local controlRodLength0To1 = averageRod / 100 + local controlRodMaxLengthOnScreen = size.y - 2 + local controlRodLengthOnScreen = math.ceil(controlRodLength0To1 * (controlRodMaxLengthOnScreen)) + + + DrawUtil.drawText( + mon, + "Control Level", + offset + Vector2.new(1, 0), + colors.black, + colors.orange + + ) + DrawUtil.drawFilledBoxWithBorder( + mon, + colors.yellow, + colors.gray, + offset + Vector2.new(0, 1), + size + ) + DrawUtil.drawFilledBox( + mon, + colors.white, + offset + Vector2.new(3, 2), + Vector2.new(9, controlRodLengthOnScreen) + ) + + local controlRodPercentTextPosition, color + if controlRodLengthOnScreen > 0 then + color = colors.white + controlRodPercentTextPosition = offset + Vector2.new(4, 1 + controlRodLengthOnScreen) + else + color = colors.yellow + controlRodPercentTextPosition = offset + Vector2.new(4, 2) + end + + DrawUtil.drawText( + mon, + string.format("%6.2f%%", averageRod), + controlRodPercentTextPosition, + color, + colors.black + ) +end \ No newline at end of file diff --git a/src/classes/monitor.lua b/src/classes/monitor.lua new file mode 100644 index 0000000..63164f3 --- /dev/null +++ b/src/classes/monitor.lua @@ -0,0 +1,620 @@ +_G.MONITOR_CONSTANTS = { + MINIMUM_DIVIDER_X_VALUE = 3, + MINIMUM_DIVIDER_Y_VALUE = 1, +} + +-- table of which graphs to draw + +---@type table +local graphs = +{ + "Energy Buffer", + "Control Level", + "Temperatures", +} + +local function round(num, dig) + return math.floor(10 ^ dig * num + 0.5) / (10 ^ dig) +end + +local function format(num) + if (num >= 1000000000) then + return string.format("%7.3f G", num / 1000000000) + elseif (num >= 1000000) then + return string.format("%7.3f M", num / 1000000) + elseif (num >= 1000) then + return string.format("%7.3f K", num / 1000) + elseif (num >= 1) then + return string.format("%7.3f ", num) + elseif (num >= .001) then + return string.format("%7.3f m", num * 1000) + elseif (num >= .000001) then + return string.format("%7.3f u", num * 1000000) + else + return string.format("%7.3f ", 0) + end +end + +local function getPercPower() + return _G.overallStats.storedThisTick / _G.overallStats.capacity * 100 +end + +--Helper method for adding buttons +local function addButton(touch, name, callBack, offset, size, color1, color2) + local buttonTopLeftCorner = offset + Vector2.one + local buttonBottomRightCorner = offset + size + touch:add( + name, + callBack, + buttonTopLeftCorner.x, + buttonTopLeftCorner.y, + buttonBottomRightCorner.x, + buttonBottomRightCorner.y, + color1, + color2 + ) +end + +local function minAdd10() + _G.minb = math.min(_G.maxb - 10, _G.minb + 10) +end +local function minSub10() + _G.minb = math.max(0, _G.minb - 10) +end +local function maxAdd10() + _G.maxb = math.min(100, _G.maxb + 10) +end +local function maxSub10() + _G.maxb = math.max(_G.minb + 10, _G.maxb - 10) +end + +local function turnOff() + if _G.btnOn then + _G.btnOn = false + setReactors(false) + end +end + +local function turnOn() + if not _G.btnOn then + _G.btnOn = true + setReactors(true) + end +end + +--adds buttons +local function addReactorControlButtons(touch, offset, shouldDrawBufferVisualization) + local buttonSize = Vector2.new(8, 3) + local offsetOnOff = offset + Vector2.new(5, 3) + + addButton(touch, "On", turnOn, offsetOnOff, buttonSize, colors.red, colors.lime) + addButton(touch, "Off", turnOff, offsetOnOff + Vector2.new(12, 0), buttonSize, colors.red, colors.lime) + if shouldDrawBufferVisualization then + addButton(touch, "+ 10", minAdd10, offset + Vector2.new(5, 14), buttonSize, colors.purple, colors.pink) + addButton(touch, " + 10 ", maxAdd10, offset + Vector2.new(17, 14), buttonSize, colors.magenta, colors.pink) + addButton(touch, "- 10", minSub10, offset + Vector2.new(5, 18), buttonSize, colors.purple, colors.pink) + addButton(touch, " - 10 ", maxSub10, offset + Vector2.new(17, 18), buttonSize, colors.magenta, colors.pink) + end +end + +local GRAPH_SEPARATION_X = 23 +local GRAPH_FIRST_OFFSET_X = 4 + +local function getFirstAvailableGraphSlot(graphSlots) + local offset = GRAPH_FIRST_OFFSET_X + while graphSlots[offset] ~= nil do + offset = offset + GRAPH_SEPARATION_X + end + return offset +end + +local function getGraphXCoord(graphSlots, name) + for xCoord, graph in pairs(graphSlots) do + if graph.name == name then + return xCoord + end + end + return -1 +end + +local function createGraph(name) + return {name = name} +end + +local function enableGraph(graphSlots, name) + if getGraphXCoord(graphSlots, name) > -1 then + return + end + + local slotXCoord = getFirstAvailableGraphSlot(graphSlots) + + graphSlots[slotXCoord] = createGraph(name) +end + +local function disableGraph(graphSlots, name) + local graphXSlot = getGraphXCoord(graphSlots, name) + + graphSlots[graphXSlot] = nil +end + +local function toggleGraph(graphSlots, name) + + local graphSlotX = getGraphXCoord(graphSlots, name) + if graphSlotX == -1 then + enableGraph(graphSlots, name) + else + disableGraph(graphSlots, name) + end +end + +---comment +---@param monitor Monitor +local function addGraphButtons(monitor, graphSlots, offset, size) + for i, graphName in pairs(graphs) do + addButton( + monitor.touch, + graphName, + function() + toggleGraph(graphSlots, graphName) + end, + offset + Vector2.new(0, i * 3 - 1), + size, + colors.red, + colors.lime + ) + end +end + + +local function drawGraphMenu(mon, offset, size) + DrawUtil.drawBox(mon, colors.orange, offset, size) + local textPos = offset + Vector2.new(4, 0) + DrawUtil.drawText(mon, " Graph Controls ", textPos, colors.black, colors.orange) +end + +local function drawEnergyBuffer(mon, offset, graphSize, drawPercentLabelOnRight) + DrawUtil.drawText(mon, "Energy Buffer", offset, colors.black, colors.orange) + DrawUtil.drawFilledBoxWithBorder(mon, colors.red, colors.gray, offset + Vector2.new(0, 1), graphSize) + + local percentFull = getPercPower() + local exactBufferAmount = _G.overallStats.storedThisTick + local energyBufferMaxHeight = graphSize.y - 2 + local unitEnergyLevel = percentFull / 100 + local energyBufferHeight = math.floor(unitEnergyLevel * energyBufferMaxHeight + 0.5) + local rndpw = round(percentFull, 2) + + local energyBufferColor + if rndpw < _G.maxb and rndpw > _G.minb then + energyBufferColor = colors.green + elseif rndpw >= _G.maxb then + energyBufferColor = colors.orange + elseif rndpw <= _G.minb then + energyBufferColor = colors.blue + end + + local energyBufferTipOffset = offset + Vector2.new(1, 2 + energyBufferMaxHeight - energyBufferHeight) + local energyBufferSize = Vector2.new(graphSize.x - 2, energyBufferHeight) + + DrawUtil.drawFilledBox(mon, energyBufferColor, energyBufferTipOffset, energyBufferSize) + + local energyBufferTextOffset = energyBufferTipOffset + local rfLabelBackgroundColor = energyBufferColor + + if energyBufferHeight <= 0 then + energyBufferTextOffset = energyBufferTipOffset + Vector2.new(0, -1) + rfLabelBackgroundColor = colors.red + end + + local percentLabelXOffset = offset.x - 6 + if drawPercentLabelOnRight then + percentLabelXOffset = offset.x + 15 + end + + DrawUtil.drawText( + mon, + string.format(drawPercentLabelOnRight and "%.2f%%" or "%5.2f%%", rndpw), + Vector2.new(percentLabelXOffset, energyBufferTextOffset.y), + colors.black, + energyBufferColor + ) + DrawUtil.drawText( + mon, + format(exactBufferAmount).."RF", + energyBufferTextOffset, + rfLabelBackgroundColor, + colors.black + ) +end + +local function drawControlGraph(mon, offset, size, averageRod) + local unitRodLevel = averageRod / 100 + local controlRodMaxPixelHeight = size.y - 2 + local controlRodPixelHeight = math.ceil(unitRodLevel * controlRodMaxPixelHeight) + + DrawUtil.drawText(mon, "Control Level", offset + Vector2.new(1, 0), colors.black, colors.orange) + DrawUtil.drawFilledBoxWithBorder(mon, colors.yellow, colors.gray, offset + Vector2.new(0, 1), size) + DrawUtil.drawFilledBox(mon, colors.white, offset + Vector2.new(3, 2), Vector2.new(9, controlRodPixelHeight)) + + local controlRodLevelTextPos, color + if controlRodPixelHeight > 0 then + color = colors.white + controlRodLevelTextPos = offset + Vector2.new(4, 1 + controlRodPixelHeight) + else + color = colors.yellow + controlRodLevelTextPos = offset + Vector2.new(4, 2) + end + + DrawUtil.drawText(mon, string.format("%6.2f%%", averageRod), controlRodLevelTextPos, color, colors.black) +end + +local function drawTemperatures(mon, offset, size) + + DrawUtil.drawBox(mon, colors.gray, offset + Vector2.one, size) + + local CASE_TEMP_COLOR = colors.lightBlue + local FUEL_TEMP_COLOR = colors.magenta + local BACKGROUND_COLOR = colors.black + + local assumedMaxCaseTemperature = 3000 + local assumedMaxFuelTemperature = 3000 + local temperatureMaxHeight = size.y - 2 + + -- local tempUnit = (_G.reactorVersion == "Bigger Reactors") and "K" or "C" + local caseTemp = _G.selectedReactor.averageCaseTemp + local tempUnit = "C" + local tempFormat = "%4s"..tempUnit + + DrawUtil.drawText(mon, "Temperatures", offset + Vector2.new(2, 0), BACKGROUND_COLOR, colors.orange) + DrawUtil.drawFilledBox(mon, colors.gray, offset + Vector2.new(8, 2), Vector2.new(1, temperatureMaxHeight)) + + -- case temp + DrawUtil.drawText(mon, "Case", offset + Vector2.new(3, 1), colors.gray, colors.lightBlue) + local caseUnit = math.min(caseTemp / assumedMaxCaseTemperature, 1) + local caseTempHeight = math.floor(caseUnit * temperatureMaxHeight + 0.5) + + local caseTempOffset = offset + Vector2.new(2, 2 + temperatureMaxHeight - caseTempHeight) + local caseTempSize = Vector2.new(6, caseTempHeight) + + DrawUtil.drawFilledBox(mon, CASE_TEMP_COLOR, caseTempOffset, caseTempSize) + + local caseTempTextOffset = caseTempOffset + local caseTempTextBackgroundColor = CASE_TEMP_COLOR + local caseTempTextColor = BACKGROUND_COLOR + + if caseTempHeight <= 0 then + caseTempTextOffset = caseTempOffset + Vector2.new(0, -1) + caseTempTextColor, caseTempTextBackgroundColor = caseTempTextBackgroundColor, caseTempTextColor + end + + local caseRnd = math.floor(caseTemp + 0.5) + DrawUtil.drawText(mon, string.format(tempFormat, caseRnd..""), caseTempTextOffset, caseTempTextBackgroundColor, caseTempTextColor) + + local fuelTemp = _G.selectedReactor.averageFuelTemp + + -- fuel temp + DrawUtil.drawText(mon, "Fuel", offset + Vector2.new(10, 1), colors.gray, colors.lightBlue) + local fuelUnit = math.min(fuelTemp / assumedMaxFuelTemperature, 1) + local fuelTempHeight = math.floor(fuelUnit * temperatureMaxHeight + 0.5) + + local fuelTempOffset = offset + Vector2.new(9, 2 + temperatureMaxHeight - fuelTempHeight) + local fuelTempSize = Vector2.new(6, fuelTempHeight) + + DrawUtil.drawFilledBox(mon, FUEL_TEMP_COLOR, fuelTempOffset, fuelTempSize) + + local fuelTempTextOffset = fuelTempOffset + local fuelTempTextBackgroundColor = FUEL_TEMP_COLOR + local fuelTempTextColor = BACKGROUND_COLOR + + if fuelTempHeight <= 0 then + fuelTempTextOffset = fuelTempOffset + Vector2.new(0, -1) + fuelTempTextColor, fuelTempTextBackgroundColor = fuelTempTextBackgroundColor, fuelTempTextColor + end + + local fuelRnd = math.floor(fuelTemp + 0.5) + DrawUtil.drawText(mon, string.format(tempFormat, fuelRnd..""), fuelTempTextOffset, fuelTempTextBackgroundColor, fuelTempTextColor) +end + +local function drawGraph(mon, dividerXCoord, name, graphOffset, graphSize) + if (name == "Energy Buffer") then + local drawPercentLabelOnRight = graphOffset.x + 19 < dividerXCoord - 1 + drawEnergyBuffer(mon, graphOffset, graphSize, drawPercentLabelOnRight) + elseif (name == "Control Level") then + drawControlGraph(mon, graphOffset, graphSize, _G.selectedReactor.averageRodLevel) + elseif (name == "Temperatures") then + drawTemperatures(mon, graphOffset, graphSize) + end +end + +local function drawGraphs(mon, monitorSize, graphSlots, dividerXCoord, offset, size) + DrawUtil.drawBox(mon, colors.lightBlue, offset, size) + local label = " Reactor Graphs " + DrawUtil.drawText( + mon, + label, + offset + Vector2.new(dividerXCoord - (#label + 5) - 1, 0), + colors.black, + colors.lightBlue + ) + + local graphSize = Vector2.new(15, monitorSize.y - 7) + local graphYOffset = 4 + for graphXOffset, graph in pairs(graphSlots) do + if graphXOffset + graphSize.x < dividerXCoord then + drawGraph(mon, dividerXCoord, graph.name, Vector2.new(graphXOffset, graphYOffset), graphSize) + end + end +end + +local function drawControls(mon, offset, size, drawBufferVisualization) + if not drawBufferVisualization then + size = Vector2.new(30, 9) + end + + DrawUtil.drawBox(mon, colors.cyan, offset, size) + DrawUtil.drawText(mon, " Reactor Controls ", offset + Vector2.new(4, 0), colors.black, colors.cyan) + + local reactorOnOffLabel = "Reactor "..(_G.btnOn and "Online" or "Offline") + local reactorOnOffLabelColor = _G.btnOn and colors.green or colors.red + DrawUtil.drawText(mon, reactorOnOffLabel, offset + Vector2.new(7, 2), colors.black, reactorOnOffLabelColor) + + if not drawBufferVisualization then + return + end + + local bufferMinInPixels = _G.minb / 5 + local bufferMaxInPixels = _G.maxb / 5 + local bufferRangePixelWidth = bufferMaxInPixels - bufferMinInPixels + + local bufferVisualOffset = offset + Vector2.new(5, 8) + local bufferVisualSize = Vector2.new(20, 3) + + DrawUtil.drawText(mon, "Buffer Target Range", bufferVisualOffset + Vector2.new(0, -1), colors.black, colors.orange) + DrawUtil.drawFilledBox(mon, colors.red, bufferVisualOffset, bufferVisualSize) + DrawUtil.drawFilledBox(mon, colors.green, bufferVisualOffset + Vector2.new(bufferMinInPixels, 0), Vector2.new(bufferRangePixelWidth, 3)) + + DrawUtil.drawText( + mon, + string.format("%3s", _G.minb.."%"), + bufferVisualOffset + Vector2.new(bufferMinInPixels - 3, bufferVisualSize.y), + colors.black, + colors.purple + ) + DrawUtil.drawText( + mon, + _G.maxb.."%", + bufferVisualOffset + Vector2.new(bufferMaxInPixels, bufferVisualSize.y), + colors.black, + colors.magenta + ) + DrawUtil.drawText(mon, "Min", offset + Vector2.new(7, 13), colors.black, colors.purple) + DrawUtil.drawText(mon, "Max", offset + Vector2.new(19, 13), colors.black, colors.purple) +end + + +local statsList = { + { message = "", color = colors.green }, + { message = "", color = colors.green }, + { message = "", color = colors.green }, + { message = "", color = colors.green }, + { message = "", color = colors.green }, +} + +local function drawStatistics(mon, offset, size) + DrawUtil.drawBox(mon, colors.blue, offset, size) + DrawUtil.drawText(mon, " Reactor Statistics ", offset + Vector2.new(4, 0), colors.black, colors.blue) + statsList[1].message = "Generating : "..format(_G.overallStats.lastRFT).."RF/t" + statsList[1].color = colors.green + statsList[2].message = "RF Drain "..(_G.overallStats.storedThisTick <= _G.overallStats.lastRFT and "> " or ": ")..format(_G.overallStats.rfLost).."RF/t" + statsList[2].color = colors.red + statsList[3].message = "Efficiency : "..format(_G.overallStats.efficiency()).."RF/B" + statsList[3].color = colors.green + statsList[4].message = "Fuel Usage : "..format(_G.overallStats.fuelUsage).."B/t" + statsList[4].color = colors.green + statsList[5].message = "Waste : "..string.format("%7d mB", _G.overallStats.waste) + statsList[5].color = colors.green + + local elemOffset = offset + Vector2.new(2, 2) + for i, details in pairs(statsList) do + DrawUtil.drawText(mon, details.message, elemOffset + Vector2.new(0, 2) * (i - 1), colors.black, details.color) + end +end + +local function updateReactorControlButtonStates(touch) + touch:setButton("On", _G.btnOn) + touch:setButton("Off", not _G.btnOn) +end + +local function updateGraphMenuButtonStates(touch, graphSlots) + for _, graphName in pairs(graphs) do + touch:setButton(graphName, false) + end + for _, graph in pairs(graphSlots) do + touch:setButton(graph.name, true) + end +end + + ---@return integer +local function calculateDividerYCoord(sizey) + if sizey <= 24 then + return MONITOR_CONSTANTS.MINIMUM_DIVIDER_Y_VALUE + end + return sizey - 37 +end + +---@return integer +local function calculateDividerXCoord(sizex) + if sizex <= 36 then + return MONITOR_CONSTANTS.MINIMUM_DIVIDER_X_VALUE + end + return sizex - 31 +end + +local function greaterThanEqualTo(firstVector2, secondVector2) + return firstVector2.x >= secondVector2.x and firstVector2.y >= secondVector2.y +end + +local MINIMUM_SIZE_TO_DRAW = {x = 36, y = 24} +local REACTOR_CONTROL_MIN_SIZE = {x = 36, y = 24} +local REACTOR_CONTROL_BUFFER_VIS_MIN_SIZE = {x = 36, y = 38} +local GRAPH_MENU_MIN_SIZE = {x = 36, y = 52} +local GRAPHS_MIN_SIZE = {x = 57, y = 24} +local STATISTICS_MIN_SIZE = {x = 36, y = 24} + +local function getDrawOptions(monitorSize) + local drawOptions = { + drawInvalidMonitorDimensions = not greaterThanEqualTo(monitorSize, MINIMUM_SIZE_TO_DRAW), + drawReactorControls = greaterThanEqualTo(monitorSize, REACTOR_CONTROL_MIN_SIZE), + drawReactorControlsBufferVisualization = greaterThanEqualTo(monitorSize, REACTOR_CONTROL_BUFFER_VIS_MIN_SIZE), + drawGraphMenu = greaterThanEqualTo(monitorSize, GRAPH_MENU_MIN_SIZE), + drawGraphs = greaterThanEqualTo(monitorSize, GRAPHS_MIN_SIZE), + drawStatistics = greaterThanEqualTo(monitorSize, STATISTICS_MIN_SIZE), + } + return drawOptions +end +local pretty = require "cc.pretty" +---@class Monitor +local Monitor = { + navbar = nil, + ---@type Page + activePage = nil, + ---@type string + id = nil, + ---@type table + mon = nil, + ---@type table + touch = nil, + monPeripheral = nil, + + -- mutable! + graphSlots = nil, + + -- Never edit these outside of handleResize()! + size = nil, + dividerYCoord = nil, + dividerXCoord = nil, + drawOptions = nil, + + ---comment + ---@param self Monitor + clear = function(self) + self.mon.setBackgroundColor(colors.black) + self.mon.clear() + self.mon.setCursorPos(1,1) + end, + + handleClick = function(self, buttonName) + + -- Enable when navbar and page is created + -- local button = self.navbar.touch.buttonList[buttonName] or self.activePage.touch.buttonList[buttonName] + -- button.func() + + self.touch.buttonList[buttonName].func() + print(buttonName, "clicked on", self.id) + end, + + handleResize = function(self) + self.monPeripheral.setTextScale(0.5) + self.size = Vector2.new(self.monPeripheral.getSize()) + self.mon = window.create(self.monPeripheral, 1, 1, self.size.x, self.size.y, false) + self.touch = _G.Touchpoint.new(self.id, self.mon) + self.dividerXCoord = calculateDividerXCoord(self.size.x) + self.dividerYCoord = calculateDividerYCoord(self.size.y) + + self.drawOptions = getDrawOptions(self.size) + if self.drawOptions.drawGraphMenu then + local offset = Vector2.new(self.dividerXCoord + 5, self.dividerYCoord - 14) + local size = Vector2.new(20, 3) + addGraphButtons(self, self.graphSlots, offset, size) + end + + if self.drawOptions.drawReactorControls then + local offset = Vector2.new(self.dividerXCoord, self.dividerYCoord) + addReactorControlButtons(self.touch, offset, self.drawOptions.drawReactorControlsBufferVisualization) + end + end, + + handleEvents = function(self, event) + if event[2] ~= self.id then + return + end + local touchpointEvent = { self.touch:handleEvents(unpack(event)) } + if touchpointEvent[1] == "button_click" then + local buttonName = touchpointEvent[3] + self:handleClick(buttonName) + end + if event[1] == "monitor_resize" then + if self.monPeripheral.getTextScale() ~= 0.5 then + self.mon.setVisible(false) + self.monPeripheral.setTextScale(0.5) + return + end + self:handleResize() + end + + -- Immediately draw the clicked monitor so that users don't feel any input delay when using the monitors + self:draw() + end, + + ---@param self Monitor + draw = function(self) + self.mon.setVisible(false) + self:clear() + + if self.drawOptions.drawInvalidMonitorDimensions then + self.mon.write("Invalid Monitor Dimensions") + end + + if self.drawOptions.drawGraphMenu then + local offset = Vector2.new(self.dividerXCoord + 1, self.dividerYCoord - 14 + 1) + local size = Vector2.new(30, 13) + drawGraphMenu(self.mon, offset, size) + updateGraphMenuButtonStates(self.touch, self.graphSlots) + end + + if self.drawOptions.drawReactorControls then + local offset = Vector2.new(self.dividerXCoord + 1, self.dividerYCoord + 1) + local size = Vector2.new(30, 23) + drawControls(self.mon, offset, size, self.drawOptions.drawReactorControlsBufferVisualization) + updateReactorControlButtonStates(self.touch) + end + + if self.drawOptions.drawGraphs then + local offset = Vector2.new(2, 2) + local size = Vector2.new(self.dividerXCoord - 2, self.size.y - 2) + drawGraphs(self.mon, self.size, self.graphSlots, self.dividerXCoord, offset, size) + end + + if self.drawOptions.drawStatistics then + local offset = Vector2.new(self.dividerXCoord + 1, self.size.y - 12) + local size = Vector2.new(30, 12) + drawStatistics(self.mon, offset, size) + end + + self.touch:drawAllButtons() + self.mon.setVisible(true) + end +} + +---comment +---@param id string +---@return Monitor +local function new(id) + local monPeripheral = peripheral.wrap(id) + + local monitorInstance = { + id = id, + monPeripheral = monPeripheral, + graphSlots = {}, + } + setmetatable(monitorInstance, {__index = Monitor}) + monitorInstance:handleResize() + + enableGraph(monitorInstance.graphSlots, "Energy Buffer") + enableGraph(monitorInstance.graphSlots, "Control Level") + enableGraph(monitorInstance.graphSlots, "Temperatures") + return monitorInstance +end + +_G.Monitor = { + new = new +} diff --git a/src/classes/navbar.lua b/src/classes/navbar.lua new file mode 100644 index 0000000..fc4cf66 --- /dev/null +++ b/src/classes/navbar.lua @@ -0,0 +1,43 @@ +---@class Navbar +local Navbar = { + ---@type table + pageNameMap = nil, + + touch = nil, + ---@type Vector2 + offset = nil, + ---@type Vector2 + size = nil, + + handleEvents = function() + + end, + + ---comment + ---@param self Navbar + ---@param reactorStats ReactorStatistics + draw = function(self, reactorStats) + + end +} + +---comment +---@param peripheralId string +---@return Navbar +local function new(peripheralId) + local monitorPeripheral = peripheral.wrap(peripheralId) + local touch = _G.Touchpoint.new(peripheralId) + + local monitorInstance = { + id = peripheralId, + peripheral = monitorPeripheral, + touch = touch, + } + + setmetatable(monitorInstance, {__index = Navbar}) + return monitorInstance +end + +_G.Navbar = { + new = new +} diff --git a/src/classes/page.lua b/src/classes/page.lua new file mode 100644 index 0000000..f69d886 --- /dev/null +++ b/src/classes/page.lua @@ -0,0 +1,52 @@ +---@class Page +local Page = { + ---@type string + name = nil, + ---@type table + mon = nil, + ---@type table + touch = nil, + ---@type Vector2 + offset = nil, + ---@type Vector2 + size = nil, + + ---@param self Page + draw = function(self) + if (_G.displayingGraphMenu) then + drawGraphButtons() + end + drawControls() + drawStatus() + drawStatistics() + self.touch:draw() + end, + + ---comment + ---@param self Page + ---@param reactorStats ReactorStatistics + update = function(self, reactorStats) + + end +} + +---comment +---@param peripheralId string +---@return Page +local function new(peripheralId) + local monitorPeripheral = peripheral.wrap(peripheralId) + local touch = _G.Touchpoint.new(peripheralId) + + local monitorInstance = { + id = peripheralId, + peripheral = monitorPeripheral, + touch = touch, + } + + setmetatable(monitorInstance, {__index = Page}) + return monitorInstance +end + +_G.Page = { + new = new +} diff --git a/src/classes/pid.lua b/src/classes/pid.lua new file mode 100644 index 0000000..c567f68 --- /dev/null +++ b/src/classes/pid.lua @@ -0,0 +1,28 @@ +---@class PIDController +local PIDController = { + Kp = -.08, + Ki = -.0015, + Kd = -.01, + setpoint = 0, + integral = 0, + lastError = 0, + + iterate = function(self, error) + + end +} +---comment +---@return PIDController +local function new(Kp, Ki, Kd) + local pidControllerInstance = { + Kp = Kp, + Ki = Ki, + Kd = Kd, + } + setmetatable(pidControllerInstance, {__index = PIDController}) + return pidControllerInstance +end + +_G.PIDController = { + new = new +} \ No newline at end of file diff --git a/src/classes/reactor.lua b/src/classes/reactor.lua new file mode 100644 index 0000000..d6af5eb --- /dev/null +++ b/src/classes/reactor.lua @@ -0,0 +1,263 @@ +-- Function to calculate the average of an array of values +local function calculateAverage(array) + local sum = 0 + local count = 0 + for _, value in pairs(array) do + sum = sum + value + count = count + 1 + end + return sum / count +end + + +local function setRods(reactor, level) + level = math.max(level, 0) + level = math.min(level, 100) + local count = reactor.getNumberOfControlRods() + + local numberToAddOneLevelTo = math.floor((level - math.floor(level)) * count + 0.5) + + local levelsMap = {} + for idx0, _ in pairs(reactor.getControlRodsLevels()) do + local rodLevel = math.floor(level) + if numberToAddOneLevelTo > 0 then + rodLevel = rodLevel + 1 + numberToAddOneLevelTo = numberToAddOneLevelTo - 1 + end + levelsMap[idx0] = rodLevel + end + reactor.setControlRodsLevels(levelsMap) +end + + +local function lerp(start, finish, t) + t = math.max(0, math.min(1, t)) + + return (1 - t) * start + t * finish +end + +local function iteratePID(pid, error) + -- Proportional term + local P = pid.Kp * error + + -- Integral term + pid.integral = pid.integral + pid.Ki * error + pid.integral = math.max(math.min(100, pid.integral), -100) + + -- Derivative term + local derivative = pid.Kd * (error - pid.lastError) + + -- Calculate control rod level + local rodLevel = math.max(math.min(P + pid.integral + derivative, 100), 0) + + -- Update PID controller state + pid.lastError = error + return rodLevel +end + +---@class Reactor +---@field id string +---@field active boolean +---@field activelyCooled boolean +---@field lastUpdatedTick number +---@field lastRFT number +---@field rodLevel number +---@field fuelUsage number +---@field waste number +---@field fuelTemp number +---@field caseTemp number +---@field fuelEfficiency number +---@field steamProductionRate number +---@field storedSteam number +---@field steamCapacity number +---@field lastRFTValues Deque +---@field rodLevelValues Deque +---@field fuelUsageValues Deque +---@field wasteValues Deque +---@field fuelTempValues Deque +---@field caseTempValues Deque +---@field steamProductionRateValues Deque +---@field storedSteamValues Deque +---@field averageLastRFT number +---@field averageRodLevel number +---@field averageFuelUsage number +---@field averageWaste number +---@field averageFuelTemp number +---@field averageSteamProductionRate number +---@field averageStoredSteam number +---@field averageFuelEfficiency number +---@field getLastRFT function +---@field getRodLevel function +---@field getFuelUsage function +---@field getWaste function +---@field getFuelTemp function +---@field getCaseTemp function +---@field getSteamProductionRate function +---@field getStoredSteam function +---@field getSteamCapacity function +---@field isActivelyCooled function +---@field getActive function +---@field setActive function +---@field setRodLevels function +local Reactor = { + + lastUpdatedTick = 0, + + updateAverages = function (self) + self.fuelUsageValues:pushleft(self.fuelUsage) + self.lastRFTValues:pushleft(self.lastRFT) + self.fuelTempValues:pushleft(self.fuelTemp) + self.caseTempValues:pushleft(self.caseTemp) + self.rodLevelValues:pushleft(self.rodLevel) + self.wasteValues:pushleft(self.waste) + self.steamProductionRateValues:pushleft(self.steamProductionRate) + self.storedSteamValues:pushleft(self.storedSteam) + + local ticksToAverage = 20 * _G.SECONDS_TO_AVERAGE + while self.lastRFTValues.size > ticksToAverage do + self.fuelUsageValues:popright() + self.lastRFTValues:popright() + self.fuelTempValues:popright() + self.caseTempValues:popright() + self.rodLevelValues:popright() + self.wasteValues:popright() + self.steamProductionRateValues:popright() + self.storedSteamValues:popright() + end + + self.averageFuelUsage = self.fuelUsageValues:average() + self.averageLastRFT = self.lastRFTValues:average() + self.averageFuelTemp = self.fuelTempValues:average() + self.averageCaseTemp = self.caseTempValues:average() + self.averageRodLevel = self.rodLevelValues:average() + self.averageWaste = self.wasteValues:average() + self.averageSteamProductionRate = self.steamProductionRateValues:average() + self.averageStoredSteam = self.storedSteamValues:average() + + self.averageFuelEfficiency = self.averageLastRFT / self.averageFuelUsage + end, + + ---@param self Reactor + ---@param currentTickNumber number + update = function(self, currentTickNumber) + if self.lastUpdatedTick >= currentTickNumber then + return + elseif self.lastUpdatedTick < currentTickNumber - 1 then + -- We missed the last tick - Don't do anything different for now... + print("missed last tick!") + end + + self.activelyCooled = self.isActivelyCooled() + self.active = self.getActive() + self.lastRFT = self.getLastRFT() + self.rodLevel = self.getRodLevel() + self.fuelUsage = self.getFuelUsage() + self.waste = self.getWaste() + self.fuelTemp = self.getFuelTemp() + self.caseTemp = self.getCaseTemp() + self.steamProductionRate = self.getSteamProductionRate() + self.storedSteam = self.getStoredSteam() + self.steamCapacity = self.getSteamCapacity() + self.fuelEfficiency = self.lastRFT / self.fuelUsage + + self:updateAverages() + self.lastUpdatedTick = currentTickNumber + end, + + ---@param self Reactor + -- -@param targetRFT number + -- -@param targetRF number + updateRods = function (self) + if not self.active then + return + end + + local currentGenerationRate = self.averageLastRFT + local currentStoredAmount = _G.overallStats.storedThisTick + local capacity = _G.overallStats.capacity + local targetGenerationRate = _G.overallStats.rfLost + + if self.activelyCooled then + currentGenerationRate = self.averageSteamProductionRate + currentStoredAmount = _G.overallStats.storedSteam + capacity = _G.overallStats.steamCapacity + targetGenerationRate = _G.overallStats.steamConsumedLastTick + end + + local diffb = _G.maxb - _G.minb + local minRF = _G.minb / 100 * capacity + local diffRF = diffb / 100 * capacity + local diffr = diffb / 100 + -- local targetStoredAmount = diffRF / 2 + minRF + local targetStoredAmount = currentStoredAmount + + self.pid.setpointRFT = targetGenerationRate + self.pid.setpointRF = targetStoredAmount / capacity * 1000 + + local errorRFT = self.pid.setpointRFT - currentGenerationRate + local errorRF = self.pid.setpointRF - currentStoredAmount / capacity * 1000 + + local W_RFT = lerp(1, 0, (math.abs(targetStoredAmount - currentStoredAmount) / capacity / (diffr / 4))) + W_RFT = math.max(math.min(W_RFT, 1), 0) + + local W_RF = (1 - W_RFT) -- Adjust the weight for energy error + + -- Combine the errors with weights + local combinedError = W_RFT * errorRFT + W_RF * errorRF + local error = combinedError + local rftRodLevel = iteratePID(self.pid, error) + + -- Set control rod levels + self.setRodLevels(rftRodLevel) + end, +} + +---@param id string +---@return Reactor +local function newExtremeReactor(id) + local extremeReactor = peripheral.wrap(id) + local pid = { + setpointRFT = 0, -- Target RFT + setpointRF = 0, -- Target RF + Kp = -.008, -- Proportional gain + Ki = -.00015, -- Integral gain + Kd = -.01, -- Derivative gain + integral = 0, -- Integral term accumulator + lastError = 0, -- Last error for derivative term + } + local reactorInstance = { + id = id, + pid = pid, + fuelUsageValues = Deque.new(), + lastRFTValues = Deque.new(), + fuelTempValues = Deque.new(), + caseTempValues = Deque.new(), + rodLevelValues = Deque.new(), + wasteValues = Deque.new(), + steamProductionRateValues = Deque.new(), + storedSteamValues = Deque.new(), + + getFuelUsage = function () return extremeReactor.getFuelStats().fuelConsumedLastTick / 1000 end, + getLastRFT = function () return extremeReactor.getEnergyStats().energyProducedLastTick end, + getFuelTemp = extremeReactor.getFuelTemperature, + getCaseTemp = extremeReactor.getCasingTemperature, + getRodLevel = function () return calculateAverage(extremeReactor.getControlRodsLevels()) end, + getWaste = extremeReactor.getWasteAmount, + getSteamProductionRate = extremeReactor.getHotFluidProducedLastTick, + getSteamCapacity = extremeReactor.getHotFluidAmountMax, + getStoredSteam = extremeReactor.getHotFluidAmount, + getActive = extremeReactor.getActive, + isActivelyCooled = extremeReactor.isActivelyCooled, + setActive = extremeReactor.setActive, + setRodLevels = function (level) setRods(extremeReactor, level) end, + } + setmetatable(reactorInstance, {__index = Reactor}) + local currentTickNumber = math.floor(os.clock() * 20) + reactorInstance:update(currentTickNumber) + return reactorInstance +end + +_G.Reactor = { + newExtremeReactor = newExtremeReactor, + -- newBiggerReactor = newBiggerReactor, +} \ No newline at end of file diff --git a/touchpoint.lua b/src/classes/touchpoint.lua similarity index 72% rename from touchpoint.lua rename to src/classes/touchpoint.lua index 87656b6..db965d7 100644 --- a/touchpoint.lua +++ b/src/classes/touchpoint.lua @@ -1,186 +1,192 @@ -local version = "1.01" ---[[ -The MIT License (MIT) - -Copyright (c) 2013 Lyqyd - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -Edited by DrunkenKas. - See Github: https://github.com/Kasra-G/ReactorController/#readme ---]] - -local function setupLabel(buttonLen, minY, maxY, name) - local labelTable = {} - if type(name) == "table" then - for i = 1, #name do - labelTable[i] = name[i] - end - name = name.label - elseif type(name) == "string" then - local buttonText = string.sub(name, 1, buttonLen - 2) - if #buttonText < #name then - buttonText = " "..buttonText.." " - else - local labelLine = string.rep(" ", math.floor((buttonLen - #buttonText) / 2))..buttonText - buttonText = labelLine..string.rep(" ", buttonLen - #labelLine) - end - for i = 1, maxY - minY + 1 do - if maxY == minY or i == math.floor((maxY - minY) / 2) + 1 then - labelTable[i] = buttonText - else - labelTable[i] = string.rep(" ", buttonLen) - end - end - end - return labelTable, name -end - -local Button = { - draw = function(self) - local old = term.redirect(self.mon) - term.setTextColor(colors.white) - term.setBackgroundColor(colors.black) - for name, buttonData in pairs(self.buttonList) do - if buttonData.active then - term.setBackgroundColor(buttonData.activeColor) - term.setTextColor(buttonData.activeText) - else - term.setBackgroundColor(buttonData.inactiveColor) - term.setTextColor(buttonData.inactiveText) - end - for i = buttonData.yMin, buttonData.yMax do - term.setCursorPos(buttonData.xMin, i) - term.write(buttonData.label[i - buttonData.yMin + 1]) - end - end - if old then - term.redirect(old) - else - term.restore() - end - end, - add = function(self, name, func, xMin, yMin, xMax, yMax, inactiveColor, activeColor, inactiveText, activeText) - local label, name = setupLabel(xMax - xMin + 1, yMin, yMax, name) - if self.buttonList[name] then error("button already exists", 2) end - local x, y = self.mon.getSize() - if xMin < 1 or yMin < 1 or xMax > x or yMax > y then error("button out of bounds", 2) end - self.buttonList[name] = { - func = func, - xMin = xMin, - yMin = yMin, - xMax = xMax, - yMax = yMax, - active = false, - inactiveColor = inactiveColor or colors.red, - activeColor = activeColor or colors.lime, - inactiveText = inactiveText or colors.white, - activeText = activeText or colors.white, - label = label, - } - for i = xMin, xMax do - for j = yMin, yMax do - if self.clickMap[i][j] ~= nil then - --undo changes - for k = xMin, xMax do - for l = yMin, yMax do - if self.clickMap[k][l] == name then - self.clickMap[k][l] = nil - end - end - end - self.buttonList[name] = nil - error("overlapping button", 2) - end - self.clickMap[i][j] = name - end - end - end, - remove = function(self, name) - if self.buttonList[name] then - local button = self.buttonList[name] - for i = button.xMin, button.xMax do - for j = button.yMin, button.yMax do - self.clickMap[i][j] = nil - end - end - self.buttonList[name] = nil - end - end, - run = function(self) - while true do - self:draw() - local event = {self:handleEvents(os.pullEvent(self.side == "term" and "mouse_click" or "monitor_touch"))} - if event[1] == "button_click" then - self.buttonList[event[2]].func() - end - end - end, - handleEvents = function(self, ...) - local event = {...} - if #event == 0 then event = {os.pullEvent()} end - if (self.side == "term" and event[1] == "mouse_click") or (self.side ~= "term" and event[1] == "monitor_touch" and event[2] == self.side) then - local clicked = self.clickMap[event[3]][event[4]] - if clicked and self.buttonList[clicked] then - return "button_click", clicked - end - end - return unpack(event) - end, - toggleButton = function(self, name, noDraw) - self.buttonList[name].active = not self.buttonList[name].active - if not noDraw then self:draw() end - end, - flash = function(self, name, duration) - self:toggleButton(name) - sleep(tonumber(duration) or 0.15) - self:toggleButton(name) - end, - rename = function(self, name, newName) - self.buttonList[name].label, newName = setupLabel(self.buttonList[name].xMax - self.buttonList[name].xMin + 1, self.buttonList[name].yMin, self.buttonList[name].yMax, newName) - if not self.buttonList[name] then error("no such button", 2) end - if name ~= newName then - self.buttonList[newName] = self.buttonList[name] - self.buttonList[name] = nil - for i = self.buttonList[newName].xMin, self.buttonList[newName].xMax do - for j = self.buttonList[newName].yMin, self.buttonList[newName].yMax do - self.clickMap[i][j] = newName - end - end - end - self:draw() - end, -} - -function new(monSide) - local buttonInstance = { - side = monSide or "term", - mon = monSide and peripheral.wrap(monSide) or term.current(), - buttonList = {}, - clickMap = {}, - } - local x, y = buttonInstance.mon.getSize() - for i = 1, x do - buttonInstance.clickMap[i] = {} - end - setmetatable(buttonInstance, {__index = Button}) - return buttonInstance -end - -touchpoint = {new = new} +local version = "1.01" +--[[ +The MIT License (MIT) + +Copyright (c) 2013 Lyqyd + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +Edited by DrunkenKas. + See Github: https://github.com/Kasra-G/ReactorController/#readme +--]] + +local function setupLabel(buttonLen, minY, maxY, name) + local labelTable = {} + if type(name) == "table" then + for i = 1, #name do + labelTable[i] = name[i] + end + name = name.label + elseif type(name) == "string" then + local buttonText = string.sub(name, 1, buttonLen - 2) + if #buttonText < #name then + buttonText = " "..buttonText.." " + else + local labelLine = string.rep(" ", math.floor((buttonLen - #buttonText) / 2))..buttonText + buttonText = labelLine..string.rep(" ", buttonLen - #labelLine) + end + for i = 1, maxY - minY + 1 do + if maxY == minY or i == math.floor((maxY - minY) / 2) + 1 then + labelTable[i] = buttonText + else + labelTable[i] = string.rep(" ", buttonLen) + end + end + end + return labelTable, name +end + +---@class Touchpoint +local Touchpoint = { + drawButton = function(self, buttonName) + self.mon.setTextColor(colors.white) + self.mon.setBackgroundColor(colors.black) + local buttonData = self.buttonList[buttonName] + if buttonData == nil then + error("button does not exist") + end + if buttonData.active then + self.mon.setBackgroundColor(buttonData.activeColor) + self.mon.setTextColor(buttonData.activeText) + else + self.mon.setBackgroundColor(buttonData.inactiveColor) + self.mon.setTextColor(buttonData.inactiveText) + end + for i = buttonData.yMin, buttonData.yMax do + self.mon.setCursorPos(buttonData.xMin, i) + self.mon.write(buttonData.label[i - buttonData.yMin + 1]) + end + end, + drawAllButtons = function(self) + for name, _ in pairs(self.buttonList) do + self:drawButton(name) + end + end, + add = function(self, name, func, xMin, yMin, xMax, yMax, inactiveColor, activeColor, inactiveText, activeText) + local label, name = setupLabel(xMax - xMin + 1, yMin, yMax, name) + if self.buttonList[name] then error("button already exists", 2) end + local x, y = self.mon.getSize() + if xMin < 1 or yMin < 1 or xMax > x or yMax > y then error("button out of bounds", 2) end + self.buttonList[name] = { + func = func, + xMin = xMin, + yMin = yMin, + xMax = xMax, + yMax = yMax, + active = false, + inactiveColor = inactiveColor or colors.red, + activeColor = activeColor or colors.lime, + inactiveText = inactiveText or colors.white, + activeText = activeText or colors.white, + label = label, + } + for i = xMin, xMax do + for j = yMin, yMax do + if self.clickMap[i][j] ~= nil then + --undo changes + for k = xMin, xMax do + for l = yMin, yMax do + if self.clickMap[k][l] == name then + self.clickMap[k][l] = nil + end + end + end + self.buttonList[name] = nil + error("overlapping button", 2) + end + self.clickMap[i][j] = name + end + end + end, + remove = function(self, name) + if self.buttonList[name] then + local button = self.buttonList[name] + for i = button.xMin, button.xMax do + for j = button.yMin, button.yMax do + self.clickMap[i][j] = nil + end + end + self.buttonList[name] = nil + end + end, + run = function(self) + while true do + self:drawAllButtons() + local event = {self:handleEvents(os.pullEvent(self.id == "term" and "mouse_click" or "monitor_touch"))} + if event[1] == "button_click" then + self.buttonList[event[2]].func() + end + end + end, + handleEvents = function(self, ...) + local event = {...} + if #event == 0 then event = {os.pullEvent()} end + if (self.id == "term" and event[1] == "mouse_click") or (self.id ~= "term" and event[1] == "monitor_touch" and event[2] == self.id) then + local clicked = self.clickMap[event[3]][event[4]] + if clicked and self.buttonList[clicked] then + return "button_click", self.id, clicked + end + end + return unpack(event) + end, + setButton = function(self, name, state) + self.buttonList[name].active = state + self:drawButton(name) + end, + toggleButton = function(self, name) + self.buttonList[name].active = not self.buttonList[name].active + self:drawButton(name) + end, + flash = function(self, name, duration) + self:toggleButton(name) + sleep(tonumber(duration) or 0.15) + self:toggleButton(name) + end, + rename = function(self, name, newName) + self.buttonList[name].label, newName = setupLabel(self.buttonList[name].xMax - self.buttonList[name].xMin + 1, self.buttonList[name].yMin, self.buttonList[name].yMax, newName) + if not self.buttonList[name] then error("no such button", 2) end + if name ~= newName then + self.buttonList[newName] = self.buttonList[name] + self.buttonList[name] = nil + for i = self.buttonList[newName].xMin, self.buttonList[newName].xMax do + for j = self.buttonList[newName].yMin, self.buttonList[newName].yMax do + self.clickMap[i][j] = newName + end + end + end + self:drawAllButtons() + end, +} + +local function new(id, mon) + local touchpointInstance = { + id = id, + mon = mon, + buttonList = {}, + clickMap = {}, + } + local x, y = touchpointInstance.mon.getSize() + for i = 1, x do + touchpointInstance.clickMap[i] = {} + end + setmetatable(touchpointInstance, {__index = Touchpoint}) + return touchpointInstance +end + +_G.Touchpoint = {new = new} diff --git a/src/classes/vector2.lua b/src/classes/vector2.lua new file mode 100644 index 0000000..2f155d2 --- /dev/null +++ b/src/classes/vector2.lua @@ -0,0 +1,20 @@ +---@class Vector2 +local Vector2 = { + ---@type number + x = nil, + ---@type number + y = nil, +} + +---@param x number +---@param y number +---@return Vector2 +local function new(x, y) + return vector.new(x, y) +end + +_G.Vector2 = { + new = new, + zero = new(0, 0), + one = new(1, 1), +} diff --git a/src/config/projectConfigs.lua b/src/config/projectConfigs.lua new file mode 100644 index 0000000..b1198ba --- /dev/null +++ b/src/config/projectConfigs.lua @@ -0,0 +1,4 @@ +_G.UPDATE_CONFIG = { + FIRST_TIME_SETUP_COMPLETE = false, + AUTOUPDATE = false, +} \ No newline at end of file diff --git a/src/constants/projectConstants.lua b/src/constants/projectConstants.lua new file mode 100644 index 0000000..e69de29 diff --git a/src/scripts/controller.lua b/src/scripts/controller.lua new file mode 100644 index 0000000..4507051 --- /dev/null +++ b/src/scripts/controller.lua @@ -0,0 +1,401 @@ +---@type table +_G.monitors = {} +---@type table +_G.reactors = {} +-- _G.turbines = {} + +_G.fluidBuffers = {} + +---@type table +_G.energyBuffers = {} + +_G.masterAutoRodControl = true + +-- These are averaged when possible +---@class OverallStats +_G.overallStats = { + storedLastTick = 0, + storedThisTick = 0, + lastRFT = 0, + rfLost = 0, + steamConsumedLastTick = 2000, + fuelUsage = 0, + waste = 0, + capacity = 1000, + efficiency = function () + return _G.overallStats.lastRFT / _G.overallStats.fuelUsage + end +} + +_G.selectedReactor = nil + +-- -@class AveragedTurbineStatistics +-- -@field fuelUsage number +-- -@field fuelUsageValues Deque +-- -@field lastRFT number +-- -@field lastRFTValues Deque +-- -@field waste number +-- -@field wasteValues Deque +-- -@field fuelTemp number +-- -@field fuelTempValues Deque +-- -@field caseTemp number +-- -@field caseTempValues Deque + +local function updateOverallStats() + _G.overallStats.storedLastTick = 0 + _G.overallStats.storedThisTick = 0 + _G.overallStats.capacity = 0 + for id, energyBuffer in pairs(energyBuffers) do + _G.overallStats.storedLastTick = _G.overallStats.storedLastTick + energyBuffer.averageEnergyStoredLastTick + _G.overallStats.storedThisTick = _G.overallStats.storedThisTick + energyBuffer.averageEnergyStoredThisTick + _G.overallStats.capacity = _G.overallStats.capacity + energyBuffer.capacity + end + + _G.overallStats.fuelUsage = 0 + _G.overallStats.waste = 0 + _G.overallStats.lastRFT = 0 + _G.overallStats.steamProductionRate = 0 + _G.overallStats.storedSteam = 0 + _G.overallStats.steamCapacity = 0 + _G.overallStats.steamConsumedLastTick = 4000 + + for id, reactor in pairs(reactors) do + if reactor.isActivelyCooled then + _G.overallStats.steamProductionRate = _G.overallStats.steamProductionRate + reactor.averageSteamProductionRate + _G.overallStats.storedSteam = _G.overallStats.storedSteam + reactor.averageStoredSteam + _G.overallStats.steamCapacity = _G.overallStats.steamCapacity + reactor.steamCapacity + end + _G.overallStats.fuelUsage = _G.overallStats.fuelUsage + reactor.averageFuelUsage + _G.overallStats.lastRFT = _G.overallStats.lastRFT + reactor.averageLastRFT + _G.overallStats.waste = _G.overallStats.waste + reactor.waste + end + + _G.overallStats.rfLost = math.floor(_G.overallStats.lastRFT + _G.overallStats.storedLastTick - _G.overallStats.storedThisTick + 0.5) +end + +_G.btnOn = nil +_G.minb = nil +_G.maxb = nil + +_G.SECONDS_TO_AVERAGE = 0.5 + + +-- Function to calculate the average of an array of values +local function calculateAverage(array) + local sum = 0 + local count = 0 + for _, value in pairs(array) do + sum = sum + value + count = count + 1 + end + return sum / count +end + +-- TODO: Move to 2 or 3 stage PID controller to eliminate integral windup (oscillations) +-- TODO: Provide multiple PID presets for different sizes of reactors + -- User can choose the one that works the best for each reactor. +-- TODO: Dynamic setting of PID constants based on measured change in RFT per % change in control rods +-- TODO: Try using % of max RFT generation as basis of PID controller +-- TODO: Try using gain scheduling to reduce integral windup + +--TODO: Update this to handle settings for multiple reactors and turbines +local function saveToConfig() + local file = fs.open(tag.."Serialized.txt", "w") + local configs = { + ["_G.maxb"] = _G.maxb, + ["_G.minb"] = _G.minb, + ["_G.rod"] = _G.rod, + ["_G.btnOn"] = _G.btnOn, + graphsToDraw = graphsToDraw, + XOffs = XOffs, + } + local serialized = textutils.serialize(configs) + file.write(serialized) + file.close() +end + + + +-- local function updateStats() + -- if (_G.reactorVersion == "Big Reactors") then + -- _G.storedThisTick = _G.reactor.getEnergyStored() + -- _G.lastRFT = _G.reactor.getEnergyProducedLastTick() + -- _G.rod = _G.reactor.getControlRodLevel(0) + -- _G.fuelUsage = _G.reactor.getFuelConsumedLastTick() / 1000 + -- _G.waste = _G.reactor.getWasteAmount() + -- _G.fuelTemp = _G.reactor.getFuelTemperature() + -- _G.caseTemp = _G.reactor.getCasingTemperature() + -- -- Big Reactors doesn't give us a way to directly query RF capacity through CC APIs + -- _G.capacity = math.max(_G.capacity, _G.reactor.getEnergyStored) + -- elseif (_G.reactorVersion == "Extreme Reactors") then + -- local bat = _G.reactor.getEnergyStats() + -- local fuel = _G.reactor.getFuelStats() + + -- _G.storedThisTick = bat.energyStored + -- _G.lastRFT = bat.energyProducedLastTick + -- _G.capacity = bat.energyCapacity + -- _G.rod = calculateAverage(_G.reactor.getControlRodsLevels()) + -- _G.fuelUsage = fuel.fuelConsumedLastTick / 1000 + -- _G.waste = _G.reactor.getWasteAmount() + -- _G.fuelTemp = _G.reactor.getFuelTemperature() + -- _G.caseTemp = _G.reactor.getCasingTemperature() + -- elseif (_G.reactorVersion == "Bigger Reactors") then + -- _G.storedThisTick = _G.reactor.battery().stored() + -- _G.lastRFT = _G.reactor.battery().producedLastTick() + -- _G.capacity = _G.reactor.battery().capacity() + -- _G.rod = _G.reactor.getControlRod(0).level() + -- _G.fuelUsage = _G.reactor.fuelTank().burnedLastTick() / 1000 + -- _G.waste = _G.reactor.fuelTank().waste() + -- _G.fuelTemp = _G.reactor.fuelTemperature() + -- _G.caseTemp = _G.reactor.casingTemperature() + -- end + -- _G.rfLost = math.floor(_G.lastRFT + _G.storedLastTick - _G.storedThisTick + 0.5) +-- end + +--TODO: Update this to handle settings for multiple reactors and turbines +local function loadFromConfig() + _G.invalidDim = false + local legacyConfigExists = fs.exists(tag..".txt") + local newConfigExists = fs.exists(tag.."Serialized.txt") + if (newConfigExists) then + local file = fs.open(tag.."Serialized.txt", "r") + print("Config file "..tag.."Serialized.txt found! Using configurated settings") + + local serialized = file.readAll() + local deserialized = textutils.unserialise(serialized) + + _G.maxb = deserialized.maxb + _G.minb = deserialized.minb + _G.rod = deserialized.rod + _G.btnOn = deserialized.btnOn + graphsToDraw = deserialized.graphsToDraw + XOffs = deserialized.XOffs + else + print("Config file not found, generating default settings!") + + _G.maxb = 70 + _G.minb = 30 + _G.rod = 80 + _G.btnOn = false + end + _G.btnOff = not _G.btnOn + _G.reactor.setActive(_G.btnOn) +end + +---@param monitorID string +local function connectMonitor(monitorID) + print("Monitor "..monitorID.." connected!") + monitors[monitorID] = Monitor.new(monitorID) +end + +---@param reactorID string +local function connectExtremeReactor(reactorID) + print("Extreme Reactor "..reactorID.." connected!") + reactors[reactorID] = Reactor.newExtremeReactor(reactorID) + _G.selectedReactor = reactors[reactorID] +end + +---@param energyBufferID string +local function connectForgeEnergyBuffer(energyBufferID) + print("Energy Buffer "..energyBufferID.." connected!") + energyBuffers[energyBufferID] = EnergyBuffer.newForgeEnergyBuffer(energyBufferID) +end + +---@param energyBufferID string +local function connectReactorEnergyBuffer(energyBufferID) + print("Reactor Energy Buffer "..energyBufferID.." connected!") + energyBuffers[energyBufferID] = EnergyBuffer.newReactorEnergyBuffer(energyBufferID) +end + +local function firePeriphalAttachEventForAllPeripherals() + for _, id in pairs(peripheral.getNames()) do + os.queueEvent("peripheral", id) + end +end + +local function redrawMonitors() + for _, monitor in pairs(monitors) do + -- Eventually pass in our list of reactors and turbines to draw stats for. + monitor:draw() + end +end + +---@param currentTickNumber number +local function updateEnergyBuffers(currentTickNumber) + for _, energyBuffer in pairs(energyBuffers) do + energyBuffer:update(currentTickNumber) + end +end + +---@param currentTickNumber number +local function updateReactors(currentTickNumber) + for _, reactor in pairs(reactors) do + reactor:update(currentTickNumber) + end +end + +function _G.setReactors(active) + for _, reactor in pairs(reactors) do + reactor.setActive(active) + end +end + +local function updateReactorRods() + for _, reactor in pairs(reactors) do + reactor:updateRods() + end +end + +---@param peripheralID string +local function handlePeripheralDetach(peripheralID) + if monitors[peripheralID] ~= nil then + print("Monitor "..peripheralID.." disconnected!") + monitors[peripheralID] = nil + end + + if energyBuffers[peripheralID] ~= nil then + print("Energy Buffer "..peripheralID.." disconnected!") + energyBuffers[peripheralID] = nil + end + + if reactors[peripheralID] ~= nil then + print("Reactor "..peripheralID.." disconnected!") + reactors[peripheralID] = nil + end +end + +---@param peripheralID string +---@param peripheralType string +local function handlePeripheralAttach(peripheralID, peripheralType) + if peripheralType == "monitor" then + connectMonitor(peripheralID) + elseif peripheralType == "BigReactors-Reactor" then + connectExtremeReactor(peripheralID) + connectReactorEnergyBuffer(peripheralID) + elseif peripheralType == "energy_storage" then + connectForgeEnergyBuffer(peripheralID) + else + print("Unknown peripheral", peripheralID, "of type", peripheralType, "attached to network") + end +end + + +_G.TICKS_TO_REDRAW = 1 +local function runLoop(currentTickNumber) + updateEnergyBuffers(currentTickNumber) + updateReactors(currentTickNumber) + updateOverallStats() + if currentTickNumber % TICKS_TO_REDRAW == 0 then + redrawMonitors() + end +end + +local function eventListener() + while true do + local event = { os.pullEvent() } + + if event[1] == "monitor_touch" or event[1] == "monitor_resize" then + local monitor = monitors[event[2]] + if monitor ~= nil then + monitor:handleEvents(event) + end + elseif event[1] == "peripheral" then + handlePeripheralAttach(event[2], peripheral.getType(event[2])) + elseif event[1] == "peripheral_detach" then + handlePeripheralDetach(event[2]) + end + end +end + +local function loop() + local loopEventName = "yield" + local curTime = math.floor(os.clock() * 20) + local lastTime = curTime + + os.sleep(0) + while true do + curTime = math.floor(os.clock() * 20) + + local reactorCount = 0 + for _, reactor in pairs(_G.reactors) do + reactorCount = reactorCount + 1 + end + if reactorCount < 1 then + print("Reactor not detected! Please connect a reactor!") + sleep(1) + elseif curTime < lastTime + 1 then + os.queueEvent(loopEventName) + os.pullEvent(loopEventName) + elseif curTime > lastTime + 1 then + -- We have missed the data from the last tick + print("Missed last", curTime - lastTime - 1, "ticks!", curTime) + runLoop(curTime) + else + local t = os.epoch("utc") + -- Guaranteed to run at the start of a new tick + while os.epoch("utc") - t < 2 do + os.queueEvent(loopEventName) + os.pullEvent(loopEventName) + end + runLoop(curTime) + updateReactorRods() + os.sleep(0) + end + lastTime = curTime + end +end + +local function detectReactor() + -- Bigger Reactors V1. + local reactor_bigger_v1 = getPeripheral("bigger-reactor") + _G.reactor = reactor_bigger_v1 ~= nil and peripheral.wrap(reactor_bigger_v1) + if (_G.reactor ~= nil) then + _G.reactorVersion = "Bigger Reactors" + return true + end + + -- Bigger Reactors V2 + local reactor_bigger_v2 = getPeripheral("BiggerReactors_Reactor") + _G.reactor = reactor_bigger_v2 ~= nil and peripheral.wrap(reactor_bigger_v2) + if (_G.reactor ~= nil) then + _G.reactorVersion = "Bigger Reactors" + return true + end + + -- Big Reactors or Extreme Reactors + local reactor_extreme_or_big = getPeripheral("BigReactors-Reactor") + _G.reactor = reactor_extreme_or_big ~= nil and peripheral.wrap(reactor_extreme_or_big) + if (_G.reactor ~= nil) then + _G.reactorVersion = (_G.reactor.mbIsConnected ~= nil) and "Extreme Reactors" or "Big Reactors" + return true + end + return false +end + +--Entry point +function _G.main() + term.setBackgroundColor(colors.black) + term.clear() + term.setCursorPos(1,1) + + _G.monitors = {} + _G.reactors = {} + _G.turbines = {} + _G.energyBuffers = {} + + _G.maxb = 70 + _G.minb = 30 + _G.rod = 80 + _G.btnOn = true + + -- Manually fire the "peripheral" event to make sure all the connected peripherals are initialized correctly. + firePeriphalAttachEventForAllPeripherals() + + -- term.clear() + -- term.setCursorPos(1,1) + -- print("Reactor Controller Version "..version) + -- print("Reactor Mod: "..reactorVersion) + --main loop + + parallel.waitForAll(loop, eventListener) +end diff --git a/src/scripts/main.lua b/src/scripts/main.lua new file mode 100644 index 0000000..b56e779 --- /dev/null +++ b/src/scripts/main.lua @@ -0,0 +1,91 @@ +local function insertAllFilepathsInDirectoryToTable(path, outputFilenames) + for _, file in pairs(fs.list(path)) do + local filepath = fs.combine(path, file) + if fs.isDir(filepath) then + insertAllFilepathsInDirectoryToTable(filepath, outputFilenames) + else + table.insert(outputFilenames, filepath) + end + end +end + +local function executeAllLuaFilesInSrcFolderExceptMain() + local filepaths = {} + insertAllFilepathsInDirectoryToTable("src", filepaths) + for _, filepath in pairs(filepaths) do + if filepath ~= "src/scripts/main.lua" then + shell.run(filepath) + end + end +end + +local function promptAndReadInputAndLoopUntilValid(prompt, validAnswersList) + local validAnswer = nil + local validAnswersTable = {} + for _, answer in pairs(validAnswersList) do + validAnswersTable[answer] = true + end + while not validAnswer do + print(prompt) + local input = read() + if validAnswersTable[input] then + validAnswer = input + break + end + print("Invalid response. Please try again!") + end + return validAnswer +end + +local function runFirstTimeSetup() + local response = promptAndReadInputAndLoopUntilValid("Do you want to automatically install updates? (y/n)", {"y", "n"}) + if response == "y" then + UPDATE_CONFIG.AUTOUPDATE = true + end +end + +local function runUpdateLogic() + local updateAvailable = _G.UpdateScript.checkForUpdate() + if not updateAvailable then + return false + end + + print("Update available!") + -- Set some state variable somewhere to tell us an update is available + if not UPDATE_CONFIG.AUTOUPDATE then + print("Automatic update skipped because it's not enabled!") + return false + end + print("Automatic update is enabled! Updating...") + return _G.UpdateScript.performUpdate() +end + +local function start() + executeAllLuaFilesInSrcFolderExceptMain() + -- Let reactors run for 1 second on world load. + sleep(1) + + term.clear() + term.setCursorPos(1,1) + + ConfigUtil.writeAllConfigsAsDefaults() + ConfigUtil.readAllConfigs() + + if not UPDATE_CONFIG.FIRST_TIME_SETUP_COMPLETE then + print("First time startup detected!") + runFirstTimeSetup() + UPDATE_CONFIG.FIRST_TIME_SETUP_COMPLETE = true + ConfigUtil.writeAllConfigs() + end + + local didUpdate = runUpdateLogic() + if didUpdate then + print("Successfully updated! Rebooting...") + os.reboot() + end + + -- For now, main() is in controller.lua + main() +end + +start() diff --git a/src/scripts/update.lua b/src/scripts/update.lua new file mode 100644 index 0000000..3331aaf --- /dev/null +++ b/src/scripts/update.lua @@ -0,0 +1,133 @@ +local LOCAL_REPO_DETAILS_FILENAME = "/commit.txt" +local GITHUB_CONSTANTS = { + OWNER = "Kasra-G", + REPO = "ReactorController", + BRANCH = "development", +} + +local FILES_TO_DELETE_ON_UPDATE = { + "/src", + "/defaults", + "/state", + "startup", +} + +local function getRemoteRepoSHA() + local response = http.get("https://api.github.com/repos/"..GITHUB_CONSTANTS.OWNER.."/"..GITHUB_CONSTANTS.REPO.."/commits/"..GITHUB_CONSTANTS.BRANCH) + local responseJSON = response.readAll() + return textutils.unserialiseJSON(responseJSON).sha +end + +local function getLocalRepoSHA(filepath) + local file = fs.open(filepath, "r") + if file == nil then + print("Local version file not found! Assuming there is an update available.") + return "" + end + local contents = file.readAll() + file.close() + return textutils.unserialiseJSON(contents) +end + +local function saveRepoSHA(repoSHA, path) + local file = fs.open(path, "w") + file.write(textutils.serializeJSON(repoSHA)) + file.close() +end + +local function downloadGitHubFileByPath(filepath, tempFoldername) + if tempFoldername == nil then + tempFoldername = "" + end + + local endpoint = "https://raw.githubusercontent.com/"..GITHUB_CONSTANTS.OWNER.."/"..GITHUB_CONSTANTS.REPO.."/refs/heads/"..GITHUB_CONSTANTS.BRANCH.."/"..filepath + local response = http.get(endpoint) + local contents = response.readAll() + local file = fs.open(fs.combine(tempFoldername, filepath), "w") + file.write(contents) + file.close() + print("File", filepath, "downloaded!") +end + +local function getGitHubTreeDetails(treeSHA) + local endpoint = "https://api.github.com/repos/"..GITHUB_CONSTANTS.OWNER.."/"..GITHUB_CONSTANTS.REPO.."/git/trees/"..treeSHA + local response = http.get(endpoint) + local contents = response.readAll() + return textutils.unserialiseJSON(contents) +end + +local function downloadGitHubTreeRecursively(path, treeSHA, tempFoldername) + local treeDetails = getGitHubTreeDetails(treeSHA) + for _, treeEntry in pairs(treeDetails.tree) do + local subfilePath = path.."/"..treeEntry.path + if treeEntry.type == "tree" then + downloadGitHubTreeRecursively(subfilePath, treeEntry.sha, tempFoldername) + elseif treeEntry.type == "blob" then + downloadGitHubFileByPath(subfilePath, tempFoldername) + end + end +end + +local function downloadRemoteSrcDirectory(remoteRepoRootTreeSHA, tempFoldername) + local remoteRepoRootTreeDetails = getGitHubTreeDetails(remoteRepoRootTreeSHA) + local srcDirectoryName = "src" + + for _, treeEntry in pairs(remoteRepoRootTreeDetails.tree) do + if treeEntry.path == srcDirectoryName and treeEntry.type == "tree" then + downloadGitHubTreeRecursively(srcDirectoryName, treeEntry.sha, tempFoldername) + end + end +end + +--- Checks if there is an update to install +---@return boolean +local function checkForUpdate() + local remoteRepoSHA + local success, err = pcall(function() remoteRepoSHA = getRemoteRepoSHA() end) + if not success then + print("Could not check for update with error", err) + return false + end + local localRepoSHA = getLocalRepoSHA(LOCAL_REPO_DETAILS_FILENAME) + return localRepoSHA ~= remoteRepoSHA +end + +--- Updates and saves the project. src and startup files are deleted and then redownloaded. +local function performUpdate() + local remoteRepoSHA + local success, err = pcall(function() remoteRepoSHA = getRemoteRepoSHA() end) + if not success then + print("Could not reach remote repository with error", err) + return false + end + + local tempFoldername = "temp" + + success, err = pcall( + function() + downloadRemoteSrcDirectory(remoteRepoSHA, tempFoldername) + downloadGitHubFileByPath("startup", tempFoldername) + end + ) + if not success then + print("Repo download failed with error", err) + fs.delete(tempFoldername) + return false + end + for _, filepath in pairs(FILES_TO_DELETE_ON_UPDATE) do + fs.delete(filepath) + end + for _, filename in pairs(fs.list(tempFoldername)) do + fs.move(fs.combine(tempFoldername, filename), filename) + end + + saveRepoSHA(remoteRepoSHA, LOCAL_REPO_DETAILS_FILENAME) + fs.delete(tempFoldername) + return success +end + +_G.UpdateScript = { + checkForUpdate = checkForUpdate, + performUpdate = performUpdate, + saveRepoDetails = saveRepoSHA, +} diff --git a/src/util/config.lua b/src/util/config.lua new file mode 100644 index 0000000..86c2f40 --- /dev/null +++ b/src/util/config.lua @@ -0,0 +1,119 @@ + +local CONFIGS = {} +CONFIGS["update"] = UPDATE_CONFIG + +local DEFAULTS_PATH = "/defaults/" +local OVERRIDES_PATH = "/overrides/" +local STATE_PATH = "/state/" +local CONFIG_EXTENSION = ".default.conf" +local OVERRIDE_EXTENSION = ".override.conf" +local STATE_EXTENSION = ".state.conf" + +local function isTableEmpty(table) + for _, _ in pairs(table) do + return false + end + return true +end + +local function serializeTableAndWriteToFile(table, path) + local file = fs.open(path, "w") + file.write(textutils.serialize(table)) + file.close() +end + +local function readFileAndReturnDeserialized(path) + local file = fs.open(path, "r") + if file == nil then + return {} + end + local contents = file.readAll() + file.close() + return textutils.unserialise(contents) +end + +local function readState(stateID) + return readFileAndReturnDeserialized(STATE_PATH..stateID..STATE_EXTENSION) +end + +local function readConfigDefaults(configID) + return readFileAndReturnDeserialized(DEFAULTS_PATH..configID..CONFIG_EXTENSION) +end + +local function readConfigOverrides(configID) + return readFileAndReturnDeserialized(OVERRIDES_PATH..configID..OVERRIDE_EXTENSION) +end + +local function spread(source, destination) + for key, value in pairs(source) do + destination[key] = value + end +end + +local function readConfig(configID) + local configData = CONFIGS[configID] + local defaults = readConfigDefaults(configID) + local overrides = readConfigOverrides(configID) + spread(defaults, configData) + spread(overrides, configData) +end + +local function writeConfig(configID) + local configData = CONFIGS[configID] + local defaults = readConfigDefaults(configID) + local overrides = {} + for key, value in pairs(configData) do + if configData[key] ~= defaults[key] then + overrides[key] = value + end + end + + if isTableEmpty(overrides) then + fs.delete(OVERRIDES_PATH..configID..OVERRIDE_EXTENSION) + return + end + serializeTableAndWriteToFile(overrides, OVERRIDES_PATH..configID..OVERRIDE_EXTENSION) +end + +local function writeState(stateID, stateData) + serializeTableAndWriteToFile(stateData, STATE_PATH..stateID..STATE_EXTENSION) +end + +local function writeConfigAsDefault(configID) + local configData = CONFIGS[configID] + serializeTableAndWriteToFile(configData, DEFAULTS_PATH..configID..CONFIG_EXTENSION) +end + +local function writeAllConfigsAsDefaults() + for configID, _ in pairs(CONFIGS) do + writeConfigAsDefault(configID) + end +end + +local function readAllConfigs() + for configID, _ in pairs(CONFIGS) do + readConfig(configID) + end +end + +local function writeAllConfigs() + for configID, _ in pairs(CONFIGS) do + writeConfig(configID) + end +end + +local function resetConfig(configID) + fs.delete(OVERRIDES_PATH..configID..OVERRIDE_EXTENSION) + readConfig(configID) +end + +_G.ConfigUtil = { + writeAllConfigsAsDefaults = writeAllConfigsAsDefaults, + writeAllConfigs = writeAllConfigs, + readAllConfigs = readAllConfigs, + writeConfig = writeConfig, + writeState = writeState, + readConfig = readConfig, + readState = readState, + resetConfig = resetConfig, +} diff --git a/src/util/draw.lua b/src/util/draw.lua new file mode 100644 index 0000000..efe032f --- /dev/null +++ b/src/util/draw.lua @@ -0,0 +1,64 @@ + +---comment Draws a rectangle with a border +---@param mon table +---@param color any +---@param offset Vector2 +---@param size Vector2 +local function drawFilledBox(mon, color, offset, size) + if size.x <= 0 or size.y <= 0 then + return + end + local old = term.redirect(mon) + local endCoord = offset + size - Vector2.one + paintutils.drawFilledBox(offset.x, offset.y, endCoord.x, endCoord.y, color) + term.redirect(old) +end + +local function drawBox(mon, color, offset, size) + if size.x <= 0 or size.y <= 0 then + return + end + local old = term.redirect(mon) + local endCoord = offset + size - Vector2.one + paintutils.drawBox(offset.x, offset.y, endCoord.x, endCoord.y, color) + term.redirect(old) +end + +---comment Draws a rectangle with a fill color and border +---@param mon table +---@param innerColor any +---@param outerColor any +---@param offset Vector2 +---@param size Vector2 +local function drawFilledBoxWithBorder(mon, innerColor, outerColor, offset, size) + if size.x <= 0 or size.y <= 0 then + return + end + local old = term.redirect(mon) + local endCoord = offset + size - Vector2.one + paintutils.drawBox(offset.x, offset.y, endCoord.x, endCoord.y, outerColor) + + if size.x > 2 and size.y > 2 then + paintutils.drawFilledBox(offset.x + 1, offset.y + 1, endCoord.x - 1, endCoord.y - 1, innerColor) + end + term.redirect(old) +end + +---comment Draws text on the screen +---@param mon table +---@param text string +---@param pos Vector2 +---@param backgroundColor any +---@param textColor any +local function drawText(mon, text, pos, backgroundColor, textColor) + mon.setCursorPos(pos.x, pos.y) + local len = #text + mon.blit(text, string.rep(colors.toBlit(textColor), len), string.rep(colors.toBlit(backgroundColor), len)) +end + +_G.DrawUtil = { + drawFilledBoxWithBorder = drawFilledBoxWithBorder, + drawFilledBox = drawFilledBox, + drawBox = drawBox, + drawText = drawText, +} diff --git a/startup b/startup new file mode 100644 index 0000000..2a451b3 --- /dev/null +++ b/startup @@ -0,0 +1 @@ +shell.run("/src/scripts/main.lua") diff --git a/updater.lua b/updater.lua deleted file mode 100644 index ea99219..0000000 --- a/updater.lua +++ /dev/null @@ -1,96 +0,0 @@ -local version = "1.0" - ---Github: https://github.com/Kasra-G/ReactorController/#readme - --- ["filename"] = "pastebinCode" -local filesToUpdate = { - ["/reactorController.lua"] = "b17hfTqe", - ["/usr/apis/touchpoint.lua"] = "nx9pkLbJ", - ["/update_reactor.lua"] = "w6vVtrLb", -} - -local function getPastebinFileContents(filename, pastebinCode) - local tempFilename = "/temp" .. filename - - -- Avoid calling pastebin API directly to be more robust towards any future API changes - shell.run("pastebin", "get", pastebinCode, tempFilename) - local tempFile = fs.open(tempFilename, "r") - - if not tempFile then - return nil - end - - local fileContents = tempFile.readAll() - tempFile.close() - fs.delete(tempFilename) - return fileContents -end - --- Requires HTTP to be enabled -local function getVersion(fileContents) - if not fileContents then - return nil - end - - local _, numberChars = fileContents:lower():find('local version = "') - - if not numberChars then - return nil - end - - local fileVersion = "" - local char = "" - - while char ~= '"' do - numberChars = numberChars + 1 - char = fileContents:sub(numberChars,numberChars) - fileVersion = fileVersion .. char - end - - fileVersion = fileVersion:sub(1,#fileVersion-1) -- Remove quotes around the version number - return fileVersion -end - -local function updateFile(filename, pastebinCode) - if fs.isDir(filename) then - print("[Error] " .. filename .. " is a directory") - return - end - local pastebinContents = getPastebinFileContents(filename, pastebinCode) - - if not pastebinContents then - print("[Error] " .. filename .. " has an invalid link") - end - - local pastebinVersion = getVersion(pastebinContents) - if not pastebinVersion then - print("[Error] the pastebin code for " .. filename .. " does not have a version variable") - return - end - - local localVersion = nil - if fs.exists(filename) then - local localFile = fs.open(filename,"r") - localVersion = getVersion(localFile.readAll()) - localFile.close() - end - - if localVersion ~= pastebinVersion then - local localFile = fs.open(filename,"w") - localFile.write(pastebinContents) - localFile.close() - print("[Success] " .. filename .. " has been updated to version " .. pastebinVersion) - elseif pastebinVersion == localVersion then - print("[Success] No update required: " .. filename .. " is already the latest version") - end -end - -local function main() - fs.makeDir("/usr") - fs.makeDir("/usr/apis") - for filename, pastebinCode in pairs(filesToUpdate) do - updateFile(filename, pastebinCode) - end -end - -main()