CCCPaste

6d0b3e2ccb316

local peripheralNames = {
  reactor = "draconic_reactor_1",
  inGate  = "flow_gate_3",
  outGate = "flow_gate_2",
  monitor = "right"
}

local autoStop = 0.1
local setfield  = 0.05
local setTemp   = 8000.4
local fieldCoe  = {0.08, 0.03, 0.09}
local tempCoe   = {0.05, 0.09, 0.1}
local energyCoe = {0.045, 0.11, 0.1}
local intgTime  = 15

local TICK = 0.05
local FIELD_INTERVAL = math.floor(0.3 / TICK + 0.5)
local TEMP_INTERVAL  = math.floor(0.8 / TICK + 0.5)

-- Safety thresholds
local MAX_TEMP = 8300
local MIN_FIELD_RATE = 0.02 -- 2%

local reactor, inGate, outGate, monitor = nil, nil, nil, nil
local curIn, curOut = 0, 0
local avgOut = 0

local fieldDeltaList = {}
local tempDeltaList = {}
local energyDeltaList = {}

local state = "idle"  -- idle, check, attach, charge, active, error
local buttonState = "stop" -- start/stop text toggling on GUI
local running = false -- whether PID control is active / we have 'started' reactor control

-- PID internal trackers
local field_it, field_eI, field_lastDelta = 0, 0.0, 0.0
local temp_it, temp_eI, temp_lastDelta = 0, 0.0, 0.0
local energy_it, energy_eI, energy_lastDelta = 0, 0.0, 0.0

-- counters for tick-based updates
local tickCounter = 0
local fieldCounter = 0
local tempCounter = 0

-- monitor layout (3x2 proportion)
local mon_w, mon_h = nil, nil
local seg_w, seg_h = nil, nil

-------------------------------------------------------------------------------
-- Helper: safe call - check method exists before calling
-------------------------------------------------------------------------------
local function hasMethod(obj, name)
  if not obj then return false end
  return type(obj[name]) == "function"
end

local function safeCall(obj, name, ...)
  if not obj then return nil, "nil object" end
  local f = obj[name]
  if type(f) ~= "function" then
    return nil, "method_missing:" .. tostring(name)
  end
  return pcall(f, obj, ...)
end

-------------------------------------------------------------------------------
-- Peripheral detection helpers
-------------------------------------------------------------------------------
local function tryWrap(name)
  if name and name ~= "" then
    local ok, res = pcall(peripheral.wrap, name)
    if ok and res then return res end
  end
  return nil
end

local function findReactorPeripheral()
  for n, t in pairs(peripheral.getNames()) do
    local p = peripheral.wrap(t)
    if p and type(p.getReactorInfo) == "function" then
      return t, p
    end
  end
  return nil, nil
end

local function findGatePeripheral(exclude)
  for n, t in pairs(peripheral.getNames()) do
    if t ~= exclude then
      local p = peripheral.wrap(t)
      if p then
        -- attempt to find a method that looks like flow control
        if (type(p.setFlow) == "function")
           or (type(p.setFlowOverride) == "function")
           or (type(p.setFlowRate) == "function")
           or (type(p.setOverrideEnabled) == "function") then
          return t, p
        end
      end
    end
  end
  return nil, nil
end

local function findMonitorPeripheral()
  for n, t in pairs(peripheral.getNames()) do
    local p = peripheral.wrap(t)
    if p and type(p.clear) == "function" and type(p.setCursorPos) == "function" then
      return t, p
    end
  end
  return nil, nil
end

-------------------------------------------------------------------------------
-- Peripheral assignment / self-check
-------------------------------------------------------------------------------
local function detectPeripherals()
  -- attempt to wrap via explicit names first
  reactor = tryWrap(peripheralNames.reactor)
  inGate  = tryWrap(peripheralNames.inGate)
  outGate = tryWrap(peripheralNames.outGate)
  monitor = tryWrap(peripheralNames.monitor)

  -- auto-detect any that are still nil
  if not reactor then
    local name, p = findReactorPeripheral()
    if p then
      reactor = p
      peripheralNames.reactor = name
    end
  end
  if not inGate then
    local name, p = findGatePeripheral(peripheralNames.outGate)
    if p then
      inGate = p
      peripheralNames.inGate = name
    end
  end
  if not outGate then
    local name, p = findGatePeripheral(peripheralNames.inGate)
    if p then
      outGate = p
      peripheralNames.outGate = name
    end
  end
  if not monitor then
    local name, p = findMonitorPeripheral()
    if p then
      monitor = p
      peripheralNames.monitor = name
    end
  end
end

local function checkAPIs()
  -- return true if basic APIs exist
  if not reactor then return false, "no reactor" end
  if not hasMethod(reactor, "getReactorInfo") then return false, "reactor.getReactorInfo missing" end
  -- reactor methods may vary, but check some common ones
  if not (hasMethod(reactor, "activateReactor") and hasMethod(reactor, "stopReactor") and hasMethod(reactor, "chargeReactor")) then
    -- still allow if some present: require at least getReactorInfo
    -- but prefer to have activate/stop/charge
  end

  if not inGate or not outGate then return false, "missing gates" end
  -- check flow methods on gates
  local inOk = hasMethod(inGate, "setFlow") or hasMethod(inGate, "setFlowOverride") or hasMethod(inGate, "setFlowRate") or hasMethod(inGate, "setOverrideEnabled")
  local outOk = hasMethod(outGate, "setFlow") or hasMethod(outGate, "setFlowOverride") or hasMethod(outGate, "setFlowRate") or hasMethod(outGate, "setOverrideEnabled")
  if not (inOk and outOk) then return false, "gate methods missing" end

  if monitor then
    local monOk = hasMethod(monitor, "clear") and hasMethod(monitor, "setCursorPos") and hasMethod(monitor, "write")
    if not monOk then return false, "monitor methods missing" end
  end

  return true, "ok"
end

-------------------------------------------------------------------------------
-- Flow control wrappers (tolerant to different gate APIs)
-------------------------------------------------------------------------------
local function setIn(value)
  if not inGate then return false end
  if value < 0 then value = 0 end
  -- clamp to a large max
  if value > 64000000 then value = 64000000 end
  curIn = value
  -- try several possible methods
  if hasMethod(inGate, "setFlowOverride") then
    pcall(inGate.setFlowOverride, inGate, value)
    return true
  elseif hasMethod(inGate, "setFlow") then
    pcall(inGate.setFlow, inGate, value)
    return true
  elseif hasMethod(inGate, "setFlowRate") then
    pcall(inGate.setFlowRate, inGate, value)
    return true
  elseif hasMethod(inGate, "setOverrideEnabled") then
    -- set override, then set flow maybe via setFlow or similar
    pcall(inGate.setOverrideEnabled, inGate, true)
    if hasMethod(inGate, "setFlow") then pcall(inGate.setFlow, inGate, value) end
    return true
  else
    return false
  end
end

local function setOut(value)
  if not outGate then return false end
  if value < 0 then value = 0 end
  if value > 64000000 then value = 64000000 end
  curOut = value
  if hasMethod(outGate, "setFlowOverride") then
    pcall(outGate.setFlowOverride, outGate, value)
    return true
  elseif hasMethod(outGate, "setFlow") then
    pcall(outGate.setFlow, outGate, value)
    return true
  elseif hasMethod(outGate, "setFlowRate") then
    pcall(outGate.setFlowRate, outGate, value)
    return true
  elseif hasMethod(outGate, "setOverrideEnabled") then
    pcall(outGate.setOverrideEnabled, outGate, true)
    if hasMethod(outGate, "setFlow") then pcall(outGate.setFlow, outGate, value) end
    return true
  else
    return false
  end
end

-------------------------------------------------------------------------------
-- Reactor info helpers (wrap original names)
-------------------------------------------------------------------------------
local function getInfo()
  if not reactor then return nil end
  local ok, info = pcall(reactor.getReactorInfo, reactor)
  if ok and type(info) == "table" then return info end
  return nil
end

local function status()
  local info = getInfo()
  if not info then return nil end
  return info.status
end

local function Temp()
  local info = getInfo()
  if not info then return 0 end
  return info.temperature or 0
end

local function Field()
  local info = getInfo()
  if not info then return 0 end
  return info.fieldStrength or 0
end

local function maxField()
  local info = getInfo()
  if not info then return 1 end
  return info.maxFieldStrength or 1
end

local function FieldRate()
  local info = getInfo()
  if not info then return 0 end
  if info.maxFieldStrength and info.maxFieldStrength > 0 then
    return (info.fieldStrength or 0) / info.maxFieldStrength
  end
  return 0
end

local function FieldDrain()
  local info = getInfo()
  if not info then return 0 end
  return info.fieldDrainRate or 0
end

local function Energy()
  local info = getInfo()
  if not info then return 0 end
  return info.energySaturation or 0
end

local function EnergyRate()
  local info = getInfo()
  if not info then return 0 end
  if info.maxEnergySaturation and info.maxEnergySaturation > 0 then
    return (info.energySaturation or 0) / info.maxEnergySaturation
  end
  return 0
end

local function FuelRate()
  local info = getInfo()
  if not info then return 0 end
  if info.maxFuelConversion and info.maxFuelConversion > 0 then
    return (info.fuelConversion or 0) / info.maxFuelConversion
  end
  return 0
end

local function eV()
  local info = getInfo()
  if not info then return 0 end
  return info.generationRate or 0
end

local function startReactor()
  if not reactor then return false end
  if hasMethod(reactor, "activateReactor") then
    pcall(reactor.activateReactor, reactor)
    return true
  end
  return false
end
local function stopReactor()
  if not reactor then return false end
  if hasMethod(reactor, "stopReactor") then
    pcall(reactor.stopReactor, reactor)
    return true
  end
  return false
end
local function chargeReactor()
  if not reactor then return false end
  if hasMethod(reactor, "chargeReactor") then
    pcall(reactor.chargeReactor, reactor)
    return true
  end
  return false
end

-------------------------------------------------------------------------------
-- Initialize reactor for activation (charging, override enabling)
-------------------------------------------------------------------------------
local function initializeReactor()
  if inGate and hasMethod(inGate, "setOverrideEnabled") then
    pcall(inGate.setOverrideEnabled, inGate, true)
  end
  if outGate and hasMethod(outGate, "setOverrideEnabled") then
    pcall(outGate.setOverrideEnabled, outGate, true)
  end
  chargeReactor()
  setOut(0)
  setIn(64000000)
  -- try to activate if thresholds satisfied
  for i = 1, 200 do -- timeout loop (200*0.05 = 10s)
    local t = Temp()
    local f = Field()
    local e = Energy()
    if t >= 2000 and f >= 0.49 and e >= 0.49 then
      startReactor()
      if status() == "running" then return true end
    end
    os.sleep(TICK)
  end
  return false
end

-------------------------------------------------------------------------------
-- PID update functions (called at their respective intervals)
-------------------------------------------------------------------------------
local function sum(tb)
  local s = 0.0
  for _, v in ipairs(tb) do s = s + v end
  return s
end

local function len(tb)
  return #tb
end

local function updateFieldPID()
  -- fieldDelta relative to maxField
  local fieldDelta = maxField() * setfield - Field()
  local eP = fieldDelta * fieldCoe[1]
  -- only integrate if eP + drain >= 0 (mimic original logic)
  if eP + FieldDrain() >= 0 then
    table.insert(fieldDeltaList, fieldDelta)
    if field_it >= 3 and #fieldDeltaList >= 2 then
      field_eI = sum(fieldDeltaList) * fieldCoe[2]
      local last = fieldDeltaList[#fieldDeltaList - 1] or fieldDeltaList[#fieldDeltaList]
      local eD = (fieldDelta - last) * fieldCoe[3]
      local eT = eP + field_eI + eD
      setIn(eT + FieldDrain())
      field_lastDelta = fieldDelta
    else
      -- accumulate until integral starts
      if field_it <= intgTime then field_it = field_it + 1 end
      -- still compute proportional-only
      local eT = eP + field_eI
      setIn(eT + FieldDrain())
      field_lastDelta = fieldDelta
      table.insert(fieldDeltaList, fieldDelta)
    end
    -- limit integral history
    if #fieldDeltaList > intgTime then
      while #fieldDeltaList > intgTime do table.remove(fieldDeltaList, 1) end
    end
  else
    -- if not allowed to integrate, still set minimal override
    setIn(0)
  end
end

local function updateTempEnergyPID()
  -- Temperature PID driving target energy delta
  local tDelta = setTemp - Temp()
  local eP1 = tDelta * tempCoe[1]
  table.insert(tempDeltaList, tDelta)
  if temp_it >= 3 and #tempDeltaList >= 2 then
    temp_eI = sum(tempDeltaList) * tempCoe[2]
    local eD1 = (tDelta - (tempDeltaList[#tempDeltaList - 1] or tempDeltaList[#tempDeltaList])) * tempCoe[3]
    -- eT1 total temperature PID output
    local eT1 = eP1 + temp_eI + eD1
    local eDelta = eT1 * 100
    -- now energy PID (use eDelta as input)
    local eP2 = eDelta * energyCoe[1]
    if eP2 + eV() >= 0 then
      table.insert(energyDeltaList, eDelta)
      if energy_it >= 3 and #energyDeltaList >= 2 then
        energy_eI = sum(energyDeltaList) * energyCoe[2]
        local eD2 = (eDelta - (energyDeltaList[#energyDeltaList - 1] or energyDeltaList[#energyDeltaList])) * energyCoe[3]
        local eT2 = eP2 + energy_eI + eD2
        setOut(eV() + eT2)
      else
        -- still use proportional-only
        setOut(eV() + eP2)
      end
    end
    -- trim histories
    if #tempDeltaList > intgTime then
      while #tempDeltaList > intgTime do table.remove(tempDeltaList, 1) end
    end
    if #energyDeltaList > intgTime then
      while #energyDeltaList > intgTime do table.remove(energyDeltaList, 1) end
    end
  else
    -- initial accumulation: still set baseline outputs
    temp_it = math.min(temp_it + 1, intgTime + 1)
    energy_it = math.min(energy_it + 1, intgTime + 1)
    -- set conservative out to current generation (no extra)
    setOut(eV())
  end
end

-------------------------------------------------------------------------------
-- GUI drawing (monitor proportional to 3x2)
-------------------------------------------------------------------------------
local function initMonitorLayout()
  if not monitor then return end
  local ok, w, h = pcall(monitor.getSize, monitor)
  if ok and w and h then
    mon_w, mon_h = w, h
  else
    -- fallback
    mon_w, mon_h = 51, 19
  end
  seg_w = math.floor(mon_w / 3)
  seg_h = math.floor(mon_h / 2)
end

local function monClear()
  if not monitor then return end
  pcall(monitor.clear, monitor)
  pcall(monitor.setCursorPos, monitor, 1, 1)
end

local function drawCentered(x, y, text)
  if not monitor then return end
  local txt = tostring(text)
  local len = #txt
  local col = math.max(1, math.floor((seg_w - len) / 2))
  pcall(monitor.setCursorPos, monitor, (x - 1) * seg_w + col, (y - 1) * seg_h + 1)
  pcall(monitor.write, monitor, txt)
end

local function drawGUI()
  if not monitor then return end
  monClear()
  -- Title
  pcall(monitor.setCursorPos, monitor, 2, 1)
  pcall(monitor.write, monitor, "Draconic Reactor Control")
  -- Status region (left-top segment)
  local st = ("State: %s"):format(state:upper())
  pcall(monitor.setCursorPos, monitor, 1, 3)
  pcall(monitor.write, monitor, st)

  -- Data region (middle-top)
  pcall(monitor.setCursorPos, monitor, seg_w + 2, 3)
  local fRate = FieldRate() * 100
  local eRate = EnergyRate() * 100
  local fuelPct = (1 - FuelRate()) * 100
  pcall(monitor.write, monitor, ("Field: %.2f%%"):format(fRate))
  pcall(monitor.setCursorPos, monitor, seg_w + 2, 4)
  pcall(monitor.write, monitor, ("Energy: %.2f%%"):format(eRate))
  pcall(monitor.setCursorPos, monitor, seg_w + 2, 5)
  pcall(monitor.write, monitor, ("Fuel Left: %.2f%%"):format(fuelPct))

  -- Temp region (right-top)
  pcall(monitor.setCursorPos, monitor, 2 * seg_w + 2, 3)
  pcall(monitor.write, monitor, ("TEMP: %d C"):format(Temp()))
  pcall(monitor.setCursorPos, monitor, 2 * seg_w + 2, 4)
  pcall(monitor.write, monitor, ("Out: %.2f RF/t"):format(curOut - curIn))

  -- Center: big Field percent
  pcall(monitor.setCursorPos, monitor, seg_w + 2, seg_h - 2)
  pcall(monitor.write, monitor, ("Field Strength: %.2f%%"):format(FieldRate() * 100))

  -- Bottom area: button
  local btnY = mon_h - 2
  pcall(monitor.setCursorPos, monitor, 2, btnY)
  local btnText = (running and "[ STOP REACTOR ]" or "[ START REACTOR ]")
  pcall(monitor.write, monitor, btnText)

  -- If error state, show warning
  if state == "error" then
    pcall(monitor.setCursorPos, monitor, 2, btnY - 2)
    pcall(monitor.write, monitor, "!!! ERROR / SAFETY STOPPED !!!")
  end
end

-------------------------------------------------------------------------------
-- Touch handling: check if touch inside button area, toggle start/stop
-- Uses proportional layout (bottom-left segment area)
-------------------------------------------------------------------------------
local function handleMonitorTouch(x, y)
  if not monitor or not mon_w or not mon_h then return end
  local btnY = mon_h - 2
  -- simplify: if y >= btnY and x between 1..mon_w, toggle
  if y >= btnY then
    if state ~= "error" then
      if running then
        -- stop reactor control: stop PIDs, try to stop reactor
        running = false
        -- stop reactor and zero outputs
        stopReactor()
        setOut(0)
        setIn(64000000)
      else
        running = true
        -- attempt initialize/attach
        initializeReactor()
      end
    else
      -- if in error, clicking does nothing (could be extended to reset)
    end
  end
end

-------------------------------------------------------------------------------
-- Emergency supervise (single-step check inside main loop)
-------------------------------------------------------------------------------
local function emergencyCheck()
  if state == "active" and (Temp() >= MAX_TEMP or FieldRate() <= MIN_FIELD_RATE) then
    -- immediate protective actions
    setOut(0)
    setIn(64000000)
    pcall(stopReactor)
    state = "error"
    running = false
  end
end

-------------------------------------------------------------------------------
-- State machine handlers
-------------------------------------------------------------------------------
local function handleIdle()
  -- waiting for peripherals
  detectPeripherals()
  local ok, msg = checkAPIs()
  if ok then
    state = "check"
  else
    state = "idle"
  end
end

local function handleCheck()
  local ok, msg = checkAPIs()
  if ok then
    state = "attach"
  else
    state = "idle"
  end
end

local function handleAttach()
  -- try to attach to existing reactor state (charge, activate if needed)
  -- keep previous running flag
  if initializeReactor() then
    -- if reactor is running already, accept control
    if status() == "running" then
      state = "active"
      running = true
    else
      -- if not running but charged, go to charge and then active
      state = "charge"
    end
  else
    -- initialization failed, wait a bit and retry
    state = "charge"
  end
end

local function handleCharge()
  -- charge until thresholds reached then try activate
  chargeReactor()
  setOut(0)
  setIn(64000000)
  local t = Temp()
  local f = Field()
  local e = Energy()
  if t >= 2000 and f >= 0.49 and e >= 0.49 then
    startReactor()
    if status() == "running" then
      state = "active"
      running = true
    else
      -- keep charging / waiting
      state = "charge"
    end
  else
    state = "charge"
  end
end

local function handleActive()
  emergencyCheck()
  if state == "error" then return end
  -- when running flag false, interpret as user stop
  if not running then
    -- stop PIDs and attempt to stop reactor
    stopReactor()
    state = "idle"
    return
  end
  -- update PID based on tick counters handled in main loop
end

local function handleError()
  -- show error and keep outputs safe
  setOut(0)
  setIn(64000000)
  -- wait until user reconnects peripherals and manually restart by clicking Start
end

-------------------------------------------------------------------------------
-- MAIN loop: uses os.startTimer + os.pullEvent for precise tick & non-blocking touch
-------------------------------------------------------------------------------
local function main()
  -- initial detection
  detectPeripherals()
  initMonitorLayout()
  monClear()
  if monitor then
    pcall(monitor.setCursorPos, monitor, 1, 1)
    pcall(monitor.write, monitor, "Detecting peripherals...")
  end

  -- start a repeating timer for ticks
  local timerId = os.startTimer(TICK)

  while true do
    local ev = { os.pullEvent() }
    if ev[1] == "timer" and ev[2] == timerId then
      -- schedule next tick
      timerId = os.startTimer(TICK)
      tickCounter = tickCounter + 1
      fieldCounter = fieldCounter + 1
      tempCounter = tempCounter + 1

      -- refresh detection occasionally (every 1s)
      if tickCounter % math.floor(1 / TICK) == 0 then
        detectPeripherals()
        initMonitorLayout()
      end

      -- run state machine dispatcher
      if state == "idle" then handleIdle()
      elseif state == "check" then handleCheck()
      elseif state == "attach" then handleAttach()
      elseif state == "charge" then handleCharge()
      elseif state == "active" then handleActive()
      elseif state == "error" then handleError()
      end

      -- PID updates on their own intervals (only if running and state active)
      if running and state == "active" then
        if fieldCounter >= FIELD_INTERVAL then
          updateFieldPID()
          fieldCounter = 0
        end
        if tempCounter >= TEMP_INTERVAL then
          updateTempEnergyPID()
          tempCounter = 0
        end
        -- avgOut sample (simple)
        avgOut = curOut - curIn
      end

      -- emergency check each tick
      emergencyCheck()

      -- draw GUI each tick (cheap)
      pcall(drawGUI)

    elseif ev[1] == "monitor_touch" then
      -- monitor touch event: params vary but typically (side, x, y)
      local side = ev[2]
      local tx = ev[3]
      local ty = ev[4]
      -- ensure event belongs to our monitor (if monitor.name matches 'side')
      if monitor and peripheralNames.monitor ~= "" then
        if side == peripheralNames.monitor then
          handleMonitorTouch(tx, ty)
        end
      else
        -- if monitor auto-detected, we cannot get side easily; still handle by touch
        handleMonitorTouch(tx, ty)
      end

    elseif ev[1] == "terminate" then
      -- user requested stop; cleanup
      setOut(0)
      setIn(64000000)
      stopReactor()
      break
    else
      -- ignore other events
    end
  end
end

-------------------------------------------------------------------------------
-- Entry point
-------------------------------------------------------------------------------
local ok, msg = pcall(main)
if not ok then
  -- best effort safe shutdown
  pcall(setOut, 0)
  pcall(setIn, 64000000)
  pcall(stopReactor)
  -- print error
  print("Script terminated with error:", msg)
end