DarknessARises1

Mekanism Monitor/Controller using CC

May 13th, 2025 (edited)
846
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 151.68 KB | Gaming | 0 0
  1. --[[
  2. Mekanism Monitor for ComputerCraft (v86.49-Fixes)
  3. Author: DarknessARises1
  4. This version corrects several visual glitches in the UI for a cleaner and more consistent look.
  5.  
  6. Features:
  7. - Multi-Monitor Support: The script now detects and uses all connected monitors, with each monitor having independent page controls.
  8. - Asymmetric Display: Terminal shows a single large panel, while monitors can show a 4-panel grid.
  9. - Synchronized Config Menu: The configuration menu is mirrored on all screens for setup.
  10. - All device types are fully supported with detailed drawing functions.
  11. - Robust, non-compacted code for better stability and readability.
  12.  
  13. FIXES (v86.45 - UI & Event Loop):
  14. - Fixed issue with Config and Go button overlapping, causing config to launch on go press. Also noticed when go button was working the go menu would disappear and act like it was still there. Also fixed
  15. - Fixed unresponsive keypad input by prioritizing its events in the main event loop.
  16. - Fixed a crash in the configuration menu when the "Industrial Alarm" was disabled.
  17. - Corrected a visual glitch where the "Power Down Button" label had the wrong background color.
  18. - Fixed a crash when opening the keypad while a Fission Reactor was on screen.
  19. - Fixed a Bug that caused program to freeze when dragging fusion reactor control slider on the terminal, removed it to avoid waiting silent crashes.
  20. - Fixed redstone firing requiring two clicks of ignite button to send signal
  21.  
  22. FIXES (v86.44 - SCRAM Control & Heated Coolant):
  23. - Added a progress bar for Heated Coolant to the Fission Reactor panel.
  24. - SCRAM state now automatically resets when coolant levels reach 50% or more.
  25. - Added a "[Cancel SCRAM]" button that appears on the Fission Reactor panel during a SCRAM event, allowing for manual override.
  26. - Fixed the "END" and "Quit" buttons to properly terminate the program from any screen.
  27. - Auto-SCRAM will no longer trigger on an idle reactor, preventing SCRAM loops after a manual cancel.
  28.  
  29. Setup:
  30. 1. Place ComputerCraft computer adjacent to Mekanism devices.
  31. 2. Place one or more Monitor peripherals next to the computer.
  32. 3. (Optional) Place one or more Speaker peripherals next to the computer for audible alerts.
  33. 4. Run the script. If it's your first time, the config menu will appear.
  34. 5. To stop: Use the on-screen "END" button or press Ctrl+T.
  35. --]]
  36.  
  37. -- =================================================================
  38. -- Configuration
  39. -- =================================================================
  40.  
  41. -- if you want to increase how fast it reads and updates the display change the updateInterval and displayRefreshRate to smaller numbers
  42.  
  43. local configFile = "mek_monitor.cfg"
  44. local debugMode = false
  45. local updateInterval = 0.5
  46. local joulesToFeConversionFactor = 2.5
  47. local TICKS_PER_SECOND = 20
  48. local devicesPerPageMonitor = 4
  49. local devicesPerPageTerminal = 1 -- Asymmetric display setting
  50. local turbineNamePrefix = "turbineValve"
  51. local fusionReactorNamePrefix = "fusionReactorLogicAdapter"
  52. local inductionMatrixNamePrefix = "inductionPort"
  53. local boilerNamePrefix = "boilerValve"
  54. local thermalEvaporationNamePrefix = "thermalEvaporationValve"
  55. local fissionReactorNamePrefix = "fissionReactorLogicAdapter"
  56. local displayRefreshDelay = 0.05
  57. local globalButtonFeedbackDuration = 2
  58. local minInjectionRate = 2
  59. local maxInjectionRate = 98
  60. local injectionRateStep = 2
  61. local matrixRateThreshold = 0.001
  62. local scanForNewDevicesInterval = 5
  63.  
  64. -- Fission Reactor Safety
  65. local fissionCriticalTemp = 1800 -- Temp (K) to trigger automatic SCRAM. Meltdown is at 2000K.
  66. local fissionCriticalDamage = 90 -- Damage % to trigger automatic SCRAM.
  67. local fissionLowCoolantPercent = 0.10
  68. local fissionSafeCoolantPercent = 0.50 -- Coolant % to auto-reset SCRAM.
  69. local fissionScramResetTempK = 300
  70.  
  71. local dumpingModes = {
  72.     { apiName = "IDLE", displayName = "Idle" },
  73.     { apiName = "DUMPING_EXCESS", displayName = "Excess" },
  74.     { apiName = "DUMPING", displayName = "Dump" },
  75. }
  76.  
  77. local skipIsFormedValidation = false
  78.  
  79. local dynamicColorThresholds = {
  80.     caseTemp = {
  81.         noDt = { yellow = 60e6, orange = 1.875e9, red = 2.25e9 },
  82.         dtFuel = { yellow = 60e6, orange = 1.0875e10, red = 1.305e10 }
  83.     },
  84.     plasmaTemp = {
  85.         noDt = { yellow = 160e6, orange = 5.665e9, red = 6.78e9 },
  86.         dtFuel = { yellow = 160e6, orange = 2.9665e10, red = 3.558e10 }
  87.     },
  88.     energy = {
  89.         noDt = { yellow = 7.8e6, orange = 11.7e6, red = 14.0e6 },
  90.         dtFuel = { yellow = 39.8e6, orange = 59.7e6, red = 71.6e6 }
  91.     },
  92.     boilerTemp = { yellow = 400, orange = 600, red = 1000 },
  93.     thermalTemp = { yellow = 320, orange = 350, red = 400 },
  94.     fissionTemp = { yellow = 1000, orange = 1500, red = 1800 }
  95. }
  96.  
  97. -- =================================================================
  98. -- Global State and Initialization
  99. -- =================================================================
  100. local monitors = {}
  101.  
  102. -- State management for asymmetric display
  103. local sharedData = {
  104.     programRunning = true,
  105.     isGoToMenuOpen = false,
  106.     monitorRunning = false,
  107.     isMenuOpen = false,
  108.     deviceStats = {},
  109.     lastError = {},
  110.     currentPageTerm = 1,
  111.     totalPagesTerm = 1,
  112.     monitorStates = {},
  113.     totalPagesMon = 1,
  114.     pageButtonRegions = {},
  115.     quadrantRegions = {},
  116.     fissionScrollOffsets = {},
  117.     actionQueue = {},
  118.     scrammingReactors = {},
  119.     speakers = {},
  120.     flashToggle = true,
  121.     sirenBuffers = {},
  122.     overallTurbineStats = { energyStored = 0, maxEnergy = 0, energyFilledPercentage = 0, productionRate = 0, maxProduction = 0, steamAmount = 0, steamCapacity = 0, steamFilledPercentage = 0, formedCount = 0, totalTurbineCount = 0 },
  123.     globalModeButtonFeedback = { modeAPIName = nil, colorKey = nil, expiryTime = 0 },
  124.     globallyConsistentTurbineMode = nil,
  125.     foundDevices = {},
  126.     config = {
  127.         tempUnit = "K",
  128.         deviceOrder = { "sps", "fusionReactor", "fissionReactor", "inductionMatrix", "dynamicTank", "thermalEvaporation", "boiler", "turbine", "turbineSummary" },
  129.         useColoredBorders = true,
  130.         useMoreRed = false,
  131.         moreRedOutputSide = "back",
  132.         spsRedstoneControlSide = "back",
  133.         fusionIgnitionSide = "top",
  134.         fusionPowerDownSide = "right",
  135.         industrialAlarmSide = "bottom",
  136.         spsRedstoneColor = "white",
  137.         fusionIgnitionColor = "orange",
  138.         fusionPowerDownColor = "red",
  139.         industrialAlarmColor = "pink",
  140.         enableFusionIgnition = true,
  141.         enableFusionPowerDown = true,
  142.         useIndustrialAlarm = false,
  143.     },
  144.     configMenu = {
  145.         page = 1,
  146.         scrollState = { offset = 0 },
  147.         dragging = nil,
  148.         tempConfig = nil,
  149.     },
  150.     keypad = {
  151.         isOpen = false,
  152.         targetDevice = nil,
  153.         targetWindow = nil,
  154.         inputValue = "",
  155.         message = "",
  156.         qx = 0, qy = 0, qW = 0, qH = 0
  157.     },
  158.     needsRedraw = true
  159. }
  160.  
  161.  
  162. -- =================================================================
  163. -- Helper Functions
  164. -- =================================================================
  165. local function centeredPrint(text, y)
  166.     local w, _ = term.getSize()
  167.     if not text or not w then return end
  168.     local x = math.floor((w - #text) / 2) + 1
  169.     if y then term.setCursorPos(x, y) else term.setCursorPos(x, term.getCursorPos()) end
  170.     term.write(text)
  171. end
  172.  
  173. local function centeredWrapPrint(text, y)
  174.     local w, _ = term.getSize()
  175.     local maxWidth = w - 4
  176.     local words = {}
  177.     for word in text:gmatch("[^%s]+") do
  178.         table.insert(words, word)
  179.     end
  180.  
  181.     local lines = {}
  182.     local currentLine = ""
  183.     for _, word in ipairs(words) do
  184.         if #currentLine == 0 then
  185.             currentLine = word
  186.         elseif #currentLine + 1 + #word <= maxWidth then
  187.             currentLine = currentLine .. " " .. word
  188.         else
  189.             table.insert(lines, currentLine)
  190.             currentLine = word
  191.         end
  192.     end
  193.     table.insert(lines, currentLine)
  194.  
  195.     for i, line in ipairs(lines) do
  196.         centeredPrint(line, y + i - 1)
  197.     end
  198. end
  199.  
  200. local function debugPrint(...)
  201.     if debugMode then
  202.         centeredPrint(table.concat({ ... }, " "))
  203.     end
  204. end
  205.  
  206. local function drawTerminalHeader()
  207.     local w, _ = term.getSize()
  208.     local title = "Mekanism Monitor"
  209.     local currentCursorX, currentCursorY = term.getCursorPos()
  210.     centeredPrint(title, 1)
  211.     term.setTextColor(colors.white)
  212.     term.setCursorPos(currentCursorX, currentCursorY)
  213. end
  214.  
  215. local tempUnits = {
  216.     K = { symbol = "K", convert = function(k) return k end, name = "Kelvin" },
  217.     C = { symbol = "C", convert = function(k) return k - 273.15 end, name = "Celsius" },
  218.     F = { symbol = "F", convert = function(k) return (k - 273.15) * 1.8 + 32 end, name = "Fahrenheit" },
  219.     R = { symbol = "R", convert = function(k) return k * 1.8 end, name = "Rankine" },
  220.     STP = { symbol = "xSTP", convert = function(k) return k / 298.15 end, name = "Mekanism STP" }
  221. }
  222.  
  223. local function convertTemperature(kelvin, unit)
  224.     if not kelvin or not unit or not tempUnits[unit] then
  225.         return "N/A", ""
  226.     end
  227.     local convertedValue = tempUnits[unit].convert(kelvin)
  228.     local symbol = tempUnits[unit].symbol
  229.     return convertedValue, symbol
  230. end
  231.  
  232. if not table.copy then
  233.     function table.copy(original)
  234.         if type(original) ~= 'table' then return original end
  235.         local copy = {}
  236.         for k, v in pairs(original) do
  237.             copy[k] = table.copy(v)
  238.         end
  239.         setmetatable(copy, getmetatable(original))
  240.         return copy
  241.     end
  242. end
  243.  
  244. local function countKeys(t)
  245.     local count = 0
  246.     for _ in pairs(t) do count = count + 1 end
  247.     return count
  248. end
  249.  
  250. local function getSafeColor(colorName, defaultColorValue)
  251.     local default = defaultColorValue or colors.black
  252.     if not colors or type(colors) ~= "table" then return default end
  253.     local colorValue = colors[colorName]
  254.     if type(colorValue) ~= "number" then local fallback = colors.white; if type(fallback) ~= "number" then fallback = default end; return fallback end
  255.     return colorValue
  256. end
  257.  
  258. local function getDynamicColor(value, thresholdSet)
  259.     if value == nil or type(value) ~= "number" or not thresholdSet then return "white" end
  260.     if value >= thresholdSet.red then return "red"
  261.     elseif value >= thresholdSet.orange then return "orange"
  262.     elseif value >= thresholdSet.yellow then return "yellow"
  263.     else return "green"
  264.     end
  265. end
  266.  
  267. local function formatFluidName(fullName)
  268.     if fullName == nil or fullName == "" or fullName:find("empty") then return "Empty" end
  269.     local name = fullName:match(":(.+)") or fullName
  270.     name = name:gsub("_", " ")
  271.     return name:gsub("^%l", string.upper)
  272. end
  273.  
  274. local function getBorderColors(deviceType)
  275.     if not sharedData.config.useColoredBorders then
  276.         return { border = "gray", text = "white" }
  277.     end
  278.     local colorMap = {
  279.         turbine = { border = "gray", text = "orange" },
  280.         fusionReactor = { border = "brown", text = "white" },
  281.         fissionReactor = { border = "green", text = "black" },
  282.         inductionMatrix = { border = "cyan", text = "black" },
  283.         boiler = { border = "orange", text = "black" },
  284.         thermalEvaporation = { border = "lightBlue", text = "black" },
  285.         dynamicTank = { border = "magenta", text = "black" },
  286.         sps = { border = "purple", text = "white" },
  287.         turbineSummary = { border = "yellow", text = "black" }
  288.     }
  289.     return colorMap[deviceType] or { border = "gray", text = "white" }
  290. end
  291.  
  292. local function formatNumber(n,p)if n==nil then return"N/A"end;p=p or 1;if type(n)~="number"then return tostring(n)end;local s={"","k","M","G","T","P","E"};local i=1;local aN=math.abs(n);while aN>=1000 and i<#s do aN=aN/1000;i=i+1 end;local d=1000^(i-1);local sN=(d~=0)and(n/d)or 0;return string.format("%."..p.."f%s",sN,s[i])end
  293. local function formatPercentage(num) if num==nil then return "N/A" end; if type(num)~="number"then return tostring(num) end; return string.format("%.1f%%",num*100) end
  294. function formatLargeEnergyFE(n) if n == nil or type(n) ~= "number" then return "N/A" end; if n == 0 then return "0" end; local s={"","K","M","G","T","P","E","Z","Y"}; local i=1; local sg=""; if n<0 then sg="-"; n=-n end; while n>=1000 and i<#s do n=n/1000;i=i+1 end; if i==1 then return sg..string.format("%d",n)..s[i] else if n==math.floor(n) then return sg..string.format("%d",n)..s[i] else return sg..string.format("%.2f",n)..s[i] end end end
  295. function formatDuration(ts) if ts==nil or type(ts)~="number" or ts<0 then return"N/A" end; if ts==math.huge or ts>(3600*24*365*10)then return"Infinite" end; if ts<0.1 then return"~0s" end; local sm=60;local sh=sm*60;local sd=sh*24; local d=math.floor(ts/sd);local rs=ts%sd; local h=math.floor(rs/sh);rs=rs%sh; local m=math.floor(rs/sm);local sc=math.floor(rs%sm); local str=""; if d>0 then str=str..d.."d " end; if h>0 or(d>0 and(m>0 or sc>0))then str=str..h.."h " end; if m>0 or((d>0 or h>0)and sc>0)then str=str..m.."m " end; if sc>0 or str==""then str=str..sc.."s" end; str=str:gsub("%s$",""); return str==""and"~0s"or str end
  296.  
  297. local function pulseRedstone(side)
  298.     if side and (side == "top" or side == "bottom" or side == "left" or side == "right" or side == "back" or side == "front") then
  299.         redstone.setOutput(side, true)
  300.         sleep(0.2)
  301.         redstone.setOutput(side, false)
  302.     end
  303. end
  304.  
  305. local function pulseBundledRedstone(side, colorName)
  306.     local colorValue = colors[colorName]
  307.     if not side or not colorValue then return end
  308.    
  309.     local originalOutput = redstone.getBundledOutput(side)
  310.     local pulsedOutput = bit.bor(originalOutput, colorValue)
  311.  
  312.     redstone.setBundledOutput(side, pulsedOutput)
  313.     sleep(0.2)
  314.     redstone.setBundledOutput(side, originalOutput)
  315. end
  316.  
  317. local function setBundledBit(side, colorName, state)
  318.     local colorValue = colors[colorName]
  319.     if not side or not colorValue then return end
  320.  
  321.     local currentOutput = redstone.getBundledOutput(side)
  322.     local newOutput
  323.     if state then
  324.         newOutput = bit.bor(currentOutput, colorValue)
  325.     else
  326.         newOutput = bit.band(currentOutput, bit.bnot(colorValue))
  327.     end
  328.     redstone.setBundledOutput(side, newOutput)
  329. end
  330.  
  331. -- =================================================================
  332. -- Layout and Data Management
  333. -- =================================================================
  334. local function calculateMonitorPages()
  335.     local allItems = sharedData.drawableItems
  336.     if #allItems == 0 then
  337.         sharedData.pagedItemsMon = {}
  338.         sharedData.totalPagesMon = 1
  339.         return
  340.     end
  341.  
  342.     local pages = {}
  343.     local placedStatus = {}
  344.  
  345.     while countKeys(placedStatus) < #allItems do
  346.         local currentPageItems = {}
  347.         local slotsOnPage = {}
  348.        
  349.         for i = 1, #allItems do
  350.             if not placedStatus[i] then
  351.                 local item = allItems[i]
  352.                 local isDouble = false
  353.                 if item.drawType == "device" then
  354.                     if item.info.type == "fissionReactor" then
  355.                         isDouble = true
  356.                     elseif item.info.type == "fusionReactor" then
  357.                         local stats = sharedData.deviceStats[item.info.name]
  358.                         if stats and stats.fuel and (stats.waterAmount or 0) > 0 and (stats.steamAmount or 0) > 0 and not stats.fuel.hasDTFuel then
  359.                             isDouble = true
  360.                         end
  361.                     end
  362.                 end
  363.                
  364.                 if isDouble then
  365.                     if countKeys(slotsOnPage) <= (devicesPerPageMonitor - 2) then
  366.                         if not slotsOnPage[1] and not slotsOnPage[3] then
  367.                             table.insert(currentPageItems, {item=item, layout={gridX=1, gridY=1, gridW=1, gridH=2}})
  368.                             slotsOnPage[1] = true; slotsOnPage[3] = true
  369.                             placedStatus[i] = true
  370.                         elseif not slotsOnPage[2] and not slotsOnPage[4] then
  371.                              table.insert(currentPageItems, {item=item, layout={gridX=2, gridY=1, gridW=1, gridH=2}})
  372.                             slotsOnPage[2] = true; slotsOnPage[4] = true
  373.                             placedStatus[i] = true
  374.                         end
  375.                     end
  376.                 end
  377.             end
  378.         end
  379.  
  380.         for i = 1, #allItems do
  381.             if not placedStatus[i] then
  382.                 if countKeys(slotsOnPage) < devicesPerPageMonitor then
  383.                     for slot_idx=1, devicesPerPageMonitor do
  384.                         if not slotsOnPage[slot_idx] then
  385.                             local gridX = (slot_idx % 2 == 1) and 1 or 2
  386.                             local gridY = (slot_idx <= 2) and 1 or 2
  387.                             table.insert(currentPageItems, {item=allItems[i], layout={gridX=gridX, gridY=gridY, gridW=1, gridH=1}})
  388.                             slotsOnPage[slot_idx] = true
  389.                             placedStatus[i] = true
  390.                             break
  391.                         end
  392.                     end
  393.                 else
  394.                     break
  395.                 end
  396.             end
  397.         end
  398.  
  399.         table.insert(pages, currentPageItems)
  400.        
  401.         if countKeys(placedStatus) == #allItems then break end
  402.     end
  403.    
  404.     sharedData.pagedItemsMon = pages
  405.     sharedData.totalPagesMon = math.max(1, #pages)
  406.     for _, state in pairs(sharedData.monitorStates) do
  407.         if state.currentPage > sharedData.totalPagesMon then
  408.             state.currentPage = sharedData.totalPagesMon
  409.         end
  410.     end
  411. end
  412.  
  413. local function reinitializeLayout()
  414.     local allDrawableItems = {}
  415.     local turbinesFound = 0
  416.     for _, dev in ipairs(sharedData.foundDevices) do
  417.         if dev.type == "turbine" then
  418.             turbinesFound = turbinesFound + 1
  419.         end
  420.     end
  421.  
  422.     local devicesByType = {}
  423.     for _, dev in ipairs(sharedData.foundDevices) do
  424.         if not devicesByType[dev.type] then devicesByType[dev.type] = {} end
  425.         table.insert(devicesByType[dev.type], dev)
  426.     end
  427.     for _, devices in pairs(devicesByType) do
  428.         table.sort(devices, function(a, b) return a.name < b.name end)
  429.     end
  430.  
  431.     local typeCounters = { turbine = 0, fusionReactor = 0, fissionReactor = 0, inductionMatrix = 0, boiler = 0, thermalEvaporation = 0, dynamicTank = 0, sps = 0 }
  432.  
  433.     for _, itemType in ipairs(sharedData.config.deviceOrder) do
  434.         if itemType == "turbineSummary" then
  435.             if turbinesFound > 1 then
  436.                 table.insert(allDrawableItems, { drawType = "summary" })
  437.             end
  438.         else
  439.             if devicesByType[itemType] then
  440.                 for _, devInfo in ipairs(devicesByType[itemType]) do
  441.                     typeCounters[devInfo.type] = typeCounters[devInfo.type] + 1
  442.                     devInfo.typeId = typeCounters[devInfo.type]
  443.                     table.insert(allDrawableItems, { drawType = "device", info = devInfo })
  444.                 end
  445.             end
  446.         end
  447.     end
  448.  
  449.     sharedData.drawableItems = allDrawableItems
  450.     sharedData.totalPagesTerm = math.max(1, math.ceil(#sharedData.drawableItems / devicesPerPageTerminal))
  451.     if sharedData.currentPageTerm > sharedData.totalPagesTerm then sharedData.currentPageTerm = sharedData.totalPagesTerm end
  452.  
  453.     local currentFissionReactors = {}
  454.     for _, item in ipairs(allDrawableItems) do
  455.         if item.drawType == "device" and item.info.type == "fissionReactor" then
  456.             currentFissionReactors[item.info.name] = true
  457.             if sharedData.fissionScrollOffsets[item.info.name] == nil then
  458.                 sharedData.fissionScrollOffsets[item.info.name] = 0
  459.             end
  460.         end
  461.     end
  462.     for deviceName, _ in pairs(sharedData.fissionScrollOffsets) do
  463.         if not currentFissionReactors[deviceName] then
  464.             sharedData.fissionScrollOffsets[deviceName] = nil
  465.         end
  466.     end
  467.  
  468.     debugPrint("Layout reinitialized. Items: " .. #sharedData.drawableItems)
  469.     sharedData.needsRedraw = true
  470. end
  471.  
  472. -- =================================================================
  473. -- Drawing Functions
  474. -- =================================================================
  475. local function drawQuadrantBorder(window, x, y, w, h, bBCK, t, tTCK)
  476.     if type(x) ~= "number" or type(y) ~= "number" or type(w) ~= "number" or type(h) ~= "number" or w < 1 or h < 1 then return end
  477.     local bC = getSafeColor(bBCK, colors.gray)
  478.     local tTC = getSafeColor(tTCK or "black", colors.black)
  479.     local oBg, oTxt = window.getBackgroundColor(), window.getTextColor()
  480.     pcall(function()
  481.         window.setBackgroundColor(bC)
  482.         if t and t ~= "" then
  483.             local tL = string.len(t); local sPL = math.floor((w - tL) / 2); sPL = math.max(0, sPL); local rS = w - tL - (sPL * 2); local lP = string.rep(" ", sPL); local rP = string.rep(" ", sPL + rS)
  484.             window.setCursorPos(x, y); window.write(lP); window.setTextColor(tTC); window.write(t); window.setTextColor(oTxt); window.write(rP)
  485.         else
  486.             window.setCursorPos(x, y); window.write(string.rep(" ", w))
  487.         end
  488.         window.setCursorPos(x, y + h - 1); window.write(string.rep(" ", w))
  489.         for i = 1, h - 2 do
  490.             window.setCursorPos(x, y + i); window.write(" ")
  491.             window.setCursorPos(x + w - 1, y + i); window.write(" ")
  492.         end
  493.     end)
  494.     window.setBackgroundColor(oBg); window.setTextColor(oTxt)
  495. end
  496.  
  497. local function drawDetailedProgressBar(window, cV, mV, dW, x, y, fCK, eCK, oT)
  498.     local oBg, oTxt = window.getBackgroundColor(), window.getTextColor()
  499.     local barX = x + 1
  500.     local barW = dW - 2
  501.  
  502.     if type(cV) ~= "number" or type(mV) ~= "number" or mV == 0 or barW < 1 then
  503.         window.setCursorPos(x,y); window.setBackgroundColor(getSafeColor("black")); window.write(string.rep(" ", dW)); window.setBackgroundColor(oBg); return
  504.     end
  505.    
  506.     window.setCursorPos(x, y); window.setBackgroundColor(getSafeColor("black")); window.write(" ")
  507.     window.setCursorPos(x + dW - 1, y); window.setBackgroundColor(getSafeColor("black")); window.write(" ")
  508.  
  509.     local p = cV / mV; p = math.max(0, math.min(1, p)); local fC = math.floor(p * barW)
  510.     for i = 1, barW do
  511.         window.setCursorPos(barX + i - 1, y)
  512.         if i <= fC then window.setBackgroundColor(getSafeColor(fCK)) else window.setBackgroundColor(getSafeColor(eCK, colors.gray)) end
  513.         window.write(" ")
  514.     end
  515.     if oT and oT ~= "" then
  516.         local tL = string.len(oT); local tSX = barX + math.max(0, math.floor((barW - tL) / 2))
  517.         for i = 1, tL do
  518.             local cX = tSX + i - 1
  519.             if cX >= barX and cX < barX + barW then
  520.                 window.setCursorPos(cX, y)
  521.                 local bSF = (cX - barX + 1) <= fC
  522.                 if bSF then window.setBackgroundColor(getSafeColor(fCK)) else window.setBackgroundColor(getSafeColor(eCK, colors.gray)) end
  523.                 window.setTextColor(getSafeColor("black")); window.write(string.sub(oT, i, i))
  524.             end
  525.         end
  526.     end
  527.     window.setBackgroundColor(oBg); window.setTextColor(oTxt)
  528. end
  529.  
  530. local function drawTurbineInQuadrant(window, targetName, devInfo,sts,lE,qx,qy,qW,qH)
  531.     pcall(function()
  532.         local title="Turbine "..(devInfo.typeId or "N/A"); local borderColors = getBorderColors("turbine")
  533.         drawQuadrantBorder(window, qx,qy,qW,qH,borderColors.border,title,borderColors.text)
  534.         window.setBackgroundColor(colors.black); local iX, cY, cW, bDTW = qx+2, qy+2, qW-4, qW-4
  535.         if lE then window.setTextColor(colors.red);window.setCursorPos(iX,cY);window.write(("Error: "..lE):sub(1,cW)); return end
  536.         if not sts then window.setTextColor(colors.gray);window.setCursorPos(iX,cY);window.write("Waiting..."); return end
  537.  
  538.         window.setTextColor(colors.lightBlue);window.setCursorPos(iX,cY);window.write("FE:");window.setTextColor(colors.white);window.setCursorPos(iX+4,cY);window.write(("%s/%s (%s)"):format(formatNumber(sts.energyStored),formatNumber(sts.maxEnergy),formatPercentage(sts.energyFilledPercentage)):sub(1,cW-4));cY=cY+1
  539.         drawDetailedProgressBar(window,sts.energyStored,sts.maxEnergy,bDTW,iX,cY,"green","gray","("..formatPercentage(sts.energyFilledPercentage)..")");cY=cY+2
  540.         window.setTextColor(colors.lightGray);window.setCursorPos(iX,cY);window.write("Steam:");window.setTextColor(colors.white);window.setCursorPos(iX+7,cY);window.write(("%s/%s (%s)"):format(formatNumber(sts.steamAmount,0),formatNumber(sts.steamCapacity,0),formatPercentage(sts.steamFilledPercentage)):sub(1,cW-7));cY=cY+1
  541.         drawDetailedProgressBar(window,sts.steamAmount,sts.steamCapacity,bDTW,iX,cY,"lightGray","gray","("..formatPercentage(sts.steamFilledPercentage)..")");cY=cY+2
  542.         window.setTextColor(colors.green);window.setCursorPos(iX,cY);window.write("Energy:");window.setTextColor(colors.white);window.setCursorPos(iX+8,cY);window.write(("%s/%s FE/t"):format(formatNumber(sts.productionRate),formatNumber(sts.maxProduction)):sub(1,cW-8));cY=cY+2
  543.         window.setTextColor(colors.yellow);window.setCursorPos(iX,cY);local sT=(sts.width and string.format("%dx%dx%d",sts.width,sts.length,sts.height))or"N/A";window.write(("Size: %s"):format(sT):sub(1,cW));cY=cY+1
  544.        
  545.         local buttonY=qy+qH-2
  546.         local bE={};local totBW=0;for _,mI in ipairs(dumpingModes)do local iA=sts.dumpingMode==mI.apiName;local iGSL=sharedData.globalModeButtonFeedback.modeAPIName==mI.apiName and sharedData.globalModeButtonFeedback.expiryTime>os.time();local bT=iA and("["..mI.displayName.."]")or(iGSL and("["..mI.displayName.."]")or(" "..mI.displayName.." "));table.insert(bE,{text=bT,mode=mI,active=iA,globallySetLime=iGSL});totBW=totBW+#bT end;totBW=totBW+#dumpingModes-1;local bSX=iX+math.max(0,math.floor((cW-totBW)/2));local cBX=bSX;for _,bEl in ipairs(bE)do local cK=bEl.active and"yellow"or(bEl.globallySetLime and"lime"or"white");window.setTextColor(getSafeColor(cK));if cBX+#bEl.text-1<qx+qW-1 then window.setCursorPos(cBX,buttonY);window.write(bEl.text);table.insert(sharedData.pageButtonRegions,{type="dump_mode",target=targetName,turbineName=devInfo.name,modeToSet=bEl.mode.apiName,xStart=cBX,xEnd=cBX+#bEl.text-1,pageY=buttonY});cBX=cBX+#bEl.text+1 else break end end
  547.     end)
  548. end
  549.  
  550. local function drawFusionReactorQuadrant(window, targetName, devInfo,sts,lE,qx,qy,qW,qH)
  551.     pcall(function()
  552.         local title="Fusion Reactor "..(devInfo.typeId or "N/A"); local borderColors = getBorderColors("fusionReactor")
  553.         drawQuadrantBorder(window, qx,qy,qW,qH,borderColors.border,title,borderColors.text)
  554.         window.setBackgroundColor(colors.black); local iX, cY, cW, bDTW = qx+2, qy+2, qW-4, qW-4
  555.         if lE then window.setTextColor(colors.red);window.setCursorPos(iX,cY);window.write(("Error: "..lE):sub(1,cW)); return end
  556.         if not sts or not sts.fuel then window.setTextColor(colors.gray);window.setCursorPos(iX,cY);window.write("Waiting..."); return end
  557.  
  558.         local useSpacedLayout = (qH > 15)
  559.         local verticalPadding = useSpacedLayout and 2 or 1
  560.        
  561.         local mPX=iX+math.floor(cW/2); local hCW=math.floor(cW/2)-1
  562.        
  563.         if sts.waterAmount and sts.waterAmount>0 then
  564.             window.setTextColor(colors.blue);window.setCursorPos(iX,cY);window.write("Water:");window.setTextColor(colors.white);window.setCursorPos(iX+7,cY);window.write(("%s/%s (%s)"):format(formatNumber(sts.waterAmount,0),formatNumber(sts.waterCapacity,0),formatPercentage(sts.waterFilledPercentage)):sub(1,cW-7));cY=cY+1
  565.             drawDetailedProgressBar(window,sts.waterAmount,sts.waterCapacity,bDTW,iX,cY,"blue","gray","("..formatPercentage(sts.waterFilledPercentage)..")");cY=cY+verticalPadding
  566.         end
  567.         if sts.steamAmount and sts.steamAmount>0 then
  568.             window.setTextColor(colors.lightGray);window.setCursorPos(iX,cY);window.write("Steam:");window.setTextColor(colors.white);window.setCursorPos(iX+7,cY);window.write(("%s/%s (%s)"):format(formatNumber(sts.steamAmount,0),formatNumber(sts.steamCapacity,0),formatPercentage(sts.steamFilledPercentage)):sub(1,cW-7));cY=cY+1
  569.             drawDetailedProgressBar(window,sts.steamAmount,sts.steamCapacity,bDTW,iX,cY,"lightGray","gray","("..formatPercentage(sts.steamFilledPercentage)..")");cY=cY+verticalPadding
  570.         end
  571.        
  572.         if sts.fuel.hasDTFuel then
  573.             window.setTextColor(colors.purple);window.setCursorPos(iX,cY);window.write("DTFuel:");window.setTextColor(colors.white);window.setCursorPos(iX+8,cY);window.write(("%s/%s (%s)"):format(formatNumber(sts.fuel.dtFuelAmount,0),formatNumber(sts.fuel.dtFuelCapacity,0),formatPercentage(sts.fuel.dtFuelFilledPercentage)):sub(1,cW-8));cY=cY+1
  574.             drawDetailedProgressBar(window,sts.fuel.dtFuelAmount,sts.fuel.dtFuelCapacity,bDTW,iX,cY,"purple","gray","("..formatPercentage(sts.fuel.dtFuelFilledPercentage)..")");cY=cY+verticalPadding
  575.         else
  576.             window.setTextColor(colors.red);window.setCursorPos(iX,cY);window.write("Deuterium:");window.setTextColor(colors.white);window.setCursorPos(iX+11,cY);window.write(("%s/%s"):format(formatNumber(sts.fuel.deuteriumAmount,0),formatNumber(sts.fuel.deuteriumCapacity,0)):sub(1,cW-11));cY=cY+1
  577.             drawDetailedProgressBar(window,sts.fuel.deuteriumAmount,sts.fuel.deuteriumCapacity,bDTW,iX,cY,"red","gray",formatPercentage(sts.fuel.deuteriumFilledPercentage));cY=cY+verticalPadding
  578.             window.setTextColor(colors.green);window.setCursorPos(iX,cY);window.write("Tritium:");window.setTextColor(colors.white);window.setCursorPos(iX+9,cY);window.write(("%s/%s"):format(formatNumber(sts.fuel.tritiumAmount,0),formatNumber(sts.fuel.tritiumCapacity,0)):sub(1,cW-9));cY=cY+1
  579.             drawDetailedProgressBar(window,sts.fuel.tritiumAmount,sts.fuel.tritiumCapacity,bDTW,iX,cY,"green","gray",formatPercentage(sts.fuel.tritiumFilledPercentage));cY=cY+verticalPadding
  580.         end
  581.        
  582.         local caseTempThresh = sts.fuel.hasDTFuel and dynamicColorThresholds.caseTemp.dtFuel or dynamicColorThresholds.caseTemp.noDt
  583.         local plasmaTempThresh = sts.fuel.hasDTFuel and dynamicColorThresholds.plasmaTemp.dtFuel or dynamicColorThresholds.plasmaTemp.noDt
  584.         local energyThresh = sts.fuel.hasDTFuel and dynamicColorThresholds.energy.dtFuel or dynamicColorThresholds.energy.noDt
  585.         local caseTempColor = getDynamicColor(sts.caseTemp, caseTempThresh)
  586.         local plasmaTempColor = getDynamicColor(sts.plasmaTemp, plasmaTempThresh)
  587.         local energyColor = getDynamicColor(sts.productionRate, energyThresh)
  588.        
  589.         if cY < qy + qH - 2 then
  590.             local caseT, caseU = convertTemperature(sts.caseTemp, sharedData.config.tempUnit)
  591.             local plasmaT, plasmaU = convertTemperature(sts.plasmaTemp, sharedData.config.tempUnit)
  592.             local caseStr = ("Case: %s%s"):format(formatNumber(caseT,1), caseU)
  593.             window.setTextColor(getSafeColor(caseTempColor)); window.setCursorPos(iX,cY); window.write(caseStr:sub(1,hCW-1))
  594.             local plasmaStr = ("Plasma: %s%s"):format(formatNumber(plasmaT,1), plasmaU)
  595.             window.setTextColor(getSafeColor(plasmaTempColor)); window.setCursorPos(mPX,cY); window.write(plasmaStr:sub(1,cW-mPX+iX-1))
  596.             cY = cY+1
  597.         end
  598.         if cY < qy + qH - 1 then
  599.             local energyStr = ("Energy: %s FE/t"):format(formatNumber(sts.productionRate))
  600.             window.setTextColor(getSafeColor(energyColor)); window.setCursorPos(iX,cY); window.write(energyStr:sub(1,hCW-1))
  601.             local iC=sts.isIgnited and colors.lime or colors.red; local iT=sts.isIgnited and "Ignited" or "Off"; local sT="Status: "..iT
  602.             window.setTextColor(iC); window.setCursorPos(mPX+math.max(0,math.floor((hCW-#sT)/2)),cY); window.write(sT:sub(1,hCW-1))
  603.         end
  604.        
  605.         local buttonY=qy+qH-2
  606.        
  607.         local tempB, tempT = window.getBackgroundColor(), window.getTextColor()
  608.         window.setBackgroundColor(colors.black)
  609.         window.setCursorPos(qx + 1, buttonY); window.write(string.rep(" ", qW - 2))
  610.         window.setBackgroundColor(tempB); window.setTextColor(tempT)
  611.  
  612.         local btnE={};
  613.         if sharedData.config.enableFusionPowerDown then table.insert(btnE, {t="[Power Down]", a="fuel_off", c="red"}) end
  614.         table.insert(btnE,{t="[MIN]",a="set_min",c="blue"});
  615.         table.insert(btnE,{t="[-]",a="dec_rate",c="red"});
  616.         table.insert(btnE,{t=" "..string.format("%2d",sts.injectionRate or 0).." ",a="display_only",c="white"});
  617.         table.insert(btnE,{t="[+]",a="inc_rate",c="green"});
  618.         table.insert(btnE,{t="[MAX]",a="set_max",c="blue"});
  619.         if sharedData.config.enableFusionIgnition then table.insert(btnE, {t="[Ignite]", a="ignite", c="orange"}) end
  620.        
  621.         local totBW=0; for _,el in ipairs(btnE)do totBW=totBW+#el.t end;
  622.         totBW = totBW + #btnE - 1;
  623.         local bSX=iX+math.max(0,math.floor((cW-totBW)/2));
  624.         local cBX=bSX;
  625.         for _,bEl in ipairs(btnE)do
  626.             window.setTextColor(getSafeColor(bEl.c));
  627.             if cBX+#bEl.t-1<qx+qW-1 then
  628.                 window.setCursorPos(cBX,buttonY);
  629.                 window.write(bEl.t);
  630.                 if bEl.a~="display_only"then
  631.                     if bEl.a == "ignite" or bEl.a == "fuel_off" then
  632.                         table.insert(sharedData.pageButtonRegions,{type= (bEl.a == "ignite" and "fusion_ignite" or "fusion_fuel_toggle"),target=targetName, deviceName=devInfo.name, xStart=cBX,xEnd=cBX+#bEl.t-1,pageY=buttonY})
  633.                     else
  634.                         table.insert(sharedData.pageButtonRegions,{type="set_injection_rate",target=targetName,deviceName=devInfo.name,action=bEl.a,currentRate=sts.injectionRate,xStart=cBX,xEnd=cBX+#bEl.t-1,pageY=buttonY})
  635.                     end
  636.                 end;
  637.                 cBX=cBX+#bEl.t+1
  638.             else break end
  639.         end
  640.     end)
  641. end
  642.  
  643. local function drawInductionMatrixQuadrant(window, targetName, devInfo,sts,lE,qx,qy,qW,qH)
  644.     pcall(function()
  645.         local title="Induction Matrix "..(devInfo.typeId or "N/A"); local borderColors = getBorderColors("inductionMatrix")
  646.         drawQuadrantBorder(window, qx,qy,qW,qH,borderColors.border,title,borderColors.text)
  647.         window.setBackgroundColor(colors.black); local iX, cY, cW, bDTW = qx+2, qy+2, qW-4, qW-4
  648.         if lE then window.setTextColor(colors.red);window.setCursorPos(iX,cY);window.write(("Error: "..lE):sub(1,cW)); return end
  649.         if not sts then window.setTextColor(colors.gray);window.setCursorPos(iX,cY);window.write("Waiting..."); return end
  650.  
  651.         local halfWidth = math.floor(cW / 2)
  652.         window.setTextColor(colors.white);window.setCursorPos(iX,cY);window.write("Formed: ");window.setTextColor(sts.isFormed and getSafeColor("lime")or getSafeColor("red"));window.write(sts.isFormed and"Yes"or"No");cY=cY+1
  653.        
  654.         local energyStr = string.format("%s/%s", formatLargeEnergyFE(sts.energyFE), formatLargeEnergyFE(sts.maxEnergyFE))
  655.         window.setTextColor(colors.yellow); window.setCursorPos(iX, cY); window.write(("Energy: " .. energyStr):sub(1, cW)); cY=cY+1
  656.         drawDetailedProgressBar(window, sts.energyFE,sts.maxEnergyFE,bDTW,iX,cY,"green","gray", formatPercentage(sts.energyFilledPercentage)); cY=cY+2
  657.        
  658.         local rt=sts.energyChangeRatePerTick or 0;local rtP, rtCK;
  659.         if rt>matrixRateThreshold then rtP="Input: ";rtCK="green" elseif rt<-matrixRateThreshold then rtP="Output: ";rtCK="red" else rtP="Net:      ";rt=0;rtCK="gray" end
  660.         local rateDisplay = (rtP .. formatLargeEnergyFE(rt) .. "FE/t")
  661.        
  662.         local tES="N/A";local tEL="Status: ";local timeColorKey="gray";
  663.         if not sts.isFormed then tES="Not Formed";timeColorKey="lightGray"
  664.         elseif sts.energyFE >= sts.maxEnergyFE then tES="Full";timeColorKey="lime"
  665.         elseif rt > matrixRateThreshold then tEL="To Fill: ";timeColorKey="yellow";tES=formatDuration((sts.maxEnergyFE - sts.energyFE)/rt/TICKS_PER_SECOND)
  666.         elseif rt < -matrixRateThreshold then tEL="To Empty: ";timeColorKey="orange";tES=formatDuration(sts.energyFE/math.abs(rt)/TICKS_PER_SECOND)
  667.         elseif sts.energyFE <= 0 then tES="Empty";timeColorKey="red"
  668.         else tES="Stable";timeColorKey="lightGray" end
  669.  
  670.         window.setTextColor(getSafeColor(rtCK,colors.gray));window.setCursorPos(iX,cY);window.write(rateDisplay:sub(1, halfWidth -1 ))
  671.         window.setCursorPos(iX + halfWidth,cY)
  672.         window.setTextColor(colors.white); window.write(tEL:sub(1, #tEL))
  673.         window.setTextColor(getSafeColor(timeColorKey,colors.gray)); window.write(tES:sub(1, cW - halfWidth - #tEL)); cY=cY+2
  674.        
  675.         local cellsStr = ("Cells: " .. tostring(sts.installedCells or "N/A")); local providersStr = ("Prov: " .. tostring(sts.installedProviders or "N/A"))
  676.         local dS= sts.isFormed and (sts.width.."x"..sts.height.."x"..sts.length) or "N/A"
  677.         local sizeStr = ("Size: " .. dS)
  678.         local combinedStatsStr = cellsStr .. "   " .. providersStr .. "   " .. sizeStr
  679.         local statsStartX = iX + math.max(0, math.floor((cW - #combinedStatsStr) / 2))
  680.         window.setTextColor(colors.white);
  681.         window.setCursorPos(statsStartX, cY); window.write(cellsStr)
  682.         window.setCursorPos(statsStartX + #cellsStr + 2, cY); window.write(providersStr)
  683.         window.setCursorPos(statsStartX + #cellsStr + 2 + #providersStr + 2, cY); window.write(sizeStr)
  684.     end)
  685. end
  686.  
  687. local function drawBoilerQuadrant(window, targetName, devInfo,sts,lE,qx,qy,qW,qH)
  688.     pcall(function()
  689.         local title="Boiler "..(devInfo.typeId or "N/A"); local borderColors = getBorderColors("boiler")
  690.         drawQuadrantBorder(window, qx,qy,qW,qH,borderColors.border,title,borderColors.text)
  691.         window.setBackgroundColor(colors.black); local iX, cY, cW, bDTW = qx+2, qy+2, qW-4, qW-4
  692.         if lE then window.setTextColor(colors.red);window.setCursorPos(iX,cY);window.write(("Error: "..lE):sub(1,cW)); return end
  693.         if not sts then window.setTextColor(colors.gray);window.setCursorPos(iX,cY);window.write("Waiting..."); return end
  694.  
  695.         window.setTextColor(colors.white); window.setCursorPos(iX, cY); window.write("Formed: "); window.setTextColor(sts.isFormed and colors.lime or colors.red); window.write(sts.isFormed and "Yes" or "No"); cY=cY+2
  696.        
  697.         window.setTextColor(colors.blue); window.setCursorPos(iX,cY); window.write("Water:"); window.setTextColor(colors.white); window.setCursorPos(iX+7,cY); window.write(("%s/%s mB"):format(formatNumber(sts.waterAmount,0),formatNumber(sts.waterCapacity,0)):sub(1,cW-7)); cY=cY+1
  698.         drawDetailedProgressBar(window,sts.waterAmount,sts.waterCapacity,bDTW,iX,cY,"blue","gray",formatPercentage(sts.waterFilledPercentage)); cY=cY+2
  699.        
  700.         window.setTextColor(colors.lightGray); window.setCursorPos(iX,cY); window.write("Steam:"); window.setTextColor(colors.white); window.setCursorPos(iX+7,cY); window.write(("%s/%s mB"):format(formatNumber(sts.steamAmount,0),formatNumber(sts.steamCapacity,0)):sub(1,cW-7)); cY=cY+1
  701.         drawDetailedProgressBar(window,sts.steamAmount,sts.steamCapacity,bDTW,iX,cY,"lightGray","gray",formatPercentage(sts.steamFilledPercentage)); cY=cY+2
  702.        
  703.         local tempColor = getDynamicColor(sts.temperature, dynamicColorThresholds.boilerTemp)
  704.         local halfWidth = math.floor(cW / 2)
  705.         local tempVal, tempUnit = convertTemperature(sts.temperature, sharedData.config.tempUnit)
  706.         window.setTextColor(getSafeColor(tempColor)); window.setCursorPos(iX,cY); window.write("Temp:"); window.setTextColor(colors.white); window.write(("%s%s"):format(formatNumber(tempVal,1), tempUnit):sub(1,halfWidth-6));
  707.         window.setTextColor(colors.green); window.setCursorPos(iX+halfWidth,cY); window.write("Boil:"); window.setTextColor(colors.white); window.write(("%s/%s"):format(formatNumber(sts.boilRate,1),formatNumber(sts.maxBoilRate,1)):sub(1,cW-halfWidth-6)); cY=cY+2
  708.        
  709.         window.setTextColor(colors.red); window.setCursorPos(iX,cY); window.write("Env Loss:"); window.setTextColor(colors.white); window.write(("%sK/t"):format(formatNumber(sts.envLoss,2)):sub(1,cW-10)); cY=cY+1
  710.     end)
  711. end
  712.  
  713. local function drawThermalEvaporationQuadrant(window, targetName, devInfo,sts,lE,qx,qy,qW,qH)
  714.     pcall(function()
  715.         local title="Thermal Evaporation "..(devInfo.typeId or "N/A");
  716.         local borderColors = getBorderColors("thermalEvaporation")
  717.         drawQuadrantBorder(window, qx,qy,qW,qH,borderColors.border,title,borderColors.text)
  718.         window.setBackgroundColor(colors.black); local iX, cY, cW, bDTW = qx+2, qy+2, qW-4, qW-4
  719.         if lE then window.setTextColor(colors.red);window.setCursorPos(iX,cY);window.write(("Error: "..lE):sub(1,cW)); return end
  720.         if not sts then window.setTextColor(colors.gray);window.setCursorPos(iX,cY);window.write("Waiting..."); return end
  721.  
  722.         local tempColor = getDynamicColor(sts.temperature, dynamicColorThresholds.thermalTemp)
  723.         local tempVal, tempUnit = convertTemperature(sts.temperature, sharedData.config.tempUnit)
  724.         local tempStr = ("Temp: "..formatNumber(tempVal, 1)..tempUnit)
  725.         window.setTextColor(getSafeColor(tempColor)); window.setCursorPos(iX, cY); window.write(tempStr); cY=cY+2
  726.  
  727.         local inputName = formatFluidName(sts.inputName)
  728.         window.setTextColor(colors.cyan); window.setCursorPos(iX,cY); window.write(inputName..":"); window.setTextColor(colors.white); window.setCursorPos(iX+#inputName+2,cY); window.write(("%s/%s"):format(formatNumber(sts.inputAmount,0),formatNumber(sts.inputCapacity,0)):sub(1,cW-#inputName-2)); cY=cY+1
  729.         drawDetailedProgressBar(window,sts.inputAmount,sts.inputCapacity,bDTW,iX,cY,"cyan","gray",formatPercentage(sts.inputFilledPercentage)); cY=cY+2
  730.        
  731.         local outputName = formatFluidName(sts.outputName)
  732.         window.setTextColor(colors.yellow); window.setCursorPos(iX,cY); window.write(outputName..":"); window.setTextColor(colors.white); window.setCursorPos(iX+#outputName+2,cY); window.write(("%s/%s"):format(formatNumber(sts.outputAmount,0),formatNumber(sts.outputCapacity,0)):sub(1,cW-#outputName-2)); cY=cY+1
  733.         drawDetailedProgressBar(window,sts.outputAmount,sts.outputCapacity,bDTW,iX,cY,"yellow","gray",formatPercentage(sts.outputFilledPercentage)); cY=cY+2
  734.        
  735.         local prodStr = ("Production: " .. string.format("%s mB/t", formatNumber(sts.production, 2)))
  736.         local heightStr = ("Height: " .. sts.height)
  737.         window.setTextColor(colors.white); window.setCursorPos(iX, cY); window.write(prodStr)
  738.         window.setCursorPos(iX + cW - #heightStr, cY); window.write(heightStr)
  739.     end)
  740. end
  741.  
  742. local function drawDynamicTankQuadrant(window, targetName, devInfo,sts,lE,qx,qy,qW,qH)
  743.     pcall(function()
  744.         local title="Dynamic Tank "..(devInfo.typeId or "N/A"); local borderColors = getBorderColors("dynamicTank")
  745.         drawQuadrantBorder(window, qx,qy,qW,qH,borderColors.border,title,borderColors.text)
  746.         window.setBackgroundColor(colors.black); local iX, cY, cW, bDTW = qx+2, qy+2, qW-4, qW-4
  747.         if lE then window.setTextColor(colors.red);window.setCursorPos(iX,cY);window.write(("Error: "..lE):sub(1,cW)); return end
  748.         if not sts or not sts.stored then window.setTextColor(colors.gray);window.setCursorPos(iX,cY);window.write("Waiting..."); return end
  749.  
  750.         local storedName = formatFluidName(sts.stored.name)
  751.         window.setTextColor(colors.white); window.setCursorPos(iX, cY); window.write(storedName..":"); cY=cY+1
  752.        
  753.         local amountStr = ("%s/%s (%s)"):format(formatNumber(sts.stored.amount, 1), formatNumber(sts.capacity, 1), formatPercentage(sts.filledPercentage))
  754.         window.setCursorPos(iX, cY); window.write(amountStr:sub(1, cW)); cY=cY+1
  755.         drawDetailedProgressBar(window, sts.stored.amount, sts.capacity, bDTW, iX, cY, "yellow", "gray", formatPercentage(sts.filledPercentage)); cY=cY+2
  756.  
  757.         window.setTextColor(colors.yellow);
  758.         local sT = (sts.width and string.format("Size: %dx%dx%d",sts.width,sts.length,sts.height)) or "Size: N/A"
  759.         window.setCursorPos(iX, cY); window.write(sT:sub(1, cW)); cY=cY+2
  760.  
  761.         local bY = qy+qH-2
  762.         if bY >= cY then
  763.             local prevText = "< "
  764.             local modeText = " " .. (sts.editMode or "N/A") .. " "
  765.             local nextText = " >"
  766.             local fullBar = prevText .. modeText .. nextText
  767.             local startX = iX + math.floor((cW - #fullBar)/2)
  768.  
  769.             window.setTextColor(colors.white); window.setCursorPos(startX, bY); window.write(prevText)
  770.             table.insert(sharedData.pageButtonRegions, {type="container_edit_mode", target=targetName, deviceName=devInfo.name, action="dec", xStart=startX, xEnd=startX+#prevText-1, pageY=bY})
  771.            
  772.             startX = startX + #prevText
  773.             window.setTextColor(colors.yellow); window.setCursorPos(startX, bY); window.write(modeText)
  774.  
  775.             startX = startX + #modeText
  776.             window.setTextColor(colors.white); window.setCursorPos(startX, bY); window.write(nextText)
  777.             table.insert(sharedData.pageButtonRegions, {type="container_edit_mode", target=targetName, deviceName=devInfo.name, action="inc", xStart=startX, xEnd=startX+#nextText-1, pageY=bY})
  778.         end
  779.     end)
  780. end
  781.  
  782. local function drawSPSQuadrant(window, targetName, devInfo,sts,lE,qx,qy,qW,qH)
  783.     pcall(function()
  784.         local title="SPS "..(devInfo.typeId or "N/A"); local borderColors = getBorderColors("sps")
  785.         drawQuadrantBorder(window, qx,qy,qW,qH,borderColors.border,title,borderColors.text)
  786.         window.setBackgroundColor(colors.black); local iX, cY, cW, bDTW = qx+2, qy+3, qW-4, qW-4
  787.         if lE then window.setTextColor(colors.red);window.setCursorPos(iX,cY);window.write(("Error: "..lE):sub(1,cW)); return end
  788.         if not sts then window.setTextColor(colors.gray);window.setCursorPos(iX,cY);window.write("Waiting..."); return end
  789.  
  790.         window.setTextColor(colors.cyan); window.setCursorPos(iX,cY); window.write("Polonium:"); window.setTextColor(colors.white); window.setCursorPos(iX+10,cY); window.write(("%s/%s"):format(formatNumber(sts.input.amount,0),formatNumber(sts.inputCapacity,0)):sub(1,cW-10)); cY=cY+1
  791.         drawDetailedProgressBar(window,sts.input.amount,sts.inputCapacity,bDTW,iX,cY,"cyan","gray",formatPercentage(sts.inputFilledPercentage)); cY=cY+2
  792.  
  793.         window.setTextColor(colors.purple); window.setCursorPos(iX,cY); window.write("Antimatter:"); window.setTextColor(colors.white); window.setCursorPos(iX+12,cY); window.write(("%s/%s"):format(formatNumber(sts.output.amount,0),formatNumber(sts.outputCapacity,0)):sub(1,cW-12)); cY=cY+1
  794.         drawDetailedProgressBar(window,sts.output.amount,sts.outputCapacity,bDTW,iX,cY,"purple","gray",formatPercentage(sts.outputFilledPercentage)); cY=cY+2
  795.  
  796.         local isActive = sts.processRate > 0
  797.         local statusText = isActive and "Status: Active" or "Status: Idle"
  798.         local statusColor = isActive and colors.lime or colors.red
  799.         local statusX = iX + math.floor((cW - #statusText) / 2)
  800.         window.setTextColor(statusColor)
  801.         window.setCursorPos(statusX, cY); window.write(statusText); cY=cY+1
  802.        
  803.         local sizeStr = (sts.width and string.format("Size: %dx%dx%d",sts.width,sts.height,sts.length)) or "Size: N/A"
  804.         local coilsStr = ("Coils: " .. (sts.coils or "N/A"))
  805.         local rateStr = ("Rate: " .. string.format("%s mB/t", formatNumber(sts.processRate, 2)))
  806.         window.setTextColor(colors.yellow); window.setCursorPos(iX, cY); window.write(sizeStr)
  807.         window.setCursorPos(iX + cW - #coilsStr, cY); window.write(coilsStr); cY=cY+1
  808.         local rateX = iX + math.floor((cW - #rateStr) / 2)
  809.         window.setTextColor(colors.white); window.setCursorPos(rateX, cY); window.write(rateStr); cY=cY+1
  810.  
  811.         local bY = qy+qH-2
  812.         if bY >= cY then
  813.             local isRedstoneOn
  814.             if sharedData.config.useMoreRed then
  815.                 local side = sharedData.config.moreRedOutputSide
  816.                 local colorValue = colors[sharedData.config.spsRedstoneColor]
  817.                 if colorValue then
  818.                     isRedstoneOn = bit.band(redstone.getBundledOutput(side), colorValue) > 0
  819.                 end
  820.             else
  821.                 isRedstoneOn = redstone.getOutput(sharedData.config.spsRedstoneControlSide)
  822.             end
  823.  
  824.             local buttonText = isRedstoneOn and " [ OFF ] " or " [ ON ] "
  825.             local buttonTextColor = isRedstoneOn and colors.red or colors.lime
  826.             local startX = iX + math.floor((cW - #buttonText) / 2)
  827.            
  828.             window.setBackgroundColor(colors.black)
  829.             window.setTextColor(buttonTextColor)
  830.             window.setCursorPos(startX, bY); window.write(buttonText)
  831.            
  832.             table.insert(sharedData.pageButtonRegions, {
  833.                 type="sps_redstone_toggle",
  834.                 target=targetName,
  835.                 xStart=startX,
  836.                 xEnd=startX+#buttonText-1,
  837.                 pageY=bY
  838.             })
  839.         end
  840.     end)
  841. end
  842.  
  843. local function drawFissionReactorQuadrant(window, targetName, devInfo,sts,lE,qx,qy,qW,qH)
  844.     pcall(function()
  845.         local isScrammed = sharedData.scrammingReactors[devInfo.name]
  846.         local borderColors = getBorderColors("fissionReactor")
  847.        
  848.         local currentBorderColor = borderColors.border
  849.         if isScrammed and sharedData.flashToggle then
  850.             currentBorderColor = "red"
  851.         end
  852.  
  853.         local title="Fission Reactor "..(devInfo.typeId or "N/A");
  854.         drawQuadrantBorder(window, qx,qy,qW,qH,currentBorderColor,title,borderColors.text)
  855.  
  856.         window.setBackgroundColor(colors.black)
  857.         local iX, cW = qx+2, qW-4
  858.         local contentBoxX, contentBoxY, contentBoxW, contentBoxH = qx+1, qy+1, qW-2, qH-6
  859.  
  860.         if lE then window.setTextColor(colors.red);window.setCursorPos(iX,qy+2);window.write(("Error: "..lE):sub(1,cW)); return end
  861.         if not sts then window.setTextColor(colors.gray);window.setCursorPos(iX,cY);window.write("Waiting..."); return end
  862.  
  863.         local offset = sharedData.fissionScrollOffsets[devInfo.name] or 0
  864.         local contentHeight = 21 -- Increased for new lines and heated coolant
  865.         local viewHeight = contentBoxH
  866.         local maxOffset = math.max(0, contentHeight - viewHeight)
  867.        
  868.         local clampedOffset = math.max(0, math.min(offset, maxOffset))
  869.  
  870.         local vY = 1
  871.  
  872.         local function drawLine(text, textColor)
  873.             local actualY = contentBoxY + vY - clampedOffset
  874.             if actualY >= contentBoxY and actualY < contentBoxY + viewHeight then
  875.                 window.setTextColor(getSafeColor(textColor, colors.white))
  876.                 window.setCursorPos(iX, actualY)
  877.                 window.write(text:sub(1, cW-1))
  878.             end
  879.             vY = vY + 1
  880.         end
  881.  
  882.         local function drawProgressLine(value, max, fillColor, emptyColor, text)
  883.               local actualY = contentBoxY + vY - clampedOffset
  884.               if actualY >= contentBoxY and actualY < contentBoxY + viewHeight then
  885.                    drawDetailedProgressBar(window, value, max, cW-1, iX, actualY, fillColor, emptyColor, text)
  886.               end
  887.               vY = vY + 1
  888.         end
  889.  
  890.         local tempColor = getDynamicColor(sts.temperature, dynamicColorThresholds.fissionTemp)
  891.         local tempVal, tempUnit = convertTemperature(sts.temperature, sharedData.config.tempUnit)
  892.         local tempStr = ("Temp: "..formatNumber(tempVal, 1)..tempUnit)
  893.         local damageStr = ("Dmg: %.1f%%"):format(sts.damagePercent or 0)
  894.        
  895.         local combinedStr = tempStr .. string.rep(" ", cW - #tempStr - #damageStr) .. damageStr
  896.         drawLine(combinedStr, tempColor)
  897.        
  898.         local statusColor = "gray"
  899.         if sts.status == "ACTIVE" then
  900.             statusColor = "lime"
  901.         elseif sts.status == "IDLE" then
  902.             statusColor = "yellow"
  903.         elseif sts.status == "SCRAM" then
  904.             statusColor = "red"
  905.         elseif sts.status == "MELTDOWN" then
  906.             statusColor = sharedData.flashToggle and "red" or "white"
  907.         end
  908.         local statusText = "Status: " .. (sts.status or "N/A")
  909.         drawLine(statusText, statusColor)
  910.        
  911.         drawLine("Heating Rate: " .. string.format("%.2f mB/t", sts.heatingRate or 0), "white")
  912.         vY = vY+1
  913.  
  914.         drawLine("Fuel: " .. formatNumber((sts.fuel and sts.fuel.amount) or 0, 1) .. "/" .. formatNumber(sts.fuelCapacity or 1, 1), "yellow")
  915.         drawProgressLine((sts.fuel and sts.fuel.amount) or 0, sts.fuelCapacity or 1, "yellow", "gray", formatPercentage(sts.fuelFilledPercentage or 0))
  916.         vY=vY+1
  917.  
  918.         drawLine("Coolant: " .. formatNumber((sts.coolant and sts.coolant.amount) or 0, 1) .. "/" .. formatNumber(sts.coolantCapacity or 1, 1), "blue")
  919.         drawProgressLine((sts.coolant and sts.coolant.amount) or 0, sts.coolantCapacity or 1, "blue", "gray", formatPercentage(sts.coolantFilledPercentage or 0))
  920.         vY=vY+1
  921.  
  922.         local heatedCoolantColor = "lightGray" -- Default for steam
  923.         if sts.heatedCoolant and sts.heatedCoolant.name and string.find(sts.heatedCoolant.name, "sodium") then
  924.             heatedCoolantColor = "orange"
  925.         end
  926.         drawLine("Heated: " .. formatNumber((sts.heatedCoolant and sts.heatedCoolant.amount) or 0, 1) .. "/" .. formatNumber(sts.heatedCoolantCapacity or 1, 1), heatedCoolantColor)
  927.         drawProgressLine((sts.heatedCoolant and sts.heatedCoolant.amount) or 0, sts.heatedCoolantCapacity or 1, heatedCoolantColor, "gray", formatPercentage(sts.heatedCoolantFilledPercentage or 0))
  928.         vY=vY+1
  929.        
  930.         drawLine("Waste: " .. formatNumber((sts.waste and sts.waste.amount) or 0, 1) .. "/" .. formatNumber(sts.wasteCapacity or 1, 1), "red")
  931.         drawProgressLine((sts.waste and sts.waste.amount) or 0, sts.wasteCapacity or 1, "red", "gray", formatPercentage(sts.wasteFilledPercentage or 0))
  932.         vY=vY+1
  933.  
  934.         if isScrammed then
  935.             local cancelText = " [Cancel SCRAM] "
  936.             local cancelY = qy + math.floor((qH - 6) / 2) + 2
  937.             local cancelX = iX + math.floor((cW - #cancelText) / 2)
  938.            
  939.             if cancelY >= contentBoxY and cancelY < contentBoxY + viewHeight then
  940.                 window.setCursorPos(cancelX, cancelY)
  941.                 window.setBackgroundColor(colors.orange)
  942.                 window.setTextColor(colors.black)
  943.                 window.write(cancelText)
  944.                
  945.                 table.insert(sharedData.pageButtonRegions, {
  946.                     type="fission_control",
  947.                     action="cancel_scram",
  948.                     deviceName=devInfo.name,
  949.                     target=targetName,
  950.                     xStart=cancelX,
  951.                     xEnd=cancelX + #cancelText - 1,
  952.                     pageY=cancelY
  953.                 })
  954.             end
  955.         end
  956.  
  957.         for i, quad in ipairs(sharedData.quadrantRegions[targetName] or {}) do
  958.             if quad.deviceName == devInfo.name then
  959.                 quad.maxOffset = maxOffset
  960.                 break
  961.             end
  962.         end
  963.  
  964.         if maxOffset > 0 then
  965.             local scrollX = qx + qW - 2
  966.             local upArrowY = qy + 1
  967.             local downArrowY = qy + qH - 6
  968.            
  969.             window.setCursorPos(scrollX, upArrowY); window.setBackgroundColor(colors.black); window.setTextColor(colors.white); window.write("^")
  970.             table.insert(sharedData.pageButtonRegions, {type="fission_scroll", deviceName=devInfo.name, direction=-1, maxOffset=maxOffset, xStart=scrollX, xEnd=scrollX, pageY=upArrowY, target=targetName})
  971.            
  972.             window.setCursorPos(scrollX, downArrowY); window.setBackgroundColor(colors.black); window.setTextColor(colors.white); window.write("v")
  973.             table.insert(sharedData.pageButtonRegions, {type="fission_scroll", deviceName=devInfo.name, direction=1, maxOffset=maxOffset, xStart=scrollX, xEnd=scrollX, pageY=downArrowY, target=targetName})
  974.  
  975.             local trackY = upArrowY + 1
  976.             local trackHeight = downArrowY - trackY
  977.             local handleHeight = math.max(1, math.floor(trackHeight * (viewHeight / contentHeight)))
  978.             local handleY = trackY + math.floor((trackHeight - handleHeight) * (clampedOffset / maxOffset))
  979.            
  980.             for i=0, trackHeight-1 do
  981.                 window.setCursorPos(scrollX, trackY + i)
  982.                 if trackY + i >= handleY and trackY + i < handleY + handleHeight then
  983.                     window.setBackgroundColor(colors.lightGray)
  984.                 else
  985.                     window.setBackgroundColor(colors.gray)
  986.                 end
  987.                 window.write(" ")
  988.             end
  989.  
  990.             table.insert(sharedData.pageButtonRegions, {
  991.                 type="fission_scroll_handle", deviceName=devInfo.name,
  992.                 xStart=scrollX, xEnd=scrollX, yStart=handleY, yEnd=handleY+handleHeight-1,
  993.                 trackY=trackY, trackH=trackHeight, maxOffset=maxOffset,
  994.                 target=targetName
  995.             })
  996.         end
  997.        
  998.         local burnRateY = qy + qH - 4
  999.         local buttonY_1 = qy + qH - 3
  1000.         local buttonY_2 = qy + qH - 2
  1001.  
  1002.         local tempB, tempT = window.getBackgroundColor(), window.getTextColor()
  1003.         window.setBackgroundColor(colors.black)
  1004.         window.setCursorPos(qx + 1, burnRateY); window.write(string.rep(" ", qW - 2))
  1005.         window.setCursorPos(qx + 1, buttonY_1); window.write(string.rep(" ", qW - 2))
  1006.         window.setCursorPos(qx + 1, buttonY_2); window.write(string.rep(" ", qW - 2))
  1007.         window.setBackgroundColor(tempB); window.setTextColor(tempT)
  1008.  
  1009.         local burnRateStr = string.format("Burn Rate: %.2f mB/t / %.2f mB/t", sts.burnRate or 0, sts.maxBurnRate or 0)
  1010.         local burnRateX = iX + math.floor((cW - #burnRateStr) / 2)
  1011.         window.setBackgroundColor(colors.black)
  1012.         window.setCursorPos(burnRateX, burnRateY)
  1013.         window.setTextColor(colors.white)
  1014.         window.write(burnRateStr)
  1015.        
  1016.         local topRowButtons = {
  1017.             {t="[ACTIVATE]", a="activate", c="lime"},
  1018.             {t="[Set Rate]", a="open_keypad", c="blue"},
  1019.             {t="[SCRAM]", a="scram", c="red"}
  1020.         }
  1021.         local totalTopWidth = 0
  1022.         for _, btnData in ipairs(topRowButtons) do totalTopWidth = totalTopWidth + #btnData.t + 1 end
  1023.         totalTopWidth = totalTopWidth - 1
  1024.        
  1025.         local currentButtonX = iX + math.floor((cW - totalTopWidth) / 2)
  1026.         for _, btnData in ipairs(topRowButtons) do
  1027.             window.setBackgroundColor(colors.black)
  1028.             window.setTextColor(getSafeColor(btnData.c))
  1029.            
  1030.             window.setCursorPos(currentButtonX, buttonY_1); window.write(btnData.t)
  1031.  
  1032.             local regionType = (btnData.a == "open_keypad") and "fission_open_keypad" or "fission_control"
  1033.             table.insert(sharedData.pageButtonRegions, {
  1034.                 type=regionType, target=targetName, deviceName=devInfo.name, action=btnData.a,
  1035.                 xStart=currentButtonX, xEnd=currentButtonX+#btnData.t-1, pageY=buttonY_1
  1036.             })
  1037.             currentButtonX = currentButtonX + #btnData.t + 1
  1038.         end
  1039.        
  1040.         local btnElements = { {t="[<<<]",a="dec_rate_10",c="darkRed"}, {t="[<<]",a="dec_rate_1",c="red"}, {t="[<]",a="dec_rate_01",c="orange"}, {t="[>]",a="inc_rate_01",c="lime"}, {t="[>>]",a="inc_rate_1",c="green"}, {t="[>>>]",a="inc_rate_10",c="darkGreen"} }
  1041.         local totalButtonsWidth = 0
  1042.         for _,el in ipairs(btnElements) do totalButtonsWidth = totalButtonsWidth + #el.t + 1 end; totalButtonsWidth = totalButtonsWidth -1
  1043.         currentButtonX = iX + math.floor((cW - totalButtonsWidth) / 2)
  1044.         for _,btnData in ipairs(btnElements) do
  1045.             window.setBackgroundColor(colors.black); window.setTextColor(getSafeColor(btnData.c))
  1046.             window.setCursorPos(currentButtonX, buttonY_2); window.write(btnData.t)
  1047.             table.insert(sharedData.pageButtonRegions,{type="fission_control",target=targetName,deviceName=devInfo.name,action=btnData.a,xStart=currentButtonX,xEnd=currentButtonX+#btnData.t-1,pageY=buttonY_2})
  1048.             currentButtonX = currentButtonX + #btnData.t + 1
  1049.         end
  1050.  
  1051.         window.setBackgroundColor(colors.black)
  1052.     end)
  1053. end
  1054.  
  1055. local function drawSummaryPanel(window, targetName, qx,qy,qW,qH)
  1056.     pcall(function()
  1057.         local oS = sharedData.overallTurbineStats; local borderColors = getBorderColors("turbineSummary")
  1058.         drawQuadrantBorder(window, qx,qy,qW,qH,borderColors.border, "Turbine Main Panel", borderColors.text)
  1059.         window.setBackgroundColor(colors.black); local iX, cY, cW, bDTW = qx+2, qy+2, qW-4, qW-4
  1060.         if not oS then window.setTextColor(colors.gray);window.setCursorPos(iX,cY);window.write("Calculating..."); return end
  1061.        
  1062.         window.setTextColor(colors.white);window.setCursorPos(iX,cY);window.write(("Total Turbines: "..(oS.totalTurbineCount or "N/A")):sub(1,cW));cY=cY+2
  1063.        
  1064.         window.setTextColor(colors.lightBlue);window.setCursorPos(iX,cY);window.write("Total FE:");window.setTextColor(colors.white);window.setCursorPos(iX+10,cY);window.write(("%s/%s (%s)"):format(formatNumber(oS.energyStored),formatNumber(oS.maxEnergy),formatPercentage(oS.energyFilledPercentage)):sub(1,cW-10));cY=cY+1
  1065.         drawDetailedProgressBar(window,oS.energyStored,oS.maxEnergy,bDTW,iX,cY,"green","gray","("..formatPercentage(oS.energyFilledPercentage)..")");cY=cY+2
  1066.        
  1067.         window.setTextColor(colors.lightGray);window.setCursorPos(iX,cY);window.write("Total Steam:");window.setTextColor(colors.white);window.setCursorPos(iX+13,cY);window.write(("%s/%s (%s)"):format(formatNumber(oS.steamAmount,0),formatNumber(oS.steamCapacity,0),formatPercentage(oS.steamFilledPercentage)):sub(1,cW-13));cY=cY+1
  1068.         drawDetailedProgressBar(window,oS.steamAmount,oS.steamCapacity,bDTW,iX,cY,"lightGray","gray","("..formatPercentage(oS.steamFilledPercentage)..")");cY=cY+2
  1069.        
  1070.         window.setTextColor(colors.green);window.setCursorPos(iX,cY);window.write("Total Prod:");window.setTextColor(colors.white);window.setCursorPos(iX+12,cY);window.write(("%s/%s FE/t"):format(formatNumber(oS.productionRate),formatNumber(oS.maxProduction)):sub(1,cW-12));cY=cY+1
  1071.        
  1072.         local bY=qy+qH-2;if bY>=cY then local totBW=0;local bE={};for _,mI in ipairs(dumpingModes)do local iF=sharedData.globalModeButtonFeedback.modeAPIName==mI.apiName and sharedData.globalModeButtonFeedback.expiryTime~=0;local iC=sharedData.globallyConsistentTurbineMode==mI.apiName and not iF;local btT=(iF or iC)and("["..mI.displayName.."]")or(" "..mI.displayName.." ");table.insert(bE,{text=btT,mode=mI,isFeedback=iF,isConsistent=iC});totBW=totBW+#btT end;totBW=totBW+(#dumpingModes-1);local bSX=iX+math.max(0,math.floor((cW-totBW)/2));local cBX=bSX;for _,bEl in ipairs(bE)do local cTCK="white";if bEl.isFeedback then cTCK=sharedData.globalModeButtonFeedback.colorKey or"blue"elseif bEl.isConsistent then cTCK="yellow"end;window.setTextColor(getSafeColor(cTCK,colors.white));if cBX+#bEl.text-1<qx+qW-1 then window.setCursorPos(cBX,bY);window.write(bEl.text);table.insert(sharedData.pageButtonRegions,{type="global_dump_mode",target=targetName,modeToSet=bEl.mode.apiName,xStart=cBX,xEnd=cBX+#bEl.text-1,pageY=bY});cBX=cBX+#bEl.text+1 else break end end end
  1073.     end)
  1074. end
  1075.  
  1076. local function drawDeviceQuadrant(window, targetName, itemToDraw, qx, qy, qW, qH)
  1077.     if not itemToDraw then return end
  1078.  
  1079.     if not sharedData.quadrantRegions[targetName] then sharedData.quadrantRegions[targetName] = {} end
  1080.     table.insert(sharedData.quadrantRegions[targetName], {
  1081.         x1 = qx, y1 = qy, x2 = qx + qW - 1, y2 = qy + qH - 1,
  1082.         deviceType = (itemToDraw.drawType == "device") and itemToDraw.info.type or nil,
  1083.         deviceName = (itemToDraw.drawType == "device") and itemToDraw.info.name or nil
  1084.     })
  1085.    
  1086.     if itemToDraw.drawType == "summary" then
  1087.         drawSummaryPanel(window, targetName, qx, qy, qW, qH)
  1088.         return
  1089.     end
  1090.  
  1091.     if itemToDraw.drawType == "device" then
  1092.         local devInfo = itemToDraw.info
  1093.         local stats = sharedData.deviceStats[devInfo.name]
  1094.         local err = sharedData.lastError[devInfo.name]
  1095.  
  1096.         if devInfo.type == "turbine" then
  1097.             drawTurbineInQuadrant(window, targetName, devInfo, stats, err, qx, qy, qW, qH)
  1098.         elseif devInfo.type == "inductionMatrix" then
  1099.             drawInductionMatrixQuadrant(window, targetName, devInfo, stats, err, qx, qy, qW, qH)
  1100.         elseif devInfo.type == "fusionReactor" then
  1101.             drawFusionReactorQuadrant(window, targetName, devInfo, stats, err, qx, qy, qW, qH)
  1102.         elseif devInfo.type == "boiler" then
  1103.             drawBoilerQuadrant(window, targetName, devInfo, stats, err, qx, qy, qW, qH)
  1104.         elseif devInfo.type == "thermalEvaporation" then
  1105.             drawThermalEvaporationQuadrant(window, targetName, devInfo, stats, err, qx, qy, qW, qH)
  1106.         elseif devInfo.type == "dynamicTank" then
  1107.             drawDynamicTankQuadrant(window, targetName, devInfo, stats, err, qx, qy, qW, qH)
  1108.         elseif devInfo.type == "sps" then
  1109.             drawSPSQuadrant(window, targetName, devInfo, stats, err, qx, qy, qW, qH)
  1110.         elseif devInfo.type == "fissionReactor" then
  1111.             drawFissionReactorQuadrant(window, targetName, devInfo, stats, err, qx, qy, qW, qH)
  1112.         else
  1113.             local title = devInfo.type .. " " .. (devInfo.typeId or "")
  1114.             local bC = getBorderColors(devInfo.type)
  1115.             drawQuadrantBorder(window, qx, qy, qW, qH, bC.border, title, bC.text)
  1116.         end
  1117.     end
  1118. end
  1119.  
  1120. local function drawKeypad(window)
  1121.     if not sharedData.keypad.isOpen then return end
  1122.  
  1123.     local keypadWidth = 23
  1124.     local keypadHeight = 11
  1125.  
  1126.     local qx, qy, qW, qH = sharedData.keypad.qx, sharedData.keypad.qy, sharedData.keypad.qW, sharedData.keypad.qH
  1127.     if not qW or qW == 0 or not qH or qH == 0 then
  1128.         qW, qH = window.getSize()
  1129.         qx, qy = 1, 1
  1130.     end
  1131.    
  1132.     local kx = qx + math.floor((qW - keypadWidth) / 2)
  1133.     local ky = qy + math.floor((qH - keypadHeight) / 2)
  1134.  
  1135.     window.setBackgroundColor(colors.black)
  1136.     for y = ky, ky + keypadHeight - 1 do
  1137.         window.setCursorPos(kx, y)
  1138.         window.write(string.rep(" ", keypadWidth))
  1139.     end
  1140.     drawQuadrantBorder(window, kx, ky, keypadWidth, keypadHeight, "cyan", "Set Burn Rate", "black")
  1141.  
  1142.     window.setBackgroundColor(colors.white)
  1143.     window.setTextColor(colors.black)
  1144.     window.setCursorPos(kx + 2, ky + 2)
  1145.     window.write(string.rep(" ", keypadWidth - 4))
  1146.     window.setCursorPos(kx + 2, ky + 2)
  1147.     window.write(sharedData.keypad.inputValue)
  1148.    
  1149.     if sharedData.keypad.message and sharedData.keypad.message ~= "" then
  1150.         window.setBackgroundColor(colors.black)
  1151.         window.setTextColor(colors.red)
  1152.         local msg = sharedData.keypad.message:sub(1, keypadWidth - 4)
  1153.         local msgX = kx + 2 + math.floor((keypadWidth - 4 - #msg) / 2)
  1154.         window.setCursorPos(msgX, ky + 3)
  1155.         window.write(msg)
  1156.     end
  1157.  
  1158.     local buttonLayout = {
  1159.         { {t='1',w=4}, {t='2',w=4}, {t='3',w=4}, {t='C',w=4} },
  1160.         { {t='4',w=4}, {t='5',w=4}, {t='6',w=4}, {t='<-',w=4} },
  1161.         { {t='7',w=4}, {t='8',w=4}, {t='9',w=4}, {t='OK',w=4} },
  1162.         { {t='.',w=4}, {t='0',w=4} }
  1163.     }
  1164.     local buttonY = ky + 4
  1165.     for _, row in ipairs(buttonLayout) do
  1166.         local buttonX = kx + 2
  1167.         for _, btn in ipairs(row) do
  1168.             local btnText = btn.t
  1169.             local btnWidth = btn.w
  1170.            
  1171.             local isControl = btnText == 'C' or btnText == '<-' or btnText == 'OK'
  1172.             local isConfirm = btnText == 'OK'
  1173.            
  1174.             if buttonX + btnWidth -1 <= kx + keypadWidth - 2 then
  1175.                 window.setBackgroundColor(isConfirm and colors.green or (isControl and colors.red or colors.gray))
  1176.                 window.setTextColor(colors.white)
  1177.                 window.setCursorPos(buttonX, buttonY)
  1178.  
  1179.                 local centerPad = math.floor((btnWidth - #btnText) / 2)
  1180.                 local paddedText = string.rep(" ", centerPad) .. btnText .. string.rep(" ", btnWidth - #btnText - centerPad)
  1181.                 window.write(paddedText)
  1182.  
  1183.                 local regionType
  1184.                 if btnText == 'C' then regionType = "keypad_clear"
  1185.                 elseif btnText == '<-' then regionType = "keypad_backspace"
  1186.                 elseif btnText == 'OK' then regionType = "keypad_confirm"
  1187.                 else regionType = "keypad_input" end
  1188.  
  1189.                 table.insert(sharedData.pageButtonRegions, {
  1190.                     type = regionType, value = btnText, target = sharedData.keypad.targetWindow,
  1191.                     xStart = buttonX, y = buttonY, xEnd = buttonX + btnWidth - 1, pageY = buttonY
  1192.                 })
  1193.                 buttonX = buttonX + btnWidth + 1
  1194.             end
  1195.         end
  1196.         buttonY = buttonY + 1
  1197.     end
  1198.     local closeText = "[X]"
  1199.     window.setBackgroundColor(colors.red); window.setTextColor(colors.white)
  1200.     window.setCursorPos(kx + keypadWidth - 1 - #closeText, ky); window.write(closeText)
  1201.     table.insert(sharedData.pageButtonRegions, {
  1202.         type="keypad_close", target=sharedData.keypad.targetWindow,
  1203.         xStart=kx + keypadWidth - 1 - #closeText, xEnd=kx + keypadWidth - 2, pageY=ky
  1204.     })
  1205. end
  1206.  
  1207. local function drawScreen(window, targetName, itemsPerPage, currentPage, totalPages)
  1208.     local w, h = window.getSize()
  1209.     window.setCursorBlink(false); window.setBackgroundColor(colors.black); window.clear()
  1210.    
  1211.     sharedData.quadrantRegions[targetName] = {}
  1212.    
  1213.     local isMonitor = (window ~= term)
  1214.    
  1215.     if #sharedData.drawableItems == 0 then
  1216.         if pcall(window.setTextScale, 1) then end
  1217.         local msg = "Scanning for Mekanism Devices..."; window.setCursorPos(math.floor((w - #msg)/2) + 1, math.floor(h/2)); window.setTextColor(colors.yellow); window.write(msg)
  1218.     else
  1219.         local itemsToDraw = {}
  1220.         if not isMonitor then
  1221.              local startIndex = (currentPage - 1) * itemsPerPage + 1
  1222.             for i = 0, itemsPerPage - 1 do
  1223.                 if startIndex + i <= #sharedData.drawableItems then
  1224.                     table.insert(itemsToDraw, sharedData.drawableItems[startIndex + i])
  1225.                 end
  1226.             end
  1227.         end
  1228.  
  1229.         if isMonitor then
  1230.             if #sharedData.drawableItems > 1 then
  1231.                 pcall(function() window.setTextScale(0.5) end)
  1232.             else
  1233.                 pcall(function() window.setTextScale(1) end)
  1234.             end
  1235.         else
  1236.             pcall(function() window.setTextScale(1) end)
  1237.         end
  1238.        
  1239.         local title = " Mekanism Device Monitor "; window.setCursorPos(math.max(1, math.floor((w - #title) / 2) + 1), 1); window.setTextColor(colors.yellow); window.write(title)
  1240.        
  1241.         local contentX = 2
  1242.         local contentY = 2
  1243.         local contentW = w - 2
  1244.         local contentH = h - 3
  1245.  
  1246.         if isMonitor then
  1247.             if #sharedData.drawableItems == 1 then
  1248.                 drawDeviceQuadrant(window, targetName, sharedData.drawableItems[1], contentX, contentY, contentW, contentH)
  1249.             else
  1250.                 local qW = math.floor(contentW / 2)
  1251.                 local qH = math.floor(contentH / 2)
  1252.                
  1253.                 local pageLayout = sharedData.pagedItemsMon[currentPage]
  1254.                 if pageLayout then
  1255.                     for _, panelData in ipairs(pageLayout) do
  1256.                         local layout = panelData.layout
  1257.                         local drawX = (layout.gridX == 1) and contentX or (contentX + qW)
  1258.                         local drawY = (layout.gridY == 1) and contentY or (contentY + qH)
  1259.                         local drawW = (layout.gridX == 2) and (contentW - qW) or qW
  1260.                        
  1261.                         local drawH = qH
  1262.                         if layout.gridY == 2 then
  1263.                             drawH = contentH - qH
  1264.                         end
  1265.                         if layout.gridH > 1 then
  1266.                             drawH = contentH
  1267.                         end
  1268.  
  1269.                         drawDeviceQuadrant(window, targetName, panelData.item, drawX, drawY, drawW, drawH)
  1270.                     end
  1271.                 end
  1272.             end
  1273.         else
  1274.             drawDeviceQuadrant(window, targetName, itemsToDraw[1], contentX, contentY, contentW, contentH)
  1275.         end
  1276.        
  1277.         if totalPages > 1 then
  1278.             local pageY = h
  1279.            
  1280.             if isMonitor then
  1281.                 local elements = {}
  1282.                 local maxVisiblePages = math.floor((w - 20) / 4)
  1283.  
  1284.                 table.insert(elements, { text = "< ", type = "prev" })
  1285.                 if totalPages <= maxVisiblePages then
  1286.                     for i = 1, totalPages do
  1287.                         table.insert(elements, { text = (i == currentPage) and ("["..i.."]") or (" "..i.." "), type = "page", pageNum = i})
  1288.                     end
  1289.                 else
  1290.                     local showPages = {}
  1291.                     table.insert(showPages, 1)
  1292.                     if currentPage > 3 then table.insert(showPages, -1) end
  1293.                     for i = -1, 1 do
  1294.                         if currentPage + i > 1 and currentPage + i < totalPages then
  1295.                             table.insert(showPages, currentPage + i)
  1296.                         end
  1297.                     end
  1298.                     if currentPage < totalPages - 2 then table.insert(showPages, -1) end
  1299.                     table.insert(showPages, totalPages)
  1300.                    
  1301.                     local uniquePages = {}; local seen = {}
  1302.                     for _, p in ipairs(showPages) do if not seen[p] then table.insert(uniquePages, p); seen[p] = true end end
  1303.  
  1304.                     for _, p in ipairs(uniquePages) do
  1305.                         if p == -1 then
  1306.                             table.insert(elements, { text = "... ", type = "ellipsis" })
  1307.                         else
  1308.                             table.insert(elements, { text = (p == currentPage) and ("["..p.."]") or (" "..p.." "), type = "page", pageNum = p})
  1309.                         end
  1310.                     end
  1311.                 end
  1312.                 table.insert(elements, { text = " >", type = "next" })
  1313.                
  1314.                 local totalBarW = 0
  1315.                 for _, elData in ipairs(elements) do totalBarW = totalBarW + #elData.text end
  1316.                
  1317.                 local cDX = math.max(1, math.floor((w - totalBarW) / 2) + 1)
  1318.                 for _, elData in ipairs(elements) do
  1319.                     if elData.type ~= "ellipsis" then
  1320.                         window.setTextColor((elData.type == "page" and elData.pageNum == currentPage) and colors.yellow or colors.white)
  1321.                         window.setCursorPos(cDX, pageY-1); window.write(elData.text)
  1322.                         table.insert(sharedData.pageButtonRegions, { type = elData.type, target = targetName, pageNum = elData.pageNum, xStart = cDX, xEnd = cDX + #elData.text - 1, pageY = pageY-1 })
  1323.                     else
  1324.                         window.setTextColor(colors.white)
  1325.                         window.setCursorPos(cDX, pageY-1); window.write(elData.text)
  1326.                     end
  1327.                     cDX = cDX + #elData.text
  1328.                 end
  1329.             else
  1330.                 local statusText = string.format("Page %d of %d", currentPage, totalPages)
  1331.                 local statusX = math.floor((w - #statusText) / 2) + 1
  1332.                 window.setCursorPos(statusX, pageY - 1)
  1333.                 window.setTextColor(colors.yellow)
  1334.                 window.write(statusText)
  1335.                 window.setTextColor(colors.white)
  1336.  
  1337.                 if targetName == "term" and #sharedData.drawableItems >= 2 then
  1338.                     local prevText = "< "
  1339.                     local nextText = " >"
  1340.                     local goTxt = " [Go] "
  1341.                     local totalWidth = #prevText + #goTxt + #nextText + 2
  1342.                     local cDX = math.floor((w - totalWidth) / 2) + 1
  1343.  
  1344.                     window.setCursorPos(cDX, pageY); window.write(prevText)
  1345.                     table.insert(sharedData.pageButtonRegions, { type = "prev", target = targetName, xStart = cDX, xEnd = cDX + #prevText - 1, pageY = pageY })
  1346.                     cDX = cDX + #prevText + 1
  1347.  
  1348.                     window.setCursorPos(cDX, pageY); window.setBackgroundColor(colors.blue); window.write(goTxt)
  1349.                     table.insert(sharedData.pageButtonRegions, {type = "goto", target = targetName, xStart=cDX, xEnd=cDX+#goTxt-1, pageY=pageY})
  1350.                     cDX = cDX + #goTxt + 1
  1351.                     window.setBackgroundColor(colors.black)
  1352.  
  1353.                     window.setCursorPos(cDX, pageY); window.write(nextText)
  1354.                     table.insert(sharedData.pageButtonRegions, { type = "next", target = targetName, xStart = cDX, xEnd = cDX + #nextText - 1, pageY = pageY })
  1355.                 else
  1356.                     local prevText = "< "
  1357.                     local nextText = " >"
  1358.                     local fullBarText = prevText .. " " .. nextText
  1359.                     local cDX = math.floor((w - #fullBarText) / 2) + 1
  1360.  
  1361.                     window.setCursorPos(cDX, pageY); window.write(prevText)
  1362.                     table.insert(sharedData.pageButtonRegions, { type = "prev", target = targetName, xStart = cDX, xEnd = cDX + #prevText - 1, pageY = pageY })
  1363.                     cDX = cDX + #prevText + string.len(" ")
  1364.                    
  1365.                     window.setCursorPos(cDX, pageY); window.write(nextText)
  1366.                     table.insert(sharedData.pageButtonRegions, { type = "next", target = targetName, xStart = cDX, xEnd = cDX + #nextText - 1, pageY = pageY })
  1367.                 end
  1368.             end
  1369.         end
  1370.     end
  1371.     local cfgTxt=" CONFIG "; local endTxt=" END ";
  1372.     window.setCursorPos(1,h); window.setBackgroundColor(colors.yellow); window.setTextColor(colors.black); window.write(cfgTxt)
  1373.     table.insert(sharedData.pageButtonRegions,{type="config",target=targetName,xStart=1,xEnd=#cfgTxt,pageY=h})
  1374.    
  1375.     window.setCursorPos(w-#endTxt+1,h); window.setBackgroundColor(colors.red); window.setTextColor(colors.black); window.write(endTxt)
  1376.     table.insert(sharedData.pageButtonRegions,{type="end",target=targetName,xStart=w-#endTxt+1,xEnd=w,pageY=h})
  1377.    
  1378.     window.setBackgroundColor(colors.black)
  1379. end
  1380.  
  1381.  
  1382. -- =================================================================
  1383. -- Core Tasks
  1384. -- =================================================================
  1385. local function fetchSingleDeviceData(devInfo)
  1386.     local devP=devInfo.peripheral; local devType=devInfo.type; local s,d
  1387.     if devType == "turbine" then
  1388.         s,d=pcall(function() local sD=devP.getSteam(); local sA=0; if sD and type(sD)=="table" and sD.amount then sA=sD.amount end; local eS,mE,eF,pR,mP,sC,sF,iF,dM,w,l,h = devP.getEnergy(),devP.getMaxEnergy(),devP.getEnergyFilledPercentage(),devP.getProductionRate(),devP.getMaxProduction(),devP.getSteamCapacity(),devP.getSteamFilledPercentage(),devP.isFormed(),devP.getDumpingMode(),devP.getWidth(),devP.getLength(),devP.getHeight(); return {energyStored=(eS or 0)/joulesToFeConversionFactor,maxEnergy=(mE or 0)/joulesToFeConversionFactor,energyFilledPercentage=eF,productionRate=(pR or 0)/joulesToFeConversionFactor,maxProduction=(mP or 0)/joulesToFeConversionFactor,steamAmount=sA,steamCapacity=sC,steamFilledPercentage=sF,isFormed=iF,dumpingMode=dM,width=w,length=l,height=h} end)
  1389.     elseif devType == "fusionReactor" then
  1390.         s,d=pcall(function() local w=devP.getWater();local st=devP.getSteam();local dtF=devP.getDTFuel();local deu=devP.getDeuterium();local tri=devP.getTritium();local fD={};if dtF and dtF.name~="mekanism:empty"and(dtF.amount or 0)>0 then fD.hasDTFuel=true;fD.dtFuelAmount=dtF.amount or 0;fD.dtFuelCapacity=devP.getDTFuelCapacity();fD.dtFuelFilledPercentage=devP.getDTFuelFilledPercentage()else fD.hasDTFuel=false;fD.deuteriumAmount=deu.amount or 0;fD.deuteriumCapacity=devP.getDeuteriumCapacity();fD.deuteriumFilledPercentage=devP.getDeuteriumFilledPercentage();fD.tritiumAmount=tri.amount or 0;fD.tritiumCapacity=devP.getTritiumCapacity();fD.tritiumFilledPercentage=devP.getTritiumFilledPercentage()end;return{waterAmount=(w and w.amount or 0),waterCapacity=devP.getWaterCapacity(),waterFilledPercentage=devP.getWaterFilledPercentage(),steamAmount=(st and st.amount or 0),steamCapacity=devP.getSteamCapacity(),steamFilledPercentage=devP.getSteamFilledPercentage(),fuel=fD,caseTemp=devP.getCaseTemperature(),plasmaTemp=devP.getPlasmaTemperature(),injectionRate=devP.getInjectionRate(),productionRate=(devP.getProductionRate()or 0)/joulesToFeConversionFactor,isIgnited=devP.isIgnited()}end)
  1391.    elseif devType == "inductionMatrix" then
  1392.     s,d=pcall(function()
  1393.         local stats={}
  1394.         stats.isFormed = devP.isFormed()
  1395.        
  1396.         -- REMOVED: stats.mode = devP.getMode() (This was causing the crash)
  1397.        
  1398.         stats.energyFE = (devP.getEnergy() or 0) / joulesToFeConversionFactor
  1399.         stats.maxEnergyFE = (devP.getMaxEnergy() or 0) / joulesToFeConversionFactor
  1400.         stats.energyFilledPercentage = devP.getEnergyFilledPercentage() or 0
  1401.        
  1402.         -- Calculate I/O Rate
  1403.         local lastInput = devP.getLastInput() or 0
  1404.         local lastOutput = devP.getLastOutput() or 0
  1405.         stats.energyChangeRatePerTick = (lastInput - lastOutput) / joulesToFeConversionFactor
  1406.        
  1407.         -- Get structural info
  1408.         stats.installedCells = devP.getInstalledCells and devP.getInstalledCells() or "N/A"
  1409.         stats.installedProviders = devP.getInstalledProviders and devP.getInstalledProviders() or "N/A"
  1410.        
  1411.         if stats.isFormed then
  1412.             stats.width = devP.getWidth()
  1413.             stats.height = devP.getHeight()
  1414.             stats.length = devP.getLength()
  1415.         end
  1416.        
  1417.         return stats
  1418.     end)
  1419.     elseif devType == "boiler" then
  1420.         s,d=pcall(function() local waterStack=devP.getWater(); local steamStack=devP.getSteam(); return {isFormed=devP.isFormed(),temperature=devP.getTemperature(),waterAmount=waterStack.amount or 0,waterCapacity=devP.getWaterCapacity(),waterFilledPercentage=devP.getWaterFilledPercentage(),steamAmount=steamStack.amount or 0,steamCapacity=devP.getSteamCapacity(),steamFilledPercentage=devP.getSteamFilledPercentage(),boilRate=devP.getBoilRate(),maxBoilRate=devP.getMaxBoilRate(),envLoss=devP.getEnvironmentalLoss()} end)
  1421.     elseif devType == "thermalEvaporation" then
  1422.         s,d=pcall(function() local data={}; data.isFormed=devP.isFormed(); if data.isFormed then local iF=devP.getInput();local oF=devP.getOutput(); data.temperature=devP.getTemperature();data.height=devP.getHeight(); data.inputName=iF.name;data.inputAmount=iF.amount or 0;data.inputCapacity=devP.getInputCapacity();data.inputFilledPercentage=devP.getInputFilledPercentage(); data.outputName=oF.name;data.outputAmount=oF.amount or 0;data.outputCapacity=devP.getOutputCapacity();data.outputFilledPercentage=devP.getOutputFilledPercentage(); data.production=devP.getProductionAmount(); else for k,v in pairs({temperature=0,height=0,inputName="mekanism:empty",inputAmount=0,inputCapacity=0,inputFilledPercentage=0,outputName="mekanism:empty",outputAmount=0,outputCapacity=0,outputFilledPercentage=0,production=0})do data[k]=v end end; return data end)
  1423.     elseif devType == "dynamicTank" then
  1424.         s,d=pcall(function()
  1425.             local stats = {}
  1426.             stats.isFormed = devP.isFormed()
  1427.             stats.stored = devP.getStored()
  1428.             local isChemical = stats.stored and stats.stored.name and not string.find(stats.stored.name, "minecraft:") and string.find(stats.stored.name, ":")
  1429.             if isChemical then
  1430.                 stats.capacity = devP.getChemicalTankCapacity() or 0
  1431.             else
  1432.                 local s_fluid, fluid_cap = pcall(devP.getTankCapacity)
  1433.                 if s_fluid then
  1434.                     stats.capacity = fluid_cap
  1435.                 else
  1436.                     stats.capacity = devP.getChemicalTankCapacity() or 0
  1437.                 end
  1438.             end
  1439.             stats.filledPercentage = (stats.stored and stats.capacity > 0 and stats.stored.amount) and stats.stored.amount / stats.capacity or 0
  1440.             if stats.isFormed then
  1441.                 stats.width=devP.getWidth()
  1442.                 stats.height=devP.getHeight()
  1443.                 stats.length=devP.getLength()
  1444.             end
  1445.             stats.editMode = devP.getContainerEditMode()
  1446.             return stats
  1447.         end)
  1448.     elseif devType == "sps" then
  1449.         s,d=pcall(function()
  1450.             local stats = {}
  1451.             stats.isFormed = devP.isFormed()
  1452.             stats.input = devP.getInput()
  1453.             stats.inputCapacity = devP.getInputCapacity()
  1454.             stats.inputFilledPercentage = devP.getInputFilledPercentage()
  1455.             stats.output = devP.getOutput()
  1456.             stats.outputCapacity = devP.getOutputCapacity()
  1457.             stats.outputFilledPercentage = devP.getOutputFilledPercentage()
  1458.             stats.processRate = devP.getProcessRate()
  1459.             if stats.isFormed then
  1460.                 stats.coils = devP.getCoils()
  1461.                 stats.width = devP.getWidth()
  1462.                 stats.height = devP.getHeight()
  1463.                 stats.length = devP.getLength()
  1464.             end
  1465.             return stats
  1466.         end)
  1467.     elseif devType == "fissionReactor" then
  1468.         s,d=pcall(function()
  1469.             local stats = {}
  1470.             stats.isFormed = devP.isFormed()
  1471.             if stats.isFormed then
  1472.                 stats.temperature = tonumber(devP.getTemperature()) or 0
  1473.                 stats.damagePercent = tonumber(devP.getDamagePercent()) or 0
  1474.                 stats.burnRate = tonumber(devP.getBurnRate()) or 0
  1475.                 stats.maxBurnRate = tonumber(devP.getFuelAssemblies()) or 0
  1476.                 stats.actualBurnRate = tonumber(devP.getActualBurnRate()) or 0
  1477.                 stats.fuel = devP.getFuel() or {amount = 0}
  1478.                 stats.fuelCapacity = tonumber(devP.getFuelCapacity()) or 1
  1479.                 stats.fuelFilledPercentage = tonumber(devP.getFuelFilledPercentage()) or 0
  1480.                 stats.coolant = devP.getCoolant() or {amount = 0}
  1481.                 stats.coolantCapacity = tonumber(devP.getCoolantCapacity()) or 1
  1482.                 stats.coolantFilledPercentage = tonumber(devP.getCoolantFilledPercentage()) or 0
  1483.                 stats.heatedCoolant = devP.getHeatedCoolant() or {amount = 0}
  1484.                 stats.heatedCoolantCapacity = tonumber(devP.getHeatedCoolantCapacity()) or 1
  1485.                 stats.heatedCoolantFilledPercentage = tonumber(devP.getHeatedCoolantFilledPercentage()) or 0
  1486.                 stats.waste = devP.getWaste() or {amount = 0}
  1487.                 stats.wasteCapacity = tonumber(devP.getWasteCapacity()) or 1
  1488.                 stats.wasteFilledPercentage = tonumber(devP.getWasteFilledPercentage()) or 0
  1489.                 stats.heatCapacity = tonumber(devP.getHeatCapacity()) or 1
  1490.                 stats.heatingRate = tonumber(devP.getHeatingRate()) or 0
  1491.  
  1492.                 if sharedData.scrammingReactors[devInfo.name] then
  1493.                     stats.status = "SCRAM"
  1494.                 elseif stats.damagePercent >= 100 then
  1495.                     stats.status = "MELTDOWN"
  1496.                 elseif stats.actualBurnRate > 0 then
  1497.                     stats.status = "ACTIVE"
  1498.                 else
  1499.                     stats.status = "IDLE"
  1500.                 end
  1501.                
  1502.             else
  1503.                 stats.temperature = 0; stats.damagePercent = 0; stats.burnRate = 0; stats.maxBurnRate = 0; stats.actualBurnRate = 0;
  1504.                 stats.fuel = {amount = 0}; stats.fuelCapacity = 1; stats.fuelFilledPercentage = 0;
  1505.                 stats.coolant = {amount = 0}; stats.coolantCapacity = 1; stats.coolantFilledPercentage = 0;
  1506.                 stats.waste = {amount = 0}; stats.wasteCapacity = 1; stats.wasteFilledPercentage = 0;
  1507.                 stats.status = "UNFORMED"; stats.heatCapacity = 1; stats.heatingRate = 0;
  1508.             end
  1509.             return stats
  1510.         end)
  1511.     end
  1512.     if s then sharedData.deviceStats[devInfo.name]=d; sharedData.lastError[devInfo.name]=nil else sharedData.lastError[devInfo.name]="Fetch Error"; debugPrint("[ERROR] Fetching "..devInfo.name..": "..tostring(d)) end
  1513. end
  1514.  
  1515. local function fetchTask()
  1516.     while sharedData.programRunning do
  1517.         local fetchFunctions = {}
  1518.         for _, devInfo in ipairs(sharedData.foundDevices) do
  1519.             table.insert(fetchFunctions, function() fetchSingleDeviceData(devInfo) end)
  1520.         end
  1521.         if #fetchFunctions > 0 then
  1522.             parallel.waitForAll(unpack(fetchFunctions))
  1523.         end
  1524.        
  1525.         for _, devInfo in ipairs(sharedData.foundDevices) do
  1526.             if devInfo.type == "fissionReactor" then
  1527.                 local devName = devInfo.name
  1528.                 local stats = sharedData.deviceStats[devName]
  1529.                
  1530.                 if stats and not sharedData.scrammingReactors[devName] and stats.actualBurnRate > 0 then
  1531.                     local needsScram = false
  1532.                     local reason = ""
  1533.  
  1534.                     if stats.coolantFilledPercentage and stats.coolantFilledPercentage <= fissionLowCoolantPercent then
  1535.                         needsScram = true
  1536.                         reason = "low coolant"
  1537.                     elseif stats.temperature > fissionCriticalTemp then
  1538.                         needsScram = true
  1539.                         reason = "critical temperature"
  1540.                     elseif stats.damagePercent > fissionCriticalDamage then
  1541.                         needsScram = true
  1542.                         reason = "critical damage"
  1543.                     end
  1544.  
  1545.                     if needsScram then
  1546.                         local dev = peripheral.wrap(devName)
  1547.                         if dev then
  1548.                             pcall(dev.scram)
  1549.                             sharedData.scrammingReactors[devName] = true
  1550.                             term.setCursorPos(1,1); term.clearLine(); term.setTextColor(colors.red)
  1551.                             print("[CRITICAL] Fission Reactor "..devName.." auto-scrammed: "..reason.."!")
  1552.                             term.setTextColor(colors.white)
  1553.                         end
  1554.                     end
  1555.                 end
  1556.  
  1557.                 if sharedData.scrammingReactors[devName] and stats and (stats.temperature < fissionScramResetTempK or stats.coolantFilledPercentage >= fissionSafeCoolantPercent) then
  1558.                     sharedData.scrammingReactors[devName] = nil
  1559.                 end
  1560.             end
  1561.         end
  1562.  
  1563.         local T={energyStored=0,maxEnergy=0,productionRate=0,maxProduction=0,steamAmount=0,steamCapacity=0,formedCount=0,totalTurbineCount=0};local fTM=nil;local aTMS=true;local tFFMC=false
  1564.         for _,dI in ipairs(sharedData.foundDevices)do local S=sharedData.deviceStats[dI.name];if S and dI.type=="turbine"then T.totalTurbineCount=T.totalTurbineCount+1;T.energyStored=T.energyStored+(S.energyStored or 0);T.maxEnergy=T.maxEnergy+(S.maxEnergy or 0);T.productionRate=T.productionRate+(S.productionRate or 0);T.maxProduction=T.maxProduction+(S.maxProduction or 0);T.steamAmount=T.steamAmount+(S.steamAmount or 0);T.steamCapacity=T.steamCapacity+(S.steamCapacity or 0);if S.isFormed then T.formedCount=T.formedCount+1 end;if not tFFMC then fTM=S.dumpingMode;tFFMC=true elseif aTMS and S.dumpingMode~=fTM then aTMS=false end elseif not S and dI.type=="turbine"then aTMS=false end end;if T.totalTurbineCount==0 then aTMS=false end;if aTMS and fTM~=nil then sharedData.globallyConsistentTurbineMode=fTM else sharedData.globallyConsistentTurbineMode=nil end;if T.maxEnergy>0 then T.energyFilledPercentage=T.energyStored/T.maxEnergy else T.energyFilledPercentage=0 end;if T.steamCapacity>0 then T.steamFilledPercentage=T.steamAmount/T.steamCapacity else T.steamFilledPercentage=0 end;sharedData.overallTurbineStats=T
  1565.  
  1566.         sharedData.needsRedraw = true
  1567.         for i = 1, math.ceil(updateInterval / 0.1) do
  1568.             if not sharedData.programRunning then break end
  1569.             sleep(0.1)
  1570.         end
  1571.     end
  1572. end
  1573.  
  1574.  
  1575. local function drawGoToMenu(window, totalPages)
  1576.     local w, h = window.getSize()
  1577.    
  1578.     local numPages = totalPages
  1579.     local pagesPerColumn = math.ceil(numPages / 2)
  1580.     local menuHeight = pagesPerColumn + 4
  1581.     local menuWidth = 20
  1582.    
  1583.     local startX = math.floor((w - menuWidth) / 2) + 1
  1584.     local startY = math.floor((h - menuHeight) / 2) + 1
  1585.    
  1586.     -- Draw the menu background and border
  1587.     window.setBackgroundColor(colors.black)
  1588.     for y = startY, startY + menuHeight - 1 do
  1589.         window.setCursorPos(startX, y)
  1590.         window.write(string.rep(" ", menuWidth))
  1591.     end
  1592.     drawQuadrantBorder(window, startX, startY, menuWidth, menuHeight, "cyan", "Go to Page", "black")
  1593.  
  1594.     -- Draw the page number buttons
  1595.     local col1X = startX + 3
  1596.     local col2X = startX + 11
  1597.     window.setTextColor(colors.white)
  1598.     for i = 1, pagesPerColumn do
  1599.         local page1 = i
  1600.         local page2 = i + pagesPerColumn
  1601.        
  1602.         if page1 <= numPages then
  1603.             window.setCursorPos(col1X, startY + 1 + i)
  1604.             window.write(string.format("Page %-3d", page1))
  1605.             table.insert(sharedData.pageButtonRegions, { type = "goto_page", page = page1, target = "term", pageY = startY + 1 + i, xStart = col1X, xEnd = col1X + 8 })
  1606.         end
  1607.  
  1608.         if page2 <= numPages then
  1609.             window.setCursorPos(col2X, startY + 1 + i)
  1610.             window.write(string.format("Page %-3d", page2))
  1611.             table.insert(sharedData.pageButtonRegions, { type = "goto_page", page = page2, target = "term", pageY = startY + 1 + i, xStart = col2X, xEnd = col2X + 8 })
  1612.         end
  1613.     end
  1614.    
  1615.     -- Draw the close button
  1616.     local closeText = "[ Close ]"
  1617.     local closeX = startX + math.floor((menuWidth - #closeText) / 2)
  1618.     local closeY = startY + menuHeight - 2
  1619.     window.setCursorPos(closeX, closeY)
  1620.     window.setBackgroundColor(colors.red)
  1621.     window.setTextColor(colors.white)
  1622.     window.write(closeText)
  1623.     table.insert(sharedData.pageButtonRegions, { type = "goto_close", target = "term", pageY = closeY, xStart = closeX, xEnd = closeX + #closeText - 1 })
  1624.     window.setBackgroundColor(colors.black)
  1625. end
  1626.  
  1627.  
  1628. local function displayTask()
  1629.     local allWindows = { term = term }
  1630.     for _, mon in ipairs(monitors) do
  1631.         allWindows[peripheral.getName(mon)] = mon
  1632.     end
  1633.  
  1634.     while sharedData.programRunning do
  1635.         if sharedData.needsRedraw then
  1636.            
  1637.             -- [FIX] Save old buttons to backup so clicks still work during the redraw pause
  1638.             if sharedData.pageButtonRegions and #sharedData.pageButtonRegions > 0 then
  1639.                 sharedData.backupRegions = sharedData.pageButtonRegions
  1640.             end
  1641.             sharedData.pageButtonRegions = {}
  1642.  
  1643.             if sharedData.isMenuOpen then
  1644.                 drawConfigurationMenu()
  1645.             elseif sharedData.isGoToMenuOpen then
  1646.                 drawGoToMenu(term, sharedData.totalPagesTerm)
  1647.             else
  1648.                 calculateMonitorPages()
  1649.                 sharedData.flashToggle = not sharedData.flashToggle
  1650.                
  1651.                 local winCount = 0
  1652.                 for targetName, window in pairs(allWindows) do
  1653.                     if window then
  1654.                         local isMonitor = (window ~= term)
  1655.                         local itemsPerPage = isMonitor and devicesPerPageMonitor or devicesPerPageTerminal
  1656.                         local currentPage = isMonitor and sharedData.monitorStates[targetName].currentPage or sharedData.currentPageTerm
  1657.                         local totalPages = isMonitor and sharedData.totalPagesMon or sharedData.totalPagesTerm
  1658.                        
  1659.                         pcall(drawScreen, window, targetName, itemsPerPage, currentPage, totalPages)
  1660.                        
  1661.                         winCount = winCount + 1
  1662.                         if winCount % 2 == 0 then sleep(0) end
  1663.                     end
  1664.                 end
  1665.             end
  1666.             sharedData.needsRedraw = false
  1667.         end
  1668.  
  1669.         if sharedData.keypad.isOpen then
  1670.             local targetName = sharedData.keypad.targetWindow
  1671.             local window = (targetName == "term") and term or allWindows[targetName]
  1672.             if window then
  1673.                 -- Clear keypad regions from previous draw to prevent ghost clicks
  1674.                 for i = #sharedData.pageButtonRegions, 1, -1 do
  1675.                     if string.find(sharedData.pageButtonRegions[i].type, "keypad_") then
  1676.                         table.remove(sharedData.pageButtonRegions, i)
  1677.                     end
  1678.                 end
  1679.                 drawKeypad(window)
  1680.             end
  1681.         end
  1682.  
  1683.         sleep(displayRefreshDelay)
  1684.     end
  1685. end
  1686.  
  1687.  
  1688. local function actionQueueProcessorTask()
  1689.     while sharedData.programRunning do
  1690.         if #sharedData.actionQueue > 0 then
  1691.             local task = table.remove(sharedData.actionQueue, 1)
  1692.            
  1693.             if task.type == "fusion_ignite" then
  1694.                 if sharedData.config.useMoreRed then
  1695.                     setBundledBit(sharedData.config.moreRedOutputSide, sharedData.config.fusionPowerDownColor, false)
  1696.                     pulseBundledRedstone(sharedData.config.moreRedOutputSide, sharedData.config.fusionIgnitionColor)
  1697.                 else
  1698.                     redstone.setOutput(sharedData.config.fusionPowerDownSide, false)
  1699.                     pulseRedstone(sharedData.config.fusionIgnitionSide)
  1700.                 end
  1701.             elseif task.type == "fusion_power_down" then
  1702.                 if sharedData.config.useMoreRed then
  1703.                     setBundledBit(sharedData.config.moreRedOutputSide, sharedData.config.fusionPowerDownColor, task.state)
  1704.                 else
  1705.                     redstone.setOutput(sharedData.config.fusionPowerDownSide, task.state)
  1706.                 end
  1707.                
  1708.                 local dev = peripheral.wrap(task.deviceName)
  1709.                 if dev then pcall(dev.setInjectionRate, 0) end
  1710.             elseif task.type == "fusion_injection" then
  1711.                 if sharedData.config.useMoreRed then
  1712.                     setBundledBit(sharedData.config.moreRedOutputSide, sharedData.config.fusionPowerDownColor, false)
  1713.                 else
  1714.                     redstone.setOutput(sharedData.config.fusionPowerDownSide, false)
  1715.                 end
  1716.                
  1717.                 local dev = peripheral.wrap(task.deviceName)
  1718.                 if dev then
  1719.                     local s, cRate = pcall(dev.getInjectionRate)
  1720.                     if s and cRate then
  1721.                         local newRate = cRate
  1722.                         if task.action=="set_min"then newRate=minInjectionRate
  1723.                         elseif task.action=="set_max"then newRate=maxInjectionRate
  1724.                         elseif task.action=="inc_rate"then newRate=math.min(maxInjectionRate,cRate+injectionRateStep)
  1725.                         elseif task.action=="dec_rate"then newRate=math.max(minInjectionRate,cRate-injectionRateStep)
  1726.                         end
  1727.                         if newRate ~= cRate then pcall(dev.setInjectionRate, newRate) end
  1728.                     end
  1729.                 end
  1730.             elseif task.type == "fission_control" then
  1731.                 local dev = peripheral.wrap(task.deviceName)
  1732.                 if dev then
  1733.                     if task.action == "scram" then
  1734.                         pcall(dev.scram); sharedData.scrammingReactors[task.deviceName] = true
  1735.                     elseif task.action == "cancel_scram" then
  1736.                         sharedData.scrammingReactors[task.deviceName] = nil
  1737.                     elseif task.action == "activate" then
  1738.                         pcall(dev.activate); sharedData.scrammingReactors[task.deviceName] = nil
  1739.                     elseif task.action == "set_rate" then
  1740.                         pcall(dev.setBurnRate, task.value)
  1741.                     else
  1742.                         local s, cRate = pcall(dev.getBurnRate)
  1743.                         if s and cRate then
  1744.                             local newRate = cRate
  1745.                             if task.action == "inc_rate_01" then newRate = cRate + 0.1
  1746.                             elseif task.action == "inc_rate_1" then newRate = cRate + 1.0
  1747.                             elseif task.action == "inc_rate_10" then newRate = cRate + 10.0
  1748.                             elseif task.action == "dec_rate_01" then newRate = cRate - 0.1
  1749.                             elseif task.action == "dec_rate_1" then newRate = cRate - 1.0
  1750.                             elseif task.action == "dec_rate_10" then newRate = cRate - 10.0
  1751.                             end
  1752.                             pcall(dev.setBurnRate, newRate)
  1753.                         end
  1754.                     end
  1755.                 end
  1756.             end
  1757.             sleep(0.05)
  1758.         else
  1759.             sleep(0.05)
  1760.         end
  1761.     end
  1762. end
  1763.  
  1764. local function setupSirenAudio()
  1765.     if #sharedData.speakers == 0 then return end
  1766.  
  1767.     local function generateSlidingWave(start_freq, end_freq, duration)
  1768.         local rate = 48000
  1769.         local bufferSize = math.floor(rate * duration)
  1770.         local buffer = {}
  1771.         local t = 0
  1772.         for i = 1, bufferSize do
  1773.             local progress = i / bufferSize
  1774.             local current_freq = start_freq + (end_freq - start_freq) * progress
  1775.             local dt = 2 * math.pi * current_freq / rate
  1776.             t = (t + dt) % (math.pi * 2)
  1777.             buffer[i] = math.floor(math.sin(t) * 127)
  1778.         end
  1779.         return buffer
  1780.     end
  1781.    
  1782.     local whoopUp = generateSlidingWave(400, 800, 0.25)
  1783.     local whoopDown = generateSlidingWave(800, 400, 0.25)
  1784.  
  1785.     sharedData.sirenBuffers.alarm = {}
  1786.     for i=1, #whoopUp do table.insert(sharedData.sirenBuffers.alarm, whoopUp[i]) end
  1787.     for i=1, #whoopDown do table.insert(sharedData.sirenBuffers.alarm, whoopDown[i]) end
  1788. end
  1789.  
  1790. local function sirenTask()
  1791.     local lastAlarmState = false
  1792.     while sharedData.programRunning do
  1793.         local isScramActive = countKeys(sharedData.scrammingReactors) > 0
  1794.  
  1795.         if sharedData.config.useIndustrialAlarm then
  1796.             if isScramActive ~= lastAlarmState then
  1797.                 if sharedData.config.useMoreRed then
  1798.                     setBundledBit(sharedData.config.moreRedOutputSide, sharedData.config.industrialAlarmColor, isScramActive)
  1799.                 else
  1800.                     redstone.setOutput(sharedData.config.industrialAlarmSide, isScramActive)
  1801.                 end
  1802.                 lastAlarmState = isScramActive
  1803.             end
  1804.             for i = 1, 5 do
  1805.                 if not sharedData.programRunning then break end
  1806.                 sleep(0.1)
  1807.             end
  1808.         else
  1809.             if isScramActive then
  1810.                 local audioTasks = {}
  1811.                 for _, speaker in ipairs(sharedData.speakers) do
  1812.                     table.insert(audioTasks, function()
  1813.                         speaker.playAudio(sharedData.sirenBuffers.alarm)
  1814.                     end)
  1815.                 end
  1816.                 if #audioTasks > 0 then
  1817.                     parallel.waitForAll(unpack(audioTasks))
  1818.                 end
  1819.                 for i = 1, 15 do
  1820.                     if not sharedData.programRunning then break end
  1821.                     sleep(0.1)
  1822.                 end
  1823.             else
  1824.                 for i = 1, 5 do
  1825.                     if not sharedData.programRunning then break end
  1826.                     sleep(0.1)
  1827.                 end
  1828.             end
  1829.         end
  1830.     end
  1831. end
  1832.  
  1833.  
  1834. local function eventListenerTask()
  1835.     while sharedData.programRunning do
  1836.         local event, p1, p2, p3, p4 = os.pullEvent()
  1837.         if event == "key" and p1 == keys.terminate then
  1838.             sharedData.programRunning = false
  1839.             break
  1840.         elseif event == "term_resize" then
  1841.             sharedData.needsRedraw = true
  1842.         end
  1843.  
  1844.         if sharedData.keypad.isOpen then
  1845.             if event == "mouse_click" or event == "monitor_touch" then
  1846.                 local clickType, clickX, clickY
  1847.                 if event == "mouse_click" then clickType, clickX, clickY = "term", p2, p3
  1848.                 else clickType, clickX, clickY = p1, p2, p3 end
  1849.  
  1850.                 for i = #sharedData.pageButtonRegions, 1, -1 do
  1851.                     local region = sharedData.pageButtonRegions[i]
  1852.                     if region and region.target == clickType and clickY >= (region.pageY or region.yStart or region.y) and clickY <= (region.pageY or region.yEnd or region.y) and clickX >= (region.xStart or region.x1) and clickX <= (region.xEnd or region.x2) then
  1853.                         if region.type == "keypad_close" then
  1854.                             sharedData.keypad.isOpen = false
  1855.                             sharedData.needsRedraw = true
  1856.                             break
  1857.                         elseif region.type == "keypad_input" then
  1858.                             if #sharedData.keypad.inputValue < 10 then
  1859.                                 sharedData.keypad.inputValue = sharedData.keypad.inputValue .. region.value
  1860.                                 sharedData.keypad.message = ""
  1861.                             end
  1862.                             break
  1863.                         elseif region.type == "keypad_backspace" then
  1864.                             sharedData.keypad.inputValue = string.sub(sharedData.keypad.inputValue, 1, -2)
  1865.                             sharedData.keypad.message = ""
  1866.                             break
  1867.                         elseif region.type == "keypad_clear" then
  1868.                             sharedData.keypad.inputValue = ""
  1869.                             sharedData.keypad.message = ""
  1870.                             break
  1871.                         elseif region.type == "keypad_confirm" then
  1872.                             local val = tonumber(sharedData.keypad.inputValue)
  1873.                             if val and val >= 0 then
  1874.                                 table.insert(sharedData.actionQueue, {
  1875.                                     type = "fission_control",
  1876.                                     deviceName = sharedData.keypad.targetDevice,
  1877.                                     action = "set_rate",
  1878.                                     value = val
  1879.                                 })
  1880.                                 sharedData.keypad.isOpen = false
  1881.                                 sharedData.needsRedraw = true
  1882.                             else
  1883.                                 sharedData.keypad.message = "Invalid number!"
  1884.                                 sharedData.keypad.inputValue = ""
  1885.                             end
  1886.                             break
  1887.                         end
  1888.                     end
  1889.                 end
  1890.             end
  1891.         elseif sharedData.isMenuOpen then
  1892.             -- Config menu handling
  1893.             if event == "mouse_up" then
  1894.                 sharedData.configMenu.dragging = nil
  1895.             elseif event == "mouse_drag" then
  1896.                 if sharedData.configMenu.dragging then
  1897.                     local dragY = p3
  1898.                     local yDelta = dragY - sharedData.configMenu.dragging.initialMouseY
  1899.                     local offsetDelta = (yDelta / sharedData.configMenu.dragging.trackH) * sharedData.configMenu.dragging.maxOffset
  1900.                     local newOffset = sharedData.configMenu.dragging.initialOffset + offsetDelta
  1901.                     local clamped = math.floor(math.max(0, math.min(newOffset, sharedData.configMenu.dragging.maxOffset)) + 0.5)
  1902.                     if sharedData.configMenu.scrollState.offset ~= clamped then
  1903.                         sharedData.configMenu.scrollState.offset = clamped
  1904.                         sharedData.needsRedraw = true
  1905.                     end
  1906.                 end
  1907.             elseif event == "mouse_scroll" and sharedData.configMenu.page == 4 and sharedData.configMenu.scrollState.maxOffset > 0 then
  1908.                 local scrollDir = p1
  1909.                 if scrollDir then
  1910.                     local newOffset = sharedData.configMenu.scrollState.offset + scrollDir
  1911.                     local clamped = math.max(0, math.min(sharedData.configMenu.scrollState.maxOffset, newOffset))
  1912.                     if sharedData.configMenu.scrollState.offset ~= clamped then
  1913.                         sharedData.configMenu.scrollState.offset = clamped
  1914.                         sharedData.needsRedraw = true
  1915.                     end
  1916.                 end
  1917.             elseif event == "mouse_click" or event == "monitor_touch" then
  1918.                 local clickType, clickX, clickY
  1919.                 if event == "mouse_click" then clickType, clickX, clickY = "term", p2, p3
  1920.                 else clickType, clickX, clickY = p1, p2, p3 end
  1921.  
  1922.                 for i = #sharedData.pageButtonRegions, 1, -1 do
  1923.                     local r = sharedData.pageButtonRegions[i]
  1924.                     local yStart = r.y or r.yStart or r.pageY
  1925.                     local yEnd = r.y or r.yEnd or r.pageY
  1926.                     local xStart = r.x1 or r.xStart
  1927.                     local xEnd = r.x2 or r.xEnd
  1928.  
  1929.                     if r and r.target == clickType and clickY >= yStart and clickY <= yEnd and clickX >= xStart and clickX <= xEnd then
  1930.                         sharedData.needsRedraw = true
  1931.                         if r.type == "config_scroll_handle" then
  1932.                             sharedData.configMenu.dragging = {
  1933.                                 initialMouseY = clickY,
  1934.                                 initialOffset = sharedData.configMenu.scrollState.offset,
  1935.                                 trackH = r.trackH,
  1936.                                 maxOffset = r.maxOffset
  1937.                             }
  1938.                             sharedData.needsRedraw = false
  1939.                             break
  1940.                         elseif r.type == "config_scroll" then
  1941.                             local newOffset = sharedData.configMenu.scrollState.offset + r.direction
  1942.                             sharedData.configMenu.scrollState.offset = math.max(0, math.min(newOffset, r.maxOffset))
  1943.                             break
  1944.                         elseif r.type == "nav_next" then
  1945.                             sharedData.configMenu.page = sharedData.configMenu.page + 1
  1946.                             sharedData.configMenu.scrollState.offset = 0
  1947.                             break
  1948.                         elseif r.type == "nav_back" then
  1949.                             sharedData.configMenu.page = sharedData.configMenu.page - 1
  1950.                             sharedData.configMenu.scrollState.offset = 0
  1951.                             break
  1952.                         elseif r.type == "save" then
  1953.                             sharedData.config = sharedData.configMenu.tempConfig
  1954.                             saveConfig()
  1955.                             sharedData.isMenuOpen = false
  1956.                             reinitializeLayout()
  1957.                             break
  1958.                         elseif r.type == "quit" then
  1959.                             sharedData.programRunning = false
  1960.                             break
  1961.                         elseif r.type == "temp" then sharedData.configMenu.tempConfig.tempUnit = r.unit; break
  1962.                         elseif r.type == "border" then sharedData.configMenu.tempConfig.useColoredBorders = r.value; break
  1963.                         elseif r.type == "use_morered" then sharedData.configMenu.tempConfig.useMoreRed = r.value; break
  1964.                         elseif r.type == "use_alarm" then sharedData.configMenu.tempConfig.useIndustrialAlarm = r.value; break
  1965.                         elseif r.type == "set_side" then sharedData.configMenu.tempConfig[r.key] = r.value; break
  1966.                         elseif r.type == "set_color" then sharedData.configMenu.tempConfig[r.key] = r.value; break
  1967.                         elseif r.type == "enable_ignition" then sharedData.configMenu.tempConfig.enableFusionIgnition = r.value; break
  1968.                         elseif r.type == "enable_power_down" then sharedData.configMenu.tempConfig.enableFusionPowerDown = r.value; break
  1969.                         elseif r.type == "order_up" and r.index > 1 then local t=sharedData.configMenu.tempConfig.deviceOrder[r.index]; sharedData.configMenu.tempConfig.deviceOrder[r.index]=sharedData.configMenu.tempConfig.deviceOrder[r.index-1]; sharedData.configMenu.tempConfig.deviceOrder[r.index-1]=t; break
  1970.                         elseif r.type == "order_down" and r.index < #sharedData.configMenu.tempConfig.deviceOrder then local t=sharedData.configMenu.tempConfig.deviceOrder[r.index]; sharedData.configMenu.tempConfig.deviceOrder[r.index]=sharedData.configMenu.tempConfig.deviceOrder[r.index+1]; sharedData.configMenu.tempConfig.deviceOrder[r.index+1]=t; break
  1971.                         else
  1972.                             sharedData.needsRedraw = false
  1973.                         end
  1974.                     end
  1975.                 end
  1976.             end
  1977.         else -- Not in menu or keypad
  1978.             if event == "mouse_scroll" then
  1979.                  local scrollDir, scrollX, scrollY
  1980.                  local targetName
  1981.                  
  1982.                  if type(p1) == "string" then
  1983.                     targetName = p1; scrollDir = p2; scrollX = p3; scrollY = p4
  1984.                  else
  1985.                     targetName = "term"; scrollDir = p1; scrollX = p2; scrollY = p3
  1986.                  end
  1987.  
  1988.                  local region = nil
  1989.                  for _, quad in ipairs(sharedData.quadrantRegions[targetName] or {}) do
  1990.                     if scrollX >= quad.x1 and scrollX <= quad.x2 and scrollY >= quad.y1 and scrollY <= quad.y2 then
  1991.                         region = quad
  1992.                         break
  1993.                     end
  1994.                  end
  1995.  
  1996.                  if region and region.deviceType == "fissionReactor" then
  1997.                     local offset = sharedData.fissionScrollOffsets[region.deviceName] or 0
  1998.                     local newOffset = offset + scrollDir
  1999.                     local clampedOffset = math.max(0, math.min(newOffset, region.maxOffset or 0))
  2000.                     if clampedOffset ~= offset then
  2001.                         sharedData.fissionScrollOffsets[region.deviceName] = clampedOffset
  2002.                         sharedData.needsRedraw = true
  2003.                     end
  2004.                  end
  2005.             elseif event == "mouse_click" or event == "monitor_touch" then
  2006.                 local clickType, clickX, clickY
  2007.                 if event == "mouse_click" then clickType, clickX, clickY = "term", p2, p3
  2008.                 else clickType, clickX, clickY = p1, p2, p3 end
  2009.  
  2010.                 -- [FIX] Check BOTH the main list and the backup list (for double-buffer effect)
  2011.                 local listsToCheck = {sharedData.pageButtonRegions, sharedData.backupRegions}
  2012.                 local actionTaken = false
  2013.  
  2014.                 for _, currentList in ipairs(listsToCheck) do
  2015.                     if actionTaken then break end -- Don't click twice if found
  2016.                     if currentList then
  2017.                         for i = #currentList, 1, -1 do
  2018.                             local region = currentList[i]
  2019.                             local yStart = region.pageY or region.yStart
  2020.                             local yEnd = region.pageY or region.yEnd
  2021.                             local xStart = region.x1 or region.xStart
  2022.                             local xEnd = region.x2 or region.xEnd
  2023.  
  2024.                             if region and region.target == clickType and clickY >= yStart and clickY <= yEnd and clickX >= xStart and clickX <= xEnd then
  2025.                                
  2026.                                 if region.type == "config" then
  2027.                                     sharedData.isMenuOpen = true
  2028.                                     sharedData.configMenu.page = 1
  2029.                                     sharedData.configMenu.scrollState = { offset = 0, maxOffset = 0 }
  2030.                                     sharedData.configMenu.tempConfig = table.copy(sharedData.config)
  2031.                                     sharedData.needsRedraw = true
  2032.                                     actionTaken = true; break
  2033.                                 elseif region.type == "end" then sharedData.programRunning = false; actionTaken = true; break
  2034.                                 elseif region.type == "dump_mode" then
  2035.                                     local dev = peripheral.wrap(region.turbineName); if dev then pcall(dev.setDumpingMode, region.modeToSet) end; sharedData.needsRedraw = true; actionTaken = true; break
  2036.                                 elseif region.type == "global_dump_mode" then
  2037.                                     for _,dI in ipairs(sharedData.foundDevices) do if dI.type=="turbine" then pcall(dI.peripheral.setDumpingMode, region.modeToSet) end end; sharedData.needsRedraw = true; actionTaken = true; break
  2038.                                 elseif region.type == "fusion_ignite" then
  2039.                                     table.insert(sharedData.actionQueue, { type = "fusion_ignite" }); actionTaken = true; break
  2040.                                 elseif region.type == "fusion_fuel_toggle" then
  2041.                                     table.insert(sharedData.actionQueue, { type = "fusion_power_down", deviceName = region.deviceName, state = true }); actionTaken = true; break
  2042.                                 elseif region.type == "set_injection_rate" then
  2043.                                     table.insert(sharedData.actionQueue, { type = "fusion_injection", deviceName = region.deviceName, action = region.action }); actionTaken = true; break
  2044.                                 elseif region.type == "fission_scroll" then
  2045.                                     local offset = sharedData.fissionScrollOffsets[region.deviceName] or 0
  2046.                                     local newOffset = offset + region.direction
  2047.                                     sharedData.fissionScrollOffsets[region.deviceName] = math.max(0, math.min(newOffset, region.maxOffset))
  2048.                                     sharedData.needsRedraw = true
  2049.                                     actionTaken = true; break
  2050.                                 elseif region.type == "container_edit_mode" then
  2051.                                     local dev = peripheral.wrap(region.deviceName);
  2052.                                     if dev then
  2053.                                         if region.action == "inc" then pcall(dev.incrementContainerEditMode)
  2054.                                         elseif region.action == "dec" then pcall(dev.decrementContainerEditMode) end
  2055.                                     end; sharedData.needsRedraw = true; actionTaken = true; break
  2056.                                 elseif region.type == "sps_redstone_toggle" then
  2057.                                     if sharedData.config.useMoreRed then
  2058.                                         setBundledBit(sharedData.config.moreRedOutputSide, sharedData.config.spsRedstoneColor, not (bit.band(redstone.getBundledOutput(sharedData.config.moreRedOutputSide), colors[sharedData.config.spsRedstoneColor]) > 0))
  2059.                                     else
  2060.                                         local side = sharedData.config.spsRedstoneControlSide
  2061.                                         local currentState = redstone.getOutput(side)
  2062.                                         redstone.setOutput(side, not currentState)
  2063.                                     end
  2064.                                     sharedData.needsRedraw = true; actionTaken = true; break
  2065.                                 elseif region.type == "fission_control" then
  2066.                                     table.insert(sharedData.actionQueue, {type="fission_control", deviceName=region.deviceName, action=region.action})
  2067.                                     sharedData.needsRedraw = true
  2068.                                     actionTaken = true; break
  2069.                                 elseif region.type == "fission_open_keypad" then
  2070.                                     sharedData.pageButtonRegions = {}
  2071.                                     sharedData.keypad.isOpen = true
  2072.                                     sharedData.keypad.targetDevice = region.deviceName
  2073.                                     sharedData.keypad.targetWindow = region.target
  2074.                                     sharedData.keypad.inputValue = ""
  2075.                                     sharedData.keypad.message = ""
  2076.                                     for _, quad in ipairs(sharedData.quadrantRegions[region.target] or {}) do
  2077.                                         if quad.deviceName == region.deviceName then
  2078.                                             sharedData.keypad.qx = quad.x1
  2079.                                             sharedData.keypad.qy = quad.y1
  2080.                                             sharedData.keypad.qW = quad.x2 - quad.x1 + 1
  2081.                                             sharedData.keypad.qH = quad.y2 - quad.y1 + 1
  2082.                                             break
  2083.                                         end
  2084.                                     end
  2085.                                     actionTaken = true; break
  2086.                                 elseif region.type == "goto" and region.target == "term" then
  2087.                                     sharedData.isGoToMenuOpen = true
  2088.                                     sharedData.needsRedraw = true
  2089.                                     actionTaken = true; break
  2090.                                 elseif region.type == "goto_page" then
  2091.                                     sharedData.currentPageTerm = region.page
  2092.                                     sharedData.isGoToMenuOpen = false
  2093.                                     sharedData.needsRedraw = true
  2094.                                     actionTaken = true; break
  2095.                                 elseif region.type == "goto_close" then
  2096.                                     sharedData.isGoToMenuOpen = false
  2097.                                     sharedData.needsRedraw = true
  2098.                                     actionTaken = true; break
  2099.                                 else
  2100.                                     local isMonitor = (region.target ~= "term")
  2101.                                     local currentPage = isMonitor and sharedData.monitorStates[region.target].currentPage or sharedData.currentPageTerm
  2102.                                     local totalPages = isMonitor and sharedData.totalPagesMon or sharedData.totalPagesTerm
  2103.                                     local newPage = currentPage
  2104.                                     if region.type == "prev" then newPage = currentPage - 1; if newPage < 1 then newPage = totalPages end
  2105.                                     elseif region.type == "next" then newPage = currentPage + 1; if newPage > totalPages then newPage = 1 end
  2106.                                     elseif region.type == "page" then newPage = region.pageNum end
  2107.                                     if newPage ~= currentPage then
  2108.                                         if isMonitor then sharedData.monitorStates[region.target].currentPage = newPage
  2109.                                         else sharedData.currentPageTerm = newPage end
  2110.                                         sharedData.needsRedraw = true
  2111.                                     end
  2112.                                     actionTaken = true; break
  2113.                                 end
  2114.                             end
  2115.                         end
  2116.                     end
  2117.                 end
  2118.             end
  2119.         end
  2120.     end
  2121. end
  2122.  
  2123. local function scannerTask()
  2124.     while sharedData.programRunning do
  2125.         if not sharedData.isMenuOpen then
  2126.              pcall(scanAndApplyChanges)
  2127.         end
  2128.         for i = 1, math.ceil(scanForNewDevicesInterval / 0.1) do
  2129.             if not sharedData.programRunning then break end
  2130.             sleep(0.1)
  2131.         end
  2132.     end
  2133.     debugPrint("Scanner task stopped.")
  2134. end
  2135.  
  2136. -- =================================================================
  2137. -- Configuration and Main Loop
  2138. -- =================================================================
  2139. function saveConfig()
  2140.     local s = textutils.serialize(sharedData.config)
  2141.     local file, err = fs.open(configFile, "w")
  2142.     if file then
  2143.         file.write(s)
  2144.         file.close()
  2145.         debugPrint("Configuration saved.")
  2146.         return true
  2147.     else
  2148.         centeredPrint("ERROR: Could not save config file: " .. tostring(err))
  2149.         return false
  2150.     end
  2151. end
  2152.  
  2153. function scanAndApplyChanges()
  2154.     local peripherals = peripheral.getNames(); local allPeripheralNames = {}
  2155.     for _,pName in ipairs(peripherals) do allPeripheralNames[pName] = true end
  2156.     local changed = false
  2157.  
  2158.     for i = #sharedData.foundDevices, 1, -1 do
  2159.         local devInfo = sharedData.foundDevices[i]
  2160.         if not allPeripheralNames[devInfo.name] then
  2161.             debugPrint("-> Device removed: "..devInfo.type.." - "..devInfo.name)
  2162.             table.remove(sharedData.foundDevices, i)
  2163.             sharedData.deviceStats[devInfo.name] = nil
  2164.             sharedData.lastError[devInfo.name] = nil
  2165.             changed = true
  2166.         end
  2167.     end
  2168.  
  2169.     local currentDeviceNames = {}; for _, dev in ipairs(sharedData.foundDevices) do currentDeviceNames[dev.name] = true end
  2170.     for _, peripheralName in ipairs(peripherals) do
  2171.         if not currentDeviceNames[peripheralName] then
  2172.             local p = peripheral.wrap(peripheralName)
  2173.             if p and peripheral.getType(p) ~= "monitor" then
  2174.                 local deviceType, isValid = nil, false
  2175.                 if string.sub(peripheralName, 1, #turbineNamePrefix) == turbineNamePrefix then
  2176.                     deviceType = "turbine"; local s,r = pcall(p.isFormed); if (s and r) or skipIsFormedValidation then isValid = true end
  2177.                 elseif string.sub(peripheralName, 1, #fusionReactorNamePrefix) == fusionReactorNamePrefix then
  2178.                     deviceType = "fusionReactor"; if pcall(p.getWaterCapacity) then isValid = true end
  2179.                 elseif string.sub(peripheralName, 1, #fissionReactorNamePrefix) == fissionReactorNamePrefix then
  2180.                     deviceType = "fissionReactor"; if pcall(p.getFuelAssemblies) then isValid = true end
  2181.                 elseif string.sub(peripheralName, 1, #inductionMatrixNamePrefix) == inductionMatrixNamePrefix then
  2182.                     deviceType = "inductionMatrix"; if pcall(p.getMaxEnergy) then isValid = true end
  2183.                 elseif string.sub(peripheralName, 1, #boilerNamePrefix) == boilerNamePrefix then
  2184.                     deviceType = "boiler"; if pcall(p.getTemperature) then isValid = true end
  2185.                 elseif string.sub(peripheralName, 1, #thermalEvaporationNamePrefix) == thermalEvaporationNamePrefix then
  2186.                     deviceType = "thermalEvaporation"; if pcall(p.isFormed) then isValid = true end
  2187.                 elseif pcall(p.getContainerEditMode) and pcall(p.getStored) then
  2188.                     deviceType = "dynamicTank"
  2189.                     local s, r = pcall(p.isFormed)
  2190.                     if (s and r) or not s then
  2191.                         isValid = true
  2192.                     end
  2193.                 elseif pcall(p.getCoils) and pcall(p.getProcessRate) then
  2194.                     deviceType = "sps"
  2195.                     local s, r = pcall(p.isFormed)
  2196.                     if (s and r) or not s then
  2197.                         isValid = true
  2198.                     end
  2199.                 end
  2200.  
  2201.                 if isValid and deviceType then
  2202.                     debugPrint("-> New device: "..deviceType.." - "..peripheralName)
  2203.                     table.insert(sharedData.foundDevices, { name = peripheralName, peripheral = p, type = deviceType })
  2204.                     changed = true
  2205.                 end
  2206.             end
  2207.         end
  2208.     end
  2209.  
  2210.     if changed then
  2211.         debugPrint("Device list changed.")
  2212.         local oldTypes = {}; for _, devName in ipairs(sharedData.config.deviceOrder) do oldTypes[devName] = true end
  2213.         local newTypes = {}; local newTurbineCount = 0
  2214.         for _, dev in ipairs(sharedData.foundDevices) do newTypes[dev.type] = true; if dev.type == "turbine" then newTurbineCount = newTurbineCount + 1 end end
  2215.         if newTurbineCount > 1 then newTypes["turbineSummary"] = true end
  2216.  
  2217.         local typesChanged = false
  2218.         if countKeys(oldTypes) ~= countKeys(newTypes) then
  2219.             typesChanged = true
  2220.         else
  2221.             for k in pairs(oldTypes) do
  2222.                 if not newTypes[k] then
  2223.                     typesChanged = true
  2224.                     break
  2225.                 end
  2226.             end
  2227.             if not typesChanged then
  2228.                 for k in pairs(newTypes) do
  2229.                     if not oldTypes[k] then
  2230.                         typesChanged = true
  2231.                         break
  2232.                     end
  2233.                 end
  2234.             end
  2235.         end
  2236.        
  2237.         if typesChanged then
  2238.             debugPrint("Device TYPE list changed. Updating config.")
  2239.             local newOrder = {}; local seenInNew = {}
  2240.             for _, devName in ipairs(sharedData.config.deviceOrder) do if newTypes[devName] then table.insert(newOrder, devName); seenInNew[devName] = true end end
  2241.             for devName in pairs(newTypes) do if not seenInNew[devName] then table.insert(newOrder, devName) end end
  2242.             sharedData.config.deviceOrder = newOrder
  2243.             saveConfig()
  2244.         end
  2245.         reinitializeLayout()
  2246.     end
  2247. end
  2248.  
  2249. function loadConfig()
  2250.     if fs.exists(configFile) then
  2251.         local file, err_open = fs.open(configFile, "r")
  2252.         if not file then centeredPrint("Error opening config: "..tostring(err_open)); return false end
  2253.         local s = file.readAll(); file.close()
  2254.         local config, err_unserialize = textutils.unserialize(s)
  2255.         if config and type(config) == "table" and config.tempUnit and config.deviceOrder and config.useColoredBorders ~= nil then
  2256.             sharedData.config = config
  2257.             debugPrint("Configuration loaded successfully.")
  2258.             return true
  2259.         else
  2260.             debugPrint("Warning: Failed to load config. Using defaults. "..tostring(err_unserialize))
  2261.             return false
  2262.         end
  2263.     end
  2264.     debugPrint("No config file found. Will run initial setup.")
  2265.     return false
  2266. end
  2267.  
  2268. function drawConfigurationMenu()
  2269.     local tempConfig = sharedData.configMenu.tempConfig
  2270.     local configPage = sharedData.configMenu.page
  2271.     local scrollState = sharedData.configMenu.scrollState
  2272.    
  2273.     local allDeviceTypes = {}; local seenTypes = {}; for _, d in ipairs(sharedData.foundDevices) do if not seenTypes[d.type] then table.insert(allDeviceTypes, d.type); seenTypes[d.type] = true end end; local turbineCount = 0; for _, d in ipairs(sharedData.foundDevices) do if d.type == "turbine" then turbineCount = turbineCount + 1 end end; if turbineCount > 1 and not seenTypes["turbineSummary"] then table.insert(allDeviceTypes, "turbineSummary") end
  2274.     local validOrder = {}; local seen = {}; for _, name in ipairs(tempConfig.deviceOrder) do for _, vName in ipairs(allDeviceTypes) do if name == vName and not seen[name] then table.insert(validOrder, name); seen[name] = true end end end; for _, vName in ipairs(allDeviceTypes) do if not seen[vName] then table.insert(validOrder, vName) end end; tempConfig.deviceOrder = validOrder
  2275.  
  2276.     local allWindows = { term = term }
  2277.     for _, mon in ipairs(monitors) do
  2278.         allWindows[peripheral.getName(mon)] = mon
  2279.     end
  2280.  
  2281.     local regions = sharedData.pageButtonRegions
  2282.    
  2283.     for name, window in pairs(allWindows) do
  2284.         local w, h = window.getSize()
  2285.         local totalConfigPages = 6
  2286.         if configPage > totalConfigPages then sharedData.configMenu.page = totalConfigPages; configPage = totalConfigPages end
  2287.         window.setBackgroundColor(colors.black); window.clear()
  2288.         local headerText = "--- Mekanism Monitor: Configuration (Page " .. configPage .. "/" .. totalConfigPages .. ") ---"
  2289.         window.setCursorPos(math.floor((w - #headerText) / 2) + 1, 1); window.setTextColor(colors.yellow); window.write(headerText); window.setTextColor(colors.white)
  2290.        
  2291.         if configPage == 4 then
  2292.             local items = {}
  2293.             if tempConfig.useMoreRed then
  2294.                table.insert(items, {type="side_selector", title="Bundled Cable Output Side", key="moreRedOutputSide", enabled=true})
  2295.                table.insert(items, {type="color_selector", title="SPS Control Color", key="spsRedstoneColor", enabled=true})
  2296.                table.insert(items, {type="color_selector", title="Fusion Ignition Color", key="fusionIgnitionColor", enabled=tempConfig.enableFusionIgnition})
  2297.                table.insert(items, {type="color_selector", title="Fusion Power Down Color", key="fusionPowerDownColor", enabled=tempConfig.enableFusionPowerDown})
  2298.                if tempConfig.useIndustrialAlarm then
  2299.                    table.insert(items, {type="color_selector", title="Industrial Alarm Color", key="industrialAlarmColor", enabled=true})
  2300.                end
  2301.             else
  2302.                table.insert(items, {type="side_selector", title="SPS Control Side", key="spsRedstoneControlSide", enabled=true})
  2303.                table.insert(items, {type="side_selector", title="Fusion Ignition Side", key="fusionIgnitionSide", enabled=tempConfig.enableFusionIgnition})
  2304.                table.insert(items, {type="side_selector", title="Fusion Power Down Side", key="fusionPowerDownSide", enabled=tempConfig.enableFusionPowerDown})
  2305.                if tempConfig.useIndustrialAlarm then
  2306.                    table.insert(items, {type="side_selector", title="Industrial Alarm Side", key="industrialAlarmSide", enabled=true})
  2307.                end
  2308.             end
  2309.             local contentHeight = 0
  2310.             for _, item in ipairs(items) do contentHeight = contentHeight + (item.type == "side_selector" and 3 or 4) end
  2311.             local viewHeight = h - 5
  2312.             scrollState.maxOffset = math.max(0, contentHeight - viewHeight)
  2313.             local clampedOffset = math.max(0, math.min(scrollState.offset, scrollState.maxOffset))
  2314.             local currentY = 3 - clampedOffset
  2315.             for _, item in ipairs(items) do
  2316.                 if item.type == "side_selector" then
  2317.                     if currentY > 1 and currentY < h - 2 then
  2318.                         local sides={"top","bottom","left","right","front","back"}; local sideButtonsWidth=0
  2319.                         for _,side in ipairs(sides) do sideButtonsWidth=sideButtonsWidth+#(" "..side.." ")+1 end; sideButtonsWidth=sideButtonsWidth-1
  2320.                         local sideStartX=math.floor((w-sideButtonsWidth-1)/2)+1
  2321.                         window.setBackgroundColor(colors.black); window.setCursorPos(1, currentY); window.write(string.rep(" ", w))
  2322.                         window.setCursorPos(math.floor((w-#item.title)/2)+1, currentY); window.setTextColor(item.enabled and colors.white or colors.gray); window.write(item.title)
  2323.                         if currentY + 1 < h - 2 then
  2324.                             local currentX = sideStartX
  2325.                             for _, side in ipairs(sides) do
  2326.                                 local btnText=" "..side.." "; local isActive=(tempConfig[item.key]==side)
  2327.                                 window.setCursorPos(currentX,currentY+1);
  2328.                                 local bgColor = colors.darkGray
  2329.                                 if item.enabled then bgColor = colors.gray end
  2330.                                 if isActive and item.enabled then bgColor = colors.green end
  2331.                                 window.setBackgroundColor(bgColor)
  2332.                                 window.setTextColor(colors.black); window.write(btnText)
  2333.                                 if item.enabled then table.insert(regions,{type="set_side",key=item.key,value=side,x1=currentX,y=currentY+1,x2=currentX+#btnText-1,target=name}) end
  2334.                                 currentX=currentX+#btnText+1
  2335.                             end
  2336.                         end
  2337.                     end
  2338.                     currentY = currentY + 3
  2339.                 elseif item.type == "color_selector" then
  2340.                     if currentY > 1 and currentY < h-2 then
  2341.                         local colorNames={"white","orange","magenta","lightBlue","yellow","lime","pink","gray","lightGray","cyan","purple","blue","brown","green","red","black"}
  2342.                         window.setBackgroundColor(colors.black); window.setCursorPos(1, currentY); window.write(string.rep(" ", w))
  2343.                         window.setCursorPos(math.floor((w-#item.title)/2)+1, currentY); window.setTextColor(item.enabled and colors.white or colors.gray); window.write(item.title)
  2344.                         local row1,row2={}, {}; for i=1,#colorNames do if i<=8 then table.insert(row1,colorNames[i]) else table.insert(row2,colorNames[i]) end end
  2345.                         for r,row in ipairs({row1,row2}) do
  2346.                             if currentY+r < h-2 then
  2347.                                 local rowWidth=0; for _, c in ipairs(row)do rowWidth=rowWidth+4 end; rowWidth=rowWidth-1
  2348.                                 local currentX=math.floor((w-rowWidth-1)/2)+1
  2349.                                 for _, cName in ipairs(row)do
  2350.                                     window.setCursorPos(currentX,currentY+r);
  2351.                                     if not item.enabled then
  2352.                                         window.setBackgroundColor(colors.darkGray)
  2353.                                         window.write("   ")
  2354.                                     else
  2355.                                         window.setBackgroundColor(colors[cName])
  2356.                                         if tempConfig[item.key]==cName then window.setTextColor(cName=="black" and colors.white or colors.black); window.write("[X]") else window.write("   ") end
  2357.                                         table.insert(regions,{type="set_color",key=item.key,value=cName,x1=currentX,y=currentY+r,x2=currentX+2,target=name})
  2358.                                     end
  2359.                                     currentX=currentX+4
  2360.                                 end
  2361.                             end
  2362.                         end
  2363.                     end
  2364.                     currentY = currentY + 4
  2365.                 end
  2366.             end
  2367.             if scrollState.maxOffset > 0 then
  2368.                 local scrollX = w - 1
  2369.                 window.setCursorPos(scrollX, 2); window.setBackgroundColor(colors.black); window.setTextColor(colors.white); window.write("^")
  2370.                 table.insert(regions, {type="config_scroll", direction=-1, maxOffset=scrollState.maxOffset, x1=scrollX, x2=scrollX, y=2, target=name})
  2371.                 window.setCursorPos(scrollX, h-2); window.setBackgroundColor(colors.black); window.setTextColor(colors.white); window.write("v")
  2372.                 table.insert(regions, {type="config_scroll", direction=1, maxOffset=scrollState.maxOffset, x1=scrollX, x2=scrollX, y=h-2, target=name})
  2373.                 local trackY=3; local trackHeight=h-5;
  2374.                 local handleSize=math.max(1,math.floor(trackHeight*(viewHeight/contentHeight)))
  2375.                 local handlePos
  2376.                 if scrollState.maxOffset > 0 then
  2377.                     handlePos=trackY+math.floor((trackHeight-handleSize)*(clampedOffset/scrollState.maxOffset))
  2378.                 else
  2379.                     handlePos=trackY
  2380.                 end
  2381.                 for i=0,trackHeight-1 do
  2382.                     window.setCursorPos(scrollX,trackY+i);
  2383.                     if trackY+i>=handlePos and trackY+i<handlePos+handleSize then window.setBackgroundColor(colors.lightGray)
  2384.                     else window.setBackgroundColor(colors.gray) end
  2385.                     window.write(" ")
  2386.                 end
  2387.                 table.insert(regions, {type="config_scroll_handle",x1=scrollX,x2=scrollX,yStart=handlePos,yEnd=handlePos+handleSize-1,trackY=trackY,trackH=trackHeight,maxOffset=scrollState.maxOffset,target=name})
  2388.             end
  2389.         elseif configPage == 1 then
  2390.             local tempTitleY = 5; local borderTitleY = 9
  2391.             local tempUnitTitle = "Temperature Unit"
  2392.             window.setCursorPos(math.floor((w - #tempUnitTitle) / 2) + 1, tempTitleY); window.write(tempUnitTitle)
  2393.             local tempOrder = { "K", "C", "F", "R", "STP" }; local tempButtonsWidth = 0
  2394.             for _, unitKey in ipairs(tempOrder) do tempButtonsWidth = tempButtonsWidth + #(" " .. unitKey .. " ") + 1 end
  2395.             tempButtonsWidth = tempButtonsWidth - 1; local tempStartX = math.floor((w - tempButtonsWidth) / 2) + 1; local currentX = tempStartX
  2396.             for _, unitKey in ipairs(tempOrder) do
  2397.                 local btnText = " " .. unitKey .. " "; local isActive = (tempConfig.tempUnit == unitKey)
  2398.                 window.setCursorPos(currentX, tempTitleY + 1); window.setBackgroundColor(isActive and colors.green or colors.gray); window.setTextColor(colors.black); window.write(btnText)
  2399.                 table.insert(regions, { type = "temp", unit = unitKey, x1 = currentX, y = tempTitleY + 1, x2 = currentX + #btnText - 1, target = name })
  2400.                 currentX = currentX + #btnText + 1
  2401.             end
  2402.             local bordersTitle = "UI Borders"
  2403.             window.setBackgroundColor(colors.black)
  2404.             window.setTextColor(colors.white)
  2405.             window.setCursorPos(math.floor((w - #bordersTitle) / 2) + 1, borderTitleY); window.write(bordersTitle)
  2406.             local coloredBtnText = " Colored "; local grayBtnText = " Gray "; local bordersButtonsWidth = #coloredBtnText + #grayBtnText + 1
  2407.             local borderStartX = math.floor((w - bordersButtonsWidth) / 2) + 1; currentX = borderStartX
  2408.             window.setCursorPos(currentX, borderTitleY + 1); window.setBackgroundColor(tempConfig.useColoredBorders and colors.green or colors.gray); window.setTextColor(colors.black); window.write(coloredBtnText)
  2409.             table.insert(regions, { type = "border", value = true, x1 = currentX, y = borderTitleY + 1, x2 = currentX + #coloredBtnText - 1, target = name })
  2410.             currentX = currentX + #coloredBtnText + 1
  2411.             window.setCursorPos(currentX, borderTitleY + 1); window.setBackgroundColor(not tempConfig.useColoredBorders and colors.green or colors.gray); window.setTextColor(colors.black); window.write(grayBtnText)
  2412.             table.insert(regions, { type = "border", value = false, x1 = currentX, y = borderTitleY + 1, x2 = currentX + #grayBtnText - 1, target = name })
  2413.         elseif configPage == 2 then
  2414.             local moreRedY = 7; local title = "Redstone Control Method"
  2415.             window.setCursorPos(math.floor((w-#title)/2)+1, moreRedY-1); window.write(title)
  2416.             local yesBtn = " MoreRed (Bundled) "; local noBtn = " Standard (Per Side) "; local ynWidth = #yesBtn + #noBtn + 1
  2417.             local ynStartX = math.floor((w - ynWidth) / 2) + 1; local currentX = ynStartX
  2418.             window.setCursorPos(currentX, moreRedY + 1); window.setBackgroundColor(tempConfig.useMoreRed and colors.green or colors.gray); window.setTextColor(colors.black); window.write(yesBtn)
  2419.             table.insert(regions, { type = "use_morered", value = true, x1 = currentX, y = moreRedY + 1, x2 = currentX + #yesBtn - 1, target = name })
  2420.             currentX = currentX + #yesBtn + 1
  2421.             window.setCursorPos(currentX, moreRedY + 1); window.setBackgroundColor(not tempConfig.useMoreRed and colors.green or colors.gray); window.setTextColor(colors.black); window.write(noBtn)
  2422.             table.insert(regions, { type = "use_morered", value = false, x1 = currentX, y = moreRedY + 1, x2 = currentX + #noBtn - 1, target = name })
  2423.         elseif configPage == 3 then
  2424.             local alarmTypeY = 7; local title = "SCRAM Alert Method"
  2425.             window.setCursorPos(math.floor((w-#title)/2)+1, alarmTypeY-1); window.write(title)
  2426.             local speakerBtn = " CC Speakers "; local alarmBtn = " Industrial Alarm "; local btnWidth = #speakerBtn + #alarmBtn + 1
  2427.             local startX = math.floor((w - btnWidth) / 2) + 1; local currentX = startX
  2428.             window.setCursorPos(currentX, alarmTypeY + 1); window.setBackgroundColor(not tempConfig.useIndustrialAlarm and colors.green or colors.gray); window.setTextColor(colors.black); window.write(speakerBtn)
  2429.             table.insert(regions, { type = "use_alarm", value = false, x1 = currentX, y = alarmTypeY + 1, x2 = currentX + #speakerBtn - 1, target = name })
  2430.             currentX = currentX + #speakerBtn + 1
  2431.             window.setCursorPos(currentX, alarmTypeY + 1); window.setBackgroundColor(tempConfig.useIndustrialAlarm and colors.green or colors.gray); window.setTextColor(colors.black); window.write(alarmBtn)
  2432.             table.insert(regions, { type = "use_alarm", value = true, x1 = currentX, y = alarmTypeY + 1, x2 = currentX + #alarmBtn - 1, target = name })
  2433.         elseif configPage == 5 then
  2434.             local ignitionEnableY = 5; local powerDownEnableY = 10; local title = "Fusion Reactor Features"
  2435.             window.setCursorPos(math.floor((w-#title)/2)+1, 3); window.write(title)
  2436.             local enabledBtnText = " Enabled "; local disabledBtnText = " Disabled "; local enableButtonsWidth = #enabledBtnText + #disabledBtnText + 1
  2437.             local enableStartX = math.floor((w - enableButtonsWidth) / 2) + 1
  2438.             local ignitionEnableTitle = "Ignition Button"
  2439.             window.setCursorPos(math.floor((w-#ignitionEnableTitle)/2)+1, ignitionEnableY); window.write(ignitionEnableTitle)
  2440.             local currentX = enableStartX
  2441.             window.setCursorPos(currentX, ignitionEnableY + 1); window.setBackgroundColor(tempConfig.enableFusionIgnition and colors.green or colors.gray); window.setTextColor(colors.black); window.write(enabledBtnText)
  2442.             table.insert(regions, { type = "enable_ignition", value = true, x1 = currentX, y = ignitionEnableY + 1, x2 = currentX + #enabledBtnText - 1, target = name })
  2443.             currentX = currentX + #enabledBtnText + 1
  2444.             window.setCursorPos(currentX, ignitionEnableY + 1); window.setBackgroundColor(not tempConfig.enableFusionIgnition and colors.green or colors.gray); window.setTextColor(colors.black); window.write(disabledBtnText)
  2445.             table.insert(regions, { type = "enable_ignition", value = false, x1 = currentX, y = ignitionEnableY + 1, x2 = currentX + #disabledBtnText - 1, target = name })
  2446.            
  2447.             local powerDownEnableTitle = "Power Down Button"
  2448.             window.setBackgroundColor(colors.black)
  2449.             window.setTextColor(colors.white)
  2450.             window.setCursorPos(math.floor((w-#powerDownEnableTitle)/2)+1, powerDownEnableY); window.write(powerDownEnableTitle)
  2451.             currentX = enableStartX
  2452.             window.setCursorPos(currentX, powerDownEnableY + 1); window.setBackgroundColor(tempConfig.enableFusionPowerDown and colors.green or colors.gray); window.setTextColor(colors.black); window.write(enabledBtnText)
  2453.             table.insert(regions, { type = "enable_power_down", value = true, x1 = currentX, y = powerDownEnableY + 1, x2 = currentX + #enabledBtnText - 1, target = name })
  2454.             currentX = currentX + #enabledBtnText + 1
  2455.             window.setCursorPos(currentX, powerDownEnableY + 1); window.setBackgroundColor(not tempConfig.enableFusionPowerDown and colors.green or colors.gray); window.setTextColor(colors.black); window.write(disabledBtnText)
  2456.             table.insert(regions, { type = "enable_power_down", value = false, x1 = currentX, y = powerDownEnableY + 1, x2 = currentX + #disabledBtnText - 1, target = name })
  2457.         elseif configPage == 6 then
  2458.             local pageY = (h > 22) and math.floor((h - #tempConfig.deviceOrder) / 2) or 3
  2459.             local maxNameLen = 0
  2460.             for _, dName in ipairs(tempConfig.deviceOrder) do if #dName > maxNameLen then maxNameLen = #dName end end
  2461.             local orderBlockWidth = 3 + 1 + maxNameLen + 1 + 3; local orderStartX = math.floor((w - orderBlockWidth) / 2) + 1
  2462.             local orderTitle = "Device Display Order"; window.setCursorPos(orderStartX + math.floor((orderBlockWidth - #orderTitle)/2), pageY - 2); window.setTextColor(colors.white); window.write(orderTitle)
  2463.             local upArrowX = orderStartX; local nameFieldWidth = maxNameLen; local downArrowX = orderStartX + 4 + nameFieldWidth + 1
  2464.             for i, deviceName in ipairs(tempConfig.deviceOrder) do
  2465.                 local yPos = pageY + i - 1; if yPos >= h - 2 then break end; local nameStartX = orderStartX + 4 + math.floor((nameFieldWidth - #deviceName) / 2)
  2466.                 window.setCursorPos(upArrowX, yPos); window.setTextColor(i > 1 and colors.white or colors.gray); window.write("[^]"); table.insert(regions, { type = "order_up", index = i, x1 = upArrowX, y = yPos, x2 = upArrowX + 2, target = name })
  2467.                 window.setCursorPos(nameStartX, yPos); window.setTextColor(colors.yellow); window.write(deviceName)
  2468.                 window.setCursorPos(downArrowX, yPos); window.setTextColor(i < #tempConfig.deviceOrder and colors.white or colors.gray); window.write("[v]"); table.insert(regions, { type = "order_down", index = i, x1 = downArrowX, y = yPos, x2 = downArrowX + 2, target = name })
  2469.             end
  2470.         end
  2471.         local saveText = " Save & Close "; local quitText = " Discard "
  2472.         local backText = " < Back "; local nextText = " Next > "
  2473.         window.setBackgroundColor(colors.black)
  2474.         window.setCursorPos(2, h);
  2475.         window.setBackgroundColor(colors.green); window.setTextColor(colors.black); window.write(saveText)
  2476.         table.insert(regions, { type = "save", x1 = 2, y = h, x2 = 2 + #saveText - 1, target = name })
  2477.         local currentNavX = 2 + #saveText + 2
  2478.         if configPage > 1 then
  2479.             window.setCursorPos(currentNavX, h)
  2480.             window.setBackgroundColor(colors.blue); window.setTextColor(colors.white); window.write(backText)
  2481.             table.insert(regions, { type = "nav_back", x1 = currentNavX, y = h, x2 = currentNavX + #backText - 1, target = name })
  2482.         end
  2483.         local quitX = w - #quitText
  2484.         window.setCursorPos(quitX, h);
  2485.         window.setBackgroundColor(colors.red); window.setTextColor(colors.black); window.write(quitText)
  2486.         table.insert(regions, { type = "quit", x1 = quitX, y = h, x2 = w, target = name })
  2487.         if configPage < totalConfigPages then
  2488.             local nextX = quitX - #nextText - 2
  2489.             window.setCursorPos(nextX, h)
  2490.             window.setBackgroundColor(colors.blue); window.setTextColor(colors.white); window.write(nextText)
  2491.             table.insert(regions, { type = "nav_next", x1 = nextX, y = h, x2 = nextX + #nextText - 1, target = name })
  2492.         end
  2493.         window.setBackgroundColor(colors.black); window.setTextColor(colors.white)
  2494.     end
  2495. end
  2496.  
  2497.  
  2498. function main()
  2499.     term.clear()
  2500.     for _, name in ipairs(peripheral.getNames()) do
  2501.         if peripheral.getType(name) == "monitor" then
  2502.             table.insert(monitors, peripheral.wrap(name))
  2503.             sharedData.monitorStates[name] = { currentPage = 1 }
  2504.         elseif peripheral.getType(name) == "speaker" then
  2505.             table.insert(sharedData.speakers, peripheral.wrap(name))
  2506.         end
  2507.     end
  2508.  
  2509.     if #monitors == 0 then
  2510.         centeredPrint("Exiting: No monitors found.", 2)
  2511.         return
  2512.     end
  2513.  
  2514.     setupSirenAudio()
  2515.     loadConfig()
  2516.     sharedData.configMenu.tempConfig = table.copy(sharedData.config)
  2517.  
  2518.     if sharedData.config.useMoreRed then
  2519.         local side = sharedData.config.moreRedOutputSide
  2520.         setBundledBit(side, sharedData.config.spsRedstoneColor, true)
  2521.         setBundledBit(side, sharedData.config.industrialAlarmColor, false)
  2522.     else
  2523.         redstone.setOutput(sharedData.config.spsRedstoneControlSide, true)
  2524.         redstone.setOutput(sharedData.config.industrialAlarmSide, false)
  2525.     end
  2526.  
  2527.  
  2528.     if not fs.exists(configFile) then
  2529.         term.clear(); drawTerminalHeader()
  2530.         centeredPrint("No config file found. Scanning for devices...", 3)
  2531.         scanAndApplyChanges()
  2532.        
  2533.         local deviceTypesFound = {}
  2534.         for _, dev in ipairs(sharedData.foundDevices) do
  2535.             deviceTypesFound[dev.type] = true
  2536.         end
  2537.  
  2538.         local defaultOrder = {"sps", "fusionReactor", "fissionReactor", "inductionMatrix", "dynamicTank", "thermalEvaporation", "boiler", "turbine"}
  2539.         local finalOrder = {}
  2540.  
  2541.         local turbineCount = 0; for _, dev in ipairs(sharedData.foundDevices) do if dev.type == "turbine" then turbineCount = turbineCount + 1 end end
  2542.         if turbineCount > 1 and deviceTypesFound.turbine then
  2543.             table.insert(finalOrder, "turbineSummary")
  2544.         end
  2545.        
  2546.         for _, typeName in ipairs(defaultOrder) do
  2547.             if deviceTypesFound[typeName] then
  2548.                 table.insert(finalOrder, typeName)
  2549.             end
  2550.         end
  2551.  
  2552.         sharedData.config.deviceOrder = finalOrder
  2553.  
  2554.         centeredPrint("Scan complete. Found " .. #sharedData.foundDevices .. " devices.", 5)
  2555.         centeredWrapPrint("Launching configuration menu...", 6)
  2556.         sleep(2)
  2557.        
  2558.         sharedData.isMenuOpen = true
  2559.         sharedData.configMenu.tempConfig = table.copy(sharedData.config)
  2560.         saveConfig()
  2561.     end
  2562.  
  2563.     term.clear(); drawTerminalHeader()
  2564.     centeredPrint("Starting monitoring session...", 3)
  2565.     if not debugMode then
  2566.           centeredWrapPrint("Debug mode is off. Press Ctrl+T or use END button to stop.", 5)
  2567.     end
  2568.     sleep(1)
  2569.  
  2570.     sharedData.monitorRunning = true
  2571.     reinitializeLayout()
  2572.    
  2573.     parallel.waitForAll(displayTask, fetchTask, eventListenerTask, scannerTask, actionQueueProcessorTask, sirenTask)
  2574.    
  2575.     for _, mon in ipairs(monitors) do
  2576.         pcall(function() mon.setBackgroundColor(colors.black); mon.setTextColor(colors.white); mon.clear() end)
  2577.     end
  2578.     term.setBackgroundColor(colors.black); term.setTextColor(colors.white); term.clear(); term.setCursorPos(1,1);
  2579.     centeredPrint("Mekanism Monitor stopped.")
  2580.     print()
  2581. end
  2582.  
  2583. main()
  2584.  
Tags: Mekanism/CC
Advertisement
Add Comment
Please, Sign In to add comment