Files
reactor/reactor.lua
T
jiangchen 40fa421f89 @
Reactor Monitor v0.3.0 - bugfix release

Fixes applied to original code:
- instantiateControl: handle nil cfg (redstone mode "none")
- check(): preserve error message before reactor stops
- profile_count_mismatch: disable reactor and redstone, not zombie
- component.proxy pcall protection in MonitorWindow:onLoad
- instantiateItem nil guard
- App:start crash recovery with emergency shutdown
- emergencyShutdown method forces redstone off on fatal error

Co-Authored-By: Claude <noreply@anthropic.com>
@
2026-07-21 22:38:40 +08:00

4708 lines
111 KiB
Lua

local _argv = table.pack(require('shell').parse(...))
-- Bundled by luabundle {"version":"1.7.0"}
local __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)
local loadingPlaceholder = {[{}] = true}
local register
local modules = {}
local require
local loaded = {}
register = function(name, body)
if not modules[name] then
modules[name] = body
end
end
require = function(name)
local loadedModule = loaded[name]
if loadedModule then
if loadedModule == loadingPlaceholder then
return nil
end
else
if not modules[name] then
if not superRequire then
local identifier = type(name) == 'string' and '\"' .. name .. '\"' or tostring(name)
error('Tried to require ' .. identifier .. ', but no such module has been registered')
else
return superRequire(name)
end
end
loaded[name] = loadingPlaceholder
loadedModule = modules[name](require, loaded, register, modules)
loaded[name] = loadedModule
end
return loadedModule
end
return require, loaded, register, modules
end)(require)
__bundle_register("__root", function(require, _LOADED, __bundle_register, __bundle_modules)
require("core.pal")
local shell = palRequire('shell')
local event = palRequire('event')
local term = palRequire('term')
local i18n = require("core.i18n")
local utils = require("core.utils")
local config = require("reactor.config")
local builtins = require("reactor.builtins")
local App = require("ui.app")
local WindowSetup = require("reactor.window_setup")
local WindowRun = require("reactor.window_run")
local function appEntry(window)
return function(...) App:new(...):start(window:new()) end
end
local function usage()
print(_T('usage'))
end
local subCommands = {
setup = appEntry(WindowSetup),
run = appEntry(WindowRun),
help = usage,
}
local function main(args, options)
if options.debug then
debugLog = utils.debug
end
local cfg = config.load(options.config)
local lang = cfg and cfg.lang
i18n.setup('res/reactor/i18n', lang)
local mode = args[1] or 'run'
if mode and not subCommands[mode] then
print(string.format(_T('invalid_command'), mode))
usage()
return
end
local cfgReadyForRun =
cfg and
cfg.lang and
type(cfg.instances) == 'table' and
#cfg.instances > 0
if mode ~= 'setup' and not cfgReadyForRun then
mode = 'setup'
print(_T('no_valid_config'))
event.pull('key_up')
end
builtins.setup()
config.prepare()
subCommands[mode](options)
if not options.debug then
term.clear()
end
end
main(table.unpack(_argv))
end)
__bundle_register("reactor.window_run", function(require, _LOADED, __bundle_register, __bundle_modules)
local term = palRequire('term')
local event = palRequire('event')
local keyboard = palRequire('keyboard')
local class = require("core.class")
local App = require("ui.app")
local utils = require("core.utils")
local Window = require("ui.window")
local Label = require("ui.label")
local Button = require("ui.button")
local Grid = require("ui.grid")
local Row = require("ui.row")
local Column = require("ui.column")
local Tabs = require("ui.tabs")
local Frame = require("ui.frame")
local Progress = require("ui.progress")
local appVersion = require("version")
local Chamber = require("reactor.chamber")
local config = require("reactor.config")
local ReactorUI = class(Frame.class)
function ReactorUI:init(super, reactor)
super.init(reactor.i.name)
self.reactor = reactor
self.lblStatus = Label(""):size(nil, 1)
self.lblTemp = Label("0%"):size(4, 1)
self.prgTemp = Progress(0):size(nil, 1)
local inner = Column({
Row({
self.lblStatus,
self.lblTemp,
}):size(nil, 1),
self.prgTemp
})
self:addSubview(inner)
reactor:setDelegate(self)
end
function ReactorUI:onReactorUpdate()
local reactor = self.reactor
local temp = reactor:temperature()
self.lblStatus:setText(reactor:statusDescription())
self.lblTemp:setText(tostring(math.floor(temp * 100)) .. "%")
self.prgTemp:setValue(temp)
end
local function buildUI(reactors)
local title = Label(string.format(_T('title_monitor_app'), appVersion)):size(nil, 1)
local main = Grid()
for _, reactor in ipairs(reactors) do
local rcUI = ReactorUI:new(reactor):size(nil, 4)
main:addSubview(rcUI)
end
local status = Label(_T('keyboard_tips_run')):size(nil, 1)
local root = Column({
title,
main,
status
})
return {
root = root,
}
end
local alwaysOn = {
getInput = function() return true end,
}
local MonitorWindow = class(Window)
function MonitorWindow:onLoad()
self.running = false
local rawConfig = config.get()
local globalControl = alwaysOn
if rawConfig.global_control then
local ok, ctrl = pcall(config.instantiateControl, rawConfig.global_control)
if ok and ctrl then
globalControl = ctrl
else
print(string.format('Failed to initialize global control: %s', tostring(ctrl)))
event.pull('key_up')
end
end
self.globalControl = globalControl
local reactors = {}
for i, cfg in ipairs(rawConfig.instances) do
local ok, instance = pcall(config.instantiate, cfg)
if ok and instance then
local rc = Chamber:new(instance)
rc:attachTo(self.app.runloop)
reactors[#reactors + 1] = rc
else
print(string.format("Failed to initialize reactor '%s': %s", cfg.name or 'unknown', tostring(instance)))
event.pull('key_up')
end
end
self.reactors = reactors
self.app.crashHandler = function(err)
self:emergencyShutdown(err)
end
term.clear()
self.ui = buildUI(self.reactors).root
end
function MonitorWindow:startReactors()
self.running = true
if self.globalControl:getInput() then
self:startReactorsInner()
end
end
function MonitorWindow:startReactorsInner()
for _, reactor in ipairs(self.reactors) do
reactor:start()
end
end
function MonitorWindow:stopReactors()
self.running = false
self:stopReactorsInner()
end
function MonitorWindow:stopReactorsInner()
for _, reactor in ipairs(self.reactors) do
reactor:stop()
end
end
function MonitorWindow:emergencyShutdown(err)
self.running = false
for _, reactor in ipairs(self.reactors) do
reactor:stop()
pcall(function() reactor.i.control:disable() end)
end
end
-- Event handlers
function MonitorWindow:on_key_down(device, key, keycode)
if keycode == keyboard.keys.q then
self:stopReactors()
self:dismiss()
elseif keycode == keyboard.keys.r then
self:startReactors()
elseif keycode == keyboard.keys.s then
self:stopReactors()
end
end
function MonitorWindow:on_redstone_changed(device, side, oldValue, newValue, color)
debugLog('redstone changed, global control is', self.globalControl:getInput())
if not self.running then
return
end
if self.globalControl:getInput() then
self:startReactorsInner()
else
self:stopReactorsInner()
end
end
return MonitorWindow
end)
__bundle_register("reactor.config", function(require, _LOADED, __bundle_register, __bundle_modules)
local utils = require("core.utils")
local builtins = require("reactor.builtins")
local reactorControl = require("reactor.reactor_control")
local component = palRequire('component')
local filesystem = palRequire('filesystem')
local kDefaultConfigPath = 'reactor.conf'
local configFileName = nil
local config = nil
local schemasByKey = {}
local function configLoad(path)
configFileName = path or kDefaultConfigPath
config = utils.loadSerializedObject(configFileName)
return config
end
local function configSave()
if config then
utils.saveSerializedObject(configFileName, config)
end
end
local function instantiateControl(cfg, rc)
if not cfg or cfg.mode == 'none' then
return reactorControl.Adapter:new(rc)
elseif cfg.mode == 'adapter' then
return reactorControl.Adapter:new(rc)
elseif cfg.mode == 'vanilla' then
return reactorControl.Vanilla:new(cfg)
elseif cfg.mode == 'bundled' then
return reactorControl.Bundled:new(cfg)
else
error(string.format(_T('unsupported_control'), cfg.mode or '<nil>'))
end
end
local function instantiateItem(description)
if not description then
error("Item not configured for profile slot variable")
end
local item = {}
item.name = description.name
local checkFn
if description.change == 'none' then
checkFn = builtins.check.noCheck
elseif description.change == 'damage_less' then
checkFn = builtins.check.damageLess(description.threshold)
else
error(string.format(_T('unsupported_item_check'), description.change or '<nil>'))
end
item.check = checkFn
return item
end
local function configInstantiate(cfg)
if not builtins.schemas then
error('Builtins was not setup, this is a bug.')
end
local ret = {}
ret.name = cfg.name
ret.enabled = cfg.enabled
ret.minHeat = cfg.heat_min
ret.maxHeat = cfg.heat_max
ret.interval = 0.5 + 0.05 * #config.instances
-- components
ret.reactor = component.proxy(cfg.components.reactor)
local tp = {}
local tpcfg = cfg.components.transposer
local tproxy = component.proxy(tpcfg.address)
setmetatable(tp, { __index = tproxy })
tp.itemIn = tpcfg.item_in
tp.itemOut = tpcfg.item_out
tp.itemReactor = tpcfg.item_reactor
ret.transposer = tp
local ctrl = instantiateControl(cfg.components.redstone, ret.reactor)
ret.control = ctrl
-- profiles
local profiles = {}
for k, v in pairs(cfg.profiles) do
local schema = schemasByKey[v.schema]
if not schema then
error(string.format(_T('invalid_schema'), v.schema or '<nil>'))
end
local profile = {}
profile.count = schema.count
local layout = {}
for i, id in ipairs(schema.layout) do
layout[i] = instantiateItem(v.item[id])
end
profile.layout = layout
profiles[k] = profile
end
ret.profiles = profiles
return ret
end
local function configPrepare()
for _, schema in ipairs(builtins.schemas) do
schemasByKey[schema.name] = schema
end
if not config then
return
end
for _, schema in ipairs(config.schemas or {}) do
schemasByKey[schema.name] = schema
end
end
local function configBackup()
local cwd = os.getenv('PWD')
local configPath = filesystem.canonical(string.format('%s/%s', cwd, configFileName))
local backupFilename = configPath .. '.bak'
if filesystem.exists(configPath) then
filesystem.copy(configPath, backupFilename)
return backupFilename
end
return nil
end
return {
load = configLoad,
save = configSave,
backup = configBackup,
get = function() return config end,
set = function(newConfig) config = newConfig end,
instantiate = configInstantiate,
instantiateControl = instantiateControl,
prepare = configPrepare,
}
end)
__bundle_register("reactor.reactor_control", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local component = palRequire('component')
local AdapterReactorControl = class()
function AdapterReactorControl:init(_, rcproxy)
self.rc = rcproxy
self.rc.setActive(false)
self.enabled = false
end
function AdapterReactorControl:enable()
if not self.enabled then
self.rc.setActive(true)
self.enabled = true
end
end
function AdapterReactorControl:disable()
if self.enabled then
self.rc.setActive(false)
self.enabled = false
end
end
local VanillaReactorControl = class()
function VanillaReactorControl:init(_, cfg)
self.rs = component.proxy(cfg.address)
self.side = cfg.side
self.rs.setOutput(self.side, 0)
self.enabled = false
end
function VanillaReactorControl:getInput()
return self.rs.getInput(self.side) ~= 0
end
function VanillaReactorControl:enable()
if not self.enabled then
self.rs.setOutput(self.side, 15)
self.enabled = true
end
end
function VanillaReactorControl:disable()
if self.enabled then
self.rs.setOutput(self.side, 0)
self.enabled = false
end
end
local BundledReactorControl = class()
function BundledReactorControl:init(_, cfg)
self.rs = component.proxy(cfg.address)
self.side = cfg.side
self.color = cfg.color
self.rs.setBundledOutput(self.side, self.color, 0)
self.enabled = false
end
function BundledReactorControl:getInput()
return self.rs.getBundledInput(self.side, self.color) ~= 0
end
function BundledReactorControl:enable()
if not self.enabled then
self.rs.setBundledOutput(self.side, self.color, 15)
self.enabled = true
end
end
function BundledReactorControl:disable()
if self.enabled then
self.rs.setBundledOutput(self.side, self.color, 0)
self.enabled = false
end
end
return {
Adapter = AdapterReactorControl,
Vanilla = VanillaReactorControl,
Bundled = BundledReactorControl,
}
end)
__bundle_register("core.class", function(require, _LOADED, __bundle_register, __bundle_modules)
local function findImplClass(class, method)
local currentClass = class
while currentClass do
if rawget(currentClass, method) then
return currentClass
end
currentClass = getmetatable(currentClass)
end
return nil
end
-- Forward declaration for recursive calls of functions below.
local invokeSuper
local function createSuper(object, class)
local base = getmetatable(class) or {}
local superMt = {
__index = function(_, method)
if type(base[method]) == 'function' then
return function(...)
invokeSuper(object, base, method, ...)
end
end
return nil
end
}
return setmetatable({}, superMt)
end
invokeSuper = function(object, superClass, method, ...)
local base = findImplClass(superClass, method)
if base then
local super = createSuper(object, base)
base[method](object, super, ...)
end
end
-- A root class to prevent subclasses invoke `super.init()` unexpectedly.
local ObjectClass = {
init = function() end
}
local function makeClass(base)
local class = {}
class.__index = class
setmetatable(class, base or ObjectClass)
-- We may want a super object for every instant method. However
-- this brings more overhead, since OpenOS has limited "hardware"
-- resource, we only support a super object for the `init` method.
-- For other methods, using `Base.method(self, ...)` is needed.
function class:new(...)
local object = setmetatable({}, class)
local implClass = findImplClass(class, 'init')
if implClass then
local initMethod = rawget(implClass, 'init')
initMethod(object, createSuper(object, implClass), ...)
end
return object
end
return class
end
return makeClass
end)
__bundle_register("reactor.builtins", function(require, _LOADED, __bundle_register, __bundle_modules)
local builtins = {}
function builtins.setup()
local schemas = {
{
name = 'builtin.vacuum',
displayName = _T('schema_name_vacuum'),
size = {
w = 9,
h = 6,
},
count = 54,
layout = {
'B', 'A', 'A', 'A', 'B', 'A', 'A', 'B', 'A',
'A', 'A', 'B', 'A', 'A', 'A', 'A', 'B', 'A',
'B', 'A', 'A', 'A', 'A', 'B', 'A', 'A', 'A',
'A', 'A', 'A', 'B', 'A', 'A', 'A', 'A', 'B',
'A', 'B', 'A', 'A', 'A', 'A', 'B', 'A', 'A',
'A', 'B', 'A', 'A', 'B', 'A', 'A', 'A', 'B'
}
},
{
name = 'builtin.single',
displayName = _T('schema_name_single'),
size = {
w = 9,
h = 6,
},
count = 54,
layout = {
'A'
}
}
}
local items = {
{ id = 'gregtech:gt.rodUranium', reusable = true, },
{ id = 'gregtech:gt.rodUranium2', reusable = true, },
{ id = 'gregtech:gt.rodUranium4', reusable = true, },
{ id = 'gregtech:gt.rodThorium', reusable = true, },
{ id = 'gregtech:gt.rodThorium2', reusable = true, },
{ id = 'gregtech:gt.rodThorium4', reusable = true, },
{ id = 'gregtech:gt.rodMOX', reusable = true, },
{ id = 'gregtech:gt.rodMOX2', reusable = true, },
{ id = 'gregtech:gt.rodMOX4', reusable = true, },
{ id = 'gregtech:gt.rodNaquadah', reusable = true, },
{ id = 'gregtech:gt.rodNaquadah2', reusable = true, },
{ id = 'gregtech:gt.rodNaquadah4', reusable = true, },
{ id = 'gregtech:gt.rodHighDensityUranium', reusable = true, },
{ id = 'gregtech:gt.rodHighDensityUranium2', reusable = true, },
{ id = 'gregtech:gt.rodHighDensityUranium4', reusable = true, },
{ id = 'gregtech:gt.rodHighDensityPlutonium', reusable = true, },
{ id = 'gregtech:gt.rodHighDensityPlutonium2', reusable = true, },
{ id = 'gregtech:gt.rodHighDensityPlutonium4', reusable = true, },
{ id = 'gregtech:gt.rodExcitedUranium', reusable = true, },
{ id = 'gregtech:gt.rodExcitedUranium2', reusable = true, },
{ id = 'gregtech:gt.rodExcitedUranium4', reusable = true, },
{ id = 'gregtech:gt.rodExcitedPlutonium', reusable = true, },
{ id = 'gregtech:gt.rodExcitedPlutonium2', reusable = true, },
{ id = 'gregtech:gt.rodExcitedPlutonium4', reusable = true, },
{ id = 'gregtech:gt.rodNaquadria', reusable = true, },
{ id = 'gregtech:gt.rodNaquadria2', reusable = true, },
{ id = 'gregtech:gt.rodNaquadria4', reusable = true, },
{ id = 'gregtech:gt.rodTiberium', reusable = true, },
{ id = 'gregtech:gt.rodTiberium2', reusable = true, },
{ id = 'gregtech:gt.rodTiberium4', reusable = true, },
{ id = 'gregtech:gt.rodNaquadah32', reusable = true, },
{ id = 'gregtech:gt.rodGlowstone', reusable = true, },
{ id = 'gregtech:gt.60k_Helium_Coolantcell', reusable = false, },
{ id = 'gregtech:gt.180k_Helium_Coolantcell', reusable = false, },
{ id = 'gregtech:gt.360k_Helium_Coolantcell', reusable = false, },
{ id = 'gregtech:gt.60k_NaK_Coolantcell', reusable = false, },
{ id = 'gregtech:gt.180k_NaK_Coolantcell', reusable = false, },
{ id = 'gregtech:gt.360k_NaK_Coolantcell', reusable = false, },
{ id = 'gregtech:gt.180k_Space_Coolantcell', reusable = false, },
{ id = 'gregtech:gt.360k_Space_Coolantcell', reusable = false, },
{ id = 'gregtech:gt.540k_Space_Coolantcell', reusable = false, },
{ id = 'gregtech:gt.1080k_Space_Coolantcell', reusable = false, },
{ id = 'gregtech:gt.neutroniumHeatCapacitor', reusable = false, },
{ id = 'IC2:reactorVentCore', reusable = true, },
}
local reuseableItems = {}
for _, itemInfo in ipairs(items) do
reuseableItems[itemInfo.id] = itemInfo.reusable
end
local function isReusable(item)
return reuseableItems[item] == true
end
local checks = {
noCheck = function() return false end,
damageLess = function(threshold)
return function(item)
if item.damage == nil then
return false
end
local t = item.maxDamage * threshold
return (item.maxDamage - item.damage) <= t
end
end
}
builtins.schemas = schemas
builtins.items = items
builtins.isReusable = isReusable
builtins.check = checks
end
return builtins
end)
__bundle_register("core.utils", function(require, _LOADED, __bundle_register, __bundle_modules)
local serialization = palRequire('serialization')
local computer = palRequire('computer')
local colors = palRequire('colors')
local function debugLog(...)
local up = computer.uptime()
local ts = string.format('[%f]', up)
local args = table.pack(...)
table.insert(args, '\r')
print(ts, table.unpack(args))
end
local function loadSerializedObject(filename)
local f = io.open(filename)
if not f then return nil end
local s = f:read('a')
f:close()
return serialization.unserialize(s)
end
local function saveSerializedObject(filename, value)
local s = serialization.serialize(value)
local f = io.open(filename, 'w')
f:write(s)
f:close()
end
local function bind(f, ...)
local boundArgs = table.pack(...)
return function(...)
local args = table.pack(...)
for i = 1, #args do
table.insert(boundArgs, args[i])
end
f(table.unpack(boundArgs))
end
end
local function copy(obj)
if type(obj) ~= 'table' then
return obj
end
local copied = {}
for k, v in pairs(obj) do
copied[k] = copy(v)
end
return copied
end
local sideNames = {
'up', 'north', 'south', 'west', 'east', [0] = 'down'
}
local function sideDescription(side)
return sideNames[side]
end
local function colorDescription(color)
return colors[color]
end
return {
loadSerializedObject = loadSerializedObject,
saveSerializedObject = saveSerializedObject,
sideDescription = sideDescription,
colorDescription = colorDescription,
bind = bind,
copy = copy,
debug = debugLog,
}
end)
__bundle_register("reactor.chamber", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local utils = require("core.utils")
local builtins = require("reactor.builtins")
local computer = palRequire('computer')
local ReactorChamber = class()
function ReactorChamber:init(_, instance)
self.i = instance
self.rlName = 'Reactor:'..instance.name
self.running = false
self.state = _T('reactor_state_stopped')
self.profileName = 'N/A'
self.heat = 0
self.maxHeat = 10000
self:checkTemperature()
end
function ReactorChamber:attachTo(runloop)
self.rl = runloop
end
function ReactorChamber:setDelegate(delegate)
self.delegate = delegate
self.delegate:onReactorUpdate(self)
end
function ReactorChamber:start()
if self.running then return end
self.running = true
self.error = nil
self.maxHeat = self.i.reactor.getMaxHeat()
self.rl:enqueueScheduled(self.rlName, 0, utils.bind(self.check, self))
end
function ReactorChamber:stop()
self.running = false
end
-- For UI presentation.
-- Getting heat from reactor component is indirect method, it will consume
-- 1 game tick, we cache them for UI.
function ReactorChamber:temperature()
local heat = self.heat
local maxHeat = self.maxHeat
return heat / maxHeat, heat, maxHeat
end
function ReactorChamber:statusDescription()
debugLog('rc: state is' .. self.state)
return self.state
end
function ReactorChamber:check()
debugLog('rc check', self.rlName)
self.error = nil
local startTime = computer.uptime()
if not self.running then
self.i.control:disable()
self.state = _T('reactor_state_stopped')
if self.delegate then
self.delegate:onReactorUpdate(self)
end
return
end
-- check temp
self:checkTemperature()
if self.delegate then
self.delegate:onReactorUpdate(self)
end
if self.error then
debugLog('rc: checkTemp error', self.error)
self.running = false
self.i.control:disable()
if self.delegate then
self.delegate:onReactorUpdate(self)
end
return
end
-- check profile
if self:checkProfile() then
-- apply profile if needed
self.i.control:disable()
if self.error then
debugLog('rc: checkprofile error', self.error)
self.state = _T('reactor_state_error'):format(self.error)
self.running = false
else
debugLog('rc: wait for applyProfile')
self.rl:enqueueScheduled(self.rlName, computer.uptime() + 1.0, utils.bind(self.applyProfile, self))
end
return
end
self.i.control:enable()
self.state = _T('reactor_state_running'):format(self.profileName)
-- schedule next check
local nextCheck = startTime + self.i.interval
self.rl:enqueueScheduled(self.rlName, nextCheck, utils.bind(self.check, self))
end
function ReactorChamber:checkTemperature()
local function switchProfile(name)
local profile = self.i.profiles[name]
if not profile then
self.error = string.format(_T('no_profile'), name)
return
end
if self.activeProfile ~= profile then
self.activeProfile = profile
self.profileName = _T('profile_name_'..name)
debugLog('rc: switched to profile '..name)
end
end
local heat = self.i.reactor.getHeat()
self.heat = heat
if heat > self.i.maxHeat then
switchProfile('cooldown')
elseif heat < self.i.minHeat then
switchProfile('heatup')
else
switchProfile('working')
end
end
function ReactorChamber:checkProfile()
local tp = self.i.transposer
local reactorItems = tp.getAllStacks(tp.itemReactor).getAll()
local layout = self.activeProfile.layout
for i = 1, self.activeProfile.count do
local profileItem = layout[i]
local reactorItem = reactorItems[i - 1]
if not reactorItem then
self.error = _T('profile_count_mismatch')
return true
end
if not reactorItem.name and profileItem then
-- need insert
return true
elseif reactorItem.name and not profileItem then
-- need remove
return true
elseif reactorItem.name and profileItem then
-- need compare
if reactorItem.name ~= profileItem.name or profileItem.check(reactorItem) then
return true
end
end
end
return false
end
function ReactorChamber:applyProfile()
if not self.error then
self.state = _T('reactor_state_applying')
end
self.error = nil
if not self.running then
self.state = _T('reactor_state_stopped')
if self.delegate then
self.delegate:onReactorUpdate(self)
end
return
end
local tp = self.i.transposer
local function removeFromReactor(index, name)
local dst = tp.itemOut
if builtins.isReusable(name) then
dst = tp.itemIn
end
debugLog(string.format('remove reactor item #%d', index))
local count = tp.transferItem(tp.itemReactor, dst, 1, index)
coroutine.yield()
if count == 0 then
self.error = _T('output_full')
return false
end
debugLog(string.format('remove reactor item #%d success', index))
return true
end
local function insertIntoReactor(index, name)
debugLog(string.format('insert %s to reactor slot #%d', name, index))
local items = tp.getAllStacks(tp.itemIn).getAll()
coroutine.yield()
for i = 1, #items do
local item = items[i - 1]
if item.name == name then
local count = tp.transferItem(tp.itemIn, tp.itemReactor, 1, i, index)
coroutine.yield()
if count > 0 then
debugLog(string.format('insert %s to reactor slot #%d success', name, index))
return true
end
end
end
debugLog(string.format('insert %s to reactor slot #%d fail', name, index))
self.error = string.format(_T('item_shortage'), name)
return false
end
local function doProfile()
local reactorItems = tp.getAllStacks(tp.itemReactor).getAll()
coroutine.yield()
local layout = self.activeProfile.layout
for i = 1, self.activeProfile.count do
if not self.running then
self.state = _T('reactor_state_stopped')
if self.delegate then
self.delegate:onReactorUpdate(self)
end
return
end
debugLog(string.format('rc: apply for slot #%d', i))
local profileItem = layout[i]
local reactorItem = reactorItems[i - 1]
if not reactorItem then
debugLog('reactorItem not exist!')
self.error = _T('profile_count_mismatch')
self.running = false
self.i.control:disable()
self.state = _T('reactor_state_error'):format(self.error)
if self.delegate then
self.delegate:onReactorUpdate(self)
end
return
end
if not reactorItem.name and profileItem then
-- need insert
insertIntoReactor(i, profileItem.name)
elseif reactorItem.name and not profileItem then
-- need remove
removeFromReactor(i, reactorItem.name)
elseif reactorItem.name and profileItem then
-- need compare
if reactorItem.name ~= profileItem.name or profileItem.check(reactorItem) then
local _ = removeFromReactor(i, reactorItem.name) and insertIntoReactor(i, profileItem.name)
end
end
if self.error then
-- retry after 1s
debugLog('apply error:', self.error)
self.state = _T('reactor_state_error'):format(self.error)
self.rl:enqueueScheduled(self.rlName, computer.uptime() + 1.0, utils.bind(self.applyProfile, self))
return
end
if self.delegate then
self.delegate:onReactorUpdate(self)
end
end
self.rl:enqueueScheduled(self.rlName, 0, utils.bind(self.check, self))
end
self.rl:enqueueIdleYieldable(self.rlName, doProfile)
end
return ReactorChamber
end)
__bundle_register("version", function(require, _LOADED, __bundle_register, __bundle_modules)
return '0.3.0'
end)
__bundle_register("ui.progress", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local UIElement = require("ui.element").class
local wrap = require("ui.wrap_class")
local unicode = palRequire('unicode')
local term = palRequire('term')
local Progress = class(UIElement)
function Progress:init(super, value)
super.init()
self.value = value
end
local progressSym = {
filled = unicode.char(0x2588),
blank = unicode.char(0x2591)
}
function Progress:draw(gpu)
local w = self.rect.w
local left = math.floor(w * self.value)
local right = w - left
local str = progressSym.filled:rep(left) .. progressSym.blank:rep(right)
local x, y = self:screenPos(0, 0)
gpu.set(x, y, str)
end
function Progress:setValue(v)
if self.value ~= v then
self.value = v
self:setNeedUpdate()
end
end
return wrap(Progress)
end)
__bundle_register("ui.wrap_class", function(require, _LOADED, __bundle_register, __bundle_modules)
local meta = {
__call = function(class, ...)
return class.class:new(...)
end
}
local function wrapUiClass(class)
local o = {
class = class
}
setmetatable(o, meta)
return o
end
return wrapUiClass
end)
__bundle_register("ui.element", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local Navigation = require("ui.navigation")
local wrap = require("ui.wrap_class")
local UIElement = class()
function UIElement:init()
self.rect = { x = 0, y = 0, w = 0, h = 0 }
self.intrinsicSize = {}
self.needUpdate = true
end
-- MARK: - Property modifiers
function UIElement:size(w, h)
self.intrinsicSize.w = w
self.intrinsicSize.h = h
return self
end
function UIElement:makeSelectable(selectable)
self.selectable = selectable
return self
end
function UIElement:setSelected(state)
if state ~= self.selected then
self.selected = state
self:setNeedUpdate()
end
end
function UIElement:action(a)
self._action = a
return self
end
-- MARK: - Painting
function UIElement:draw(gpu)
error("UIElement:draw() method should be overriden")
end
function UIElement:clear(gpu)
gpu.fill(self.rect.x, self.rect.y, self.rect.w, self.rect.h, ' ')
end
function UIElement:update(gpu)
if self.window and self.needUpdate then
self.needUpdate = false
self:draw(gpu)
end
end
function UIElement:setNeedUpdate()
self.needUpdate = true
if self.window then
self.window:enqueueUpdate(self)
end
end
function UIElement:moveToWindow(win)
self.window = win
end
function UIElement:screenPos(x, y)
return self.rect.x + x, self.rect.y + y
end
-- MARK: - Arrow key navigation support
function UIElement:initSelection(navFrom)
if self.selectable then
return self
end
return nil
end
function UIElement:handleNavigation(nav)
if self.parent then
return self.parent:handleNavigation(nav)
end
return nil
end
-- MARK: - Touch screen support
function UIElement:elementAtPoint(x, y)
if x >= self.rect.x and y >= self.rect.y and x < self.rect.x + self.rect.w and y < self.rect.y + self.rect.h then
return self
else
return nil
end
end
function UIElement:getAction()
return self._action
end
-- MARK: - Reserved / unused
function UIElement:keyShortcut(key, action)
self.keyShortcut = key
self.keyAction = action
return self
end
function UIElement:onClick(action)
self.clickAction = action
return self
end
return wrap(UIElement)
end)
__bundle_register("ui.navigation", function(require, _LOADED, __bundle_register, __bundle_modules)
local Navigation = {
up = 1,
down = 2,
left = 3,
right = 4,
}
return Navigation
end)
__bundle_register("ui.frame", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local wrap = require("ui.wrap_class")
local Container = require("ui.container").class
local unicode = palRequire('unicode')
local borders = {
tl = unicode.char(0x2552),
t = unicode.char(0x2550),
tr = unicode.char(0x2555),
l = unicode.char(0x2502),
r = unicode.char(0x2502),
bl = unicode.char(0x2514),
b = unicode.char(0x2500),
br = unicode.char(0x2518),
}
local Frame = class(Container)
function Frame:init(super, title, content)
super.init()
self.title = title
if content then
self:addSubview(content)
end
end
function Frame:draw(gpu)
local w = self.rect.w
local topCount = (w - 2 - unicode.wlen(self.title))
local top = borders.tl .. self.title .. borders.t:rep(topCount) .. borders.tr
local sx, sy = self:screenPos(0, 0)
gpu.set(sx, sy, top)
local h = self.rect.h
for y = 1, h - 2, 1 do
sx, sy = self:screenPos(0, y)
gpu.set(sx, sy, borders.l)
sx, sy = self:screenPos(w - 1, y)
gpu.set(sx, sy, borders.r)
end
sx, sy = self:screenPos(0, h - 1)
gpu.set(sx, sy, borders.bl .. borders.b:rep(w - 2) .. borders.br)
local child = self.children[1]
if child then
child:draw(gpu)
end
end
function Frame:layout()
local child = self.children[1]
if not child then return end
child.rect.x = self.rect.x + 1
child.rect.y = self.rect.y + 1
child.rect.w = self.rect.w - 2
child.rect.h = self.rect.h - 2
if child.children then
child:layout()
end
end
return wrap(Frame)
end)
__bundle_register("ui.container", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local wrap = require("ui.wrap_class")
local UIElement = require("ui.element").class
local Container = class(UIElement)
function Container:init(super, children)
super.init()
self.selectionIndex = nil
self.children = {}
if children then
for _, child in ipairs(children) do
self:addSubview(child)
end
end
end
function Container:addSubview(v)
table.insert(self.children, v)
v.parent = self
v:moveToWindow(self.window)
end
function Container:moveToWindow(win)
self.window = win
for _, child in ipairs(self.children) do
child:moveToWindow(win)
end
end
function Container:draw(gpu)
for _, child in ipairs(self.children) do
child:draw(gpu)
child.needUpdate = false
end
end
function Container:initSelection(navFrom)
for i, child in ipairs(self.children) do
local selected = child:initSelection(navFrom)
if selected then
self.selectionIndex = i
return selected
end
end
return nil
end
function Container:trySelectPrevChild(nav)
local nextSelectionIndex = self.selectionIndex - 1
while nextSelectionIndex > 0 do
local nextSelectedElement = self.children[nextSelectionIndex]:initSelection(nav)
if nextSelectedElement then
self.selectionIndex = nextSelectionIndex
return nextSelectedElement
end
nextSelectionIndex = nextSelectionIndex - 1
end
return Container.handleNavigation(self, nav)
end
function Container:trySelectNextChild(nav)
local nextSelectionIndex = self.selectionIndex + 1
while nextSelectionIndex <= #self.children do
local nextSelectedElement = self.children[nextSelectionIndex]:initSelection(nav)
if nextSelectedElement then
self.selectionIndex = nextSelectionIndex
return nextSelectedElement
end
nextSelectionIndex = nextSelectionIndex + 1
end
return Container.handleNavigation(self, nav)
end
function Container:handleNavigation(nav)
if self.parent then
local next = self.parent:handleNavigation(nav)
if next then
-- navigated outside of this container
self.selectionIndex = nil
return next
end
end
return nil
end
function Container:elementAtPoint(x, y)
for _, child in ipairs(self.children) do
if child:elementAtPoint(x, y) then
return child
end
end
return UIElement.elementAtPoint(self, x, y)
end
function Container:layout(w, h)
error("Override me")
end
return wrap(Container)
end)
__bundle_register("ui.tabs", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local wrap = require("ui.wrap_class")
local Row = require("ui.row")
local Column = require("ui.column").class
local Label = require("ui.label")
local unicode = palRequire('unicode')
local term = palRequire('term')
local symbols = {
normal = unicode.char(0x2500),
highlight = unicode.char(0x2580),
}
local Tabs = class(Column)
function Tabs:init(super, tabs)
super.init({})
self.tabs = tabs
self.selectedTabIndex = 1
local tabRow = Row():size(nil, 2)
self.tabRow = tabRow
for i, item in ipairs(tabs) do
local label = Label(item[1]):size(nil, 1):makeSelectable(true)
tabRow:addSubview(label)
end
local origInitSelection = tabRow.initSelection
local origHandleNavigation = tabRow.handleNavigation
local outerSelf = self
-- override the tabRow instance methods
function tabRow:initSelection(nav)
-- always select the current tab index
self.selectionIndex = outerSelf.selectedTabIndex
return self.children[self.selectionIndex]
end
function tabRow:handleNavigation(nav)
local result = origHandleNavigation(self, nav)
if self.selectionIndex then
outerSelf:selectTab(self.selectionIndex)
end
return result
end
self:addSubview(tabRow)
self:addSubview(tabs[1][2])
end
function Tabs:selectTab(index)
if index == self.selectedTabIndex then
return
end
self.selectedTabIndex = index
local oldChild = self.children[2]
local newChild = self.tabs[index][2]
self.children[2] = newChild
oldChild:moveToWindow(nil)
newChild.parent = self
newChild:moveToWindow(self.window)
self:layout()
self:setNeedUpdate()
end
function Tabs:draw(gpu)
self:clear(gpu)
Column.draw(self, gpu)
self:drawTabLine(gpu)
end
function Tabs:drawTabLine(gpu)
local selectedLabel = self.tabRow.children[self.selectedTabIndex]
if not selectedLabel then
return
end
local rowRect = self.tabRow.rect
local rect = selectedLabel.rect
gpu.set(rowRect.x, rowRect.y + 1, symbols.normal:rep(rowRect.w))
gpu.set(rect.x, rect.y + 1, symbols.highlight:rep(rect.w))
end
return wrap(Tabs)
end)
__bundle_register("ui.label", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local wrap = require("ui.wrap_class")
local UIElement = require("ui.element").class
local utils = require("ui.utils")
local term = palRequire('term')
local Label = class(UIElement)
function Label:init(super, text)
super.init()
self.text = text
end
function Label:draw(gpu)
if self.selected then
utils.setHighlight(gpu)
end
self:clear(gpu)
-- TODO: support line wrapping
local x, y = self:screenPos(0, 0)
gpu.set(x, y, self.text)
if self.selected then
utils.setNormal(gpu)
end
end
function Label:setText(t)
if self.text ~= t then
self.text = t
self:setNeedUpdate()
end
end
return wrap(Label)
end)
__bundle_register("ui.utils", function(require, _LOADED, __bundle_register, __bundle_modules)
local utils = {}
function utils.setHighlight(gpu)
gpu.setBackground(0xffffff)
gpu.setForeground(0x0)
end
function utils.setNormal(gpu)
gpu.setBackground(0x0)
gpu.setForeground(0xffffff)
end
return utils
end)
__bundle_register("ui.column", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local directionContainerLayout = require("ui.layout")
local Navigation = require("ui.navigation")
local wrap = require("ui.wrap_class")
local Container = require("ui.container").class
local Column = class(Container)
function Column:layout()
directionContainerLayout(self, 'h', 'w', 'y', 'x')
-- print('-- column layout')
-- for i, c in ipairs(self.children) do
-- print(string.format('children %d: x=%d, y=%d, w=%d, h=%d', i, c.rect.x, c.rect.y, c.rect.w, c.rect.h))
-- end
end
function Column:initSelection(navFrom)
if navFrom == Navigation.up then
-- reverse order
for i = #self.children, 1, -1 do
local selected = self.children[i]:initSelection(navFrom)
if selected then
return selected
end
end
return nil
end
return Container.initSelection(self, navFrom)
end
function Column:handleNavigation(nav)
if nav == Navigation.up then
return self:trySelectPrevChild(nav)
elseif nav == Navigation.down then
return self:trySelectNextChild(nav)
end
return Container.handleNavigation(self, nav)
end
return wrap(Column)
end)
__bundle_register("ui.layout", function(require, _LOADED, __bundle_register, __bundle_modules)
local function directionContainerLayout(c, main, cross, mainOffset, crossOffset)
local w = c.rect[main]
local fixed = 0
local nFlex = 0
for i, child in ipairs(c.children) do
if child.intrinsicSize[main] then
fixed = fixed + child.intrinsicSize[main]
else
nFlex = nFlex + 1
end
end
local unit = 0
if nFlex > 0 then unit = (w - fixed) // nFlex end
local x = 0
for i, child in ipairs(c.children) do
local childMain, childCross
if child.intrinsicSize[main] then
childMain = child.intrinsicSize[main]
else
childMain = unit
end
childCross = child.intrinsicSize[cross] or c.rect[cross]
child.rect[mainOffset] = c.rect[mainOffset] + x
child.rect[crossOffset] = c.rect[crossOffset]
child.rect[main] = childMain
child.rect[cross] = childCross
if child.layout then
child:layout()
end
x = x + childMain
end
end
return directionContainerLayout
end)
__bundle_register("ui.row", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local directionContainerLayout = require("ui.layout")
local Navigation = require("ui.navigation")
local wrap = require("ui.wrap_class")
local Container = require("ui.container").class
local Row = class(Container)
function Row:layout()
directionContainerLayout(self, 'w', 'h', 'x', 'y')
-- print('-- row layout')
-- for i, c in ipairs(self.children) do
-- print(string.format('children %d: x=%d, y=%d, w=%d, h=%d', i, c.rect.x, c.rect.y, c.rect.w, c.rect.h))
-- end
end
function Row:initSelection(navFrom)
if navFrom == Navigation.left then
-- reverse order
for i = #self.children, 1, -1 do
local selected = self.children[i]:initSelection(navFrom)
if selected then
self.selectionIndex = i
return selected
end
end
return nil
end
return Container.initSelection(self, navFrom)
end
function Row:handleNavigation(nav)
if nav == Navigation.left then
return self:trySelectPrevChild(nav)
elseif nav == Navigation.right then
return self:trySelectNextChild(nav)
end
return Container.handleNavigation(self, nav)
end
return wrap(Row)
end)
__bundle_register("ui.grid", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local wrap = require("ui.wrap_class")
local Container = require("ui.container").class
local Grid = class(Container)
function Grid:layout()
if #self.children == 0 then return end
local child1 = self.children[1]
local w = self.rect.w
local h = self.rect.h
local cw = child1.intrinsicSize.w
local ch = child1.intrinsicSize.h
local nr = 1
local nc = 1
if cw == nil and ch == nil then
error("Grid element must have at least 1 intrinsic size")
end
if cw == nil then
while math.ceil(#self.children / nc) * ch > h do nc = nc + 1 end
cw = w // nc
end
if ch == nil then
while math.ceil(#self.children / nr) * cw > w do nr = nr + 1 end
ch = h // nr
end
nc = w // cw
nr = math.ceil(#self.children / nc)
for i, child in ipairs(self.children) do
local x = (i - 1) % nc
local y = (i - 1) // nc
child.rect.x = self.rect.x + x * cw
child.rect.y = self.rect.y + y * ch
child.rect.w = cw
child.rect.h = ch
if child.children then
child:layout()
end
end
end
return wrap(Grid)
end)
__bundle_register("ui.button", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local wrap = require("ui.wrap_class")
local uiUtils = require("ui.utils")
local UIElement = require("ui.element").class
local term = palRequire('term')
local Button = class(UIElement)
function Button:init(super, text)
super.init()
self.selectable = true
self.text = text
self.active = false
end
function Button:draw(gpu)
if self.selected then
uiUtils.setHighlight(gpu)
end
self:clear(gpu)
local x, y = self:screenPos(0, 0)
gpu.set(x, y, self.text)
if self.selected then
uiUtils.setNormal(gpu)
end
end
function Button:setText(t)
if self.text ~= t then
self.text = t
self:setNeedUpdate()
end
end
return wrap(Button)
end)
__bundle_register("ui.window", function(require, _LOADED, __bundle_register, __bundle_modules)
local term = palRequire('term')
local keyboard = palRequire('keyboard')
local class = require("core.class")
local Navigation = require("ui.navigation")
local Window = class()
function Window:init(_)
self.updateQueue = {}
end
function Window:onLoad()
error('Should override Window:onLoad()')
end
function Window:onUnload()
end
function Window:handleEvent(name, ...)
if self.presentedWindow then
self.presentedWindow:handleEvent(name, ...)
return
end
local handler = self['on_' .. name]
if handler then
handler(self, ...)
end
end
function Window:present(aWindow, resultHandler)
local gpu = term.gpu()
local w, h = gpu.getResolution()
local bg = gpu.allocateBuffer(w, h)
gpu.bitblt(bg, 1, 1, w, h, 0)
self.presentedWindow = aWindow
self.app:present(
aWindow,
function(...)
self.presentedWindow = nil
gpu.bitblt(0, 1, 1, w, h, bg)
gpu.freeBuffer(bg)
self.ui:setNeedUpdate()
if resultHandler then
resultHandler(...)
end
end
)
end
function Window:dismiss(...)
if self.dismissHandler then
self.dismissHandler(...)
end
end
function Window:enqueueUpdate(element)
table.insert(self.updateQueue, element)
self.app:enqueueUpdate()
end
function Window:update(gpu)
if self.presentedWindow then
self.presentedWindow:update(gpu)
return
end
local q = self.updateQueue
self.updateQueue = {}
for _, e in pairs(q) do
e:update(gpu)
end
end
-- MARK: - keyboard navigation
function Window:initSelection()
local selectedElement = self.ui:initSelection(nil)
self.selectedElement = selectedElement
if selectedElement then
self.selectedElement:setSelected(true)
end
end
function Window:on_key_down(device, key, keycode)
if keycode == keyboard.keys.enter and self.selectedElement then
local actionParams = table.pack(self.selectedElement:getAction())
if #actionParams >= 1 then
local action = table.remove(actionParams, 1)
if self[action] then
self[action](self, table.unpack(actionParams))
return
end
end
end
local navigation = nil
if keycode == keyboard.keys.up then
navigation = Navigation.up
elseif keycode == keyboard.keys.down then
navigation = Navigation.down
elseif keycode == keyboard.keys.left then
navigation = Navigation.left
elseif keycode == keyboard.keys.right then
navigation = Navigation.right
end
if navigation and self.selectedElement then
local nextElement = self.selectedElement:handleNavigation(navigation)
if nextElement then
self.selectedElement:setSelected(false)
nextElement:setSelected(true)
self.selectedElement = nextElement
end
end
end
return Window
end)
__bundle_register("ui.app", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local utils = require("core.utils")
local Runloop = require("core.runloop")
local term = palRequire('term')
local App = class()
-- MARK: - App methods
function App:init(_, options)
self.runloop = Runloop:new()
self.options = options or {}
self.hasUpdates = false
App.shared = self
end
function App:start(window)
self.window = window
self:app_setup()
self.runloop:addEventHandler(self)
if not self.options.debug and self.window then
self:present(window, utils.bind(self.stop, self))
else --for debug
window.app = self
window.dismissHandler = utils.bind(self.stop, self)
window:onLoad()
end
local ok, err = pcall(function() self.runloop:run() end)
if not ok then
if self.crashHandler then
pcall(self.crashHandler, err)
end
term.clear()
print('===== FATAL ERROR =====')
print(tostring(err))
print('Press any key to exit...')
local ev = palRequire('event')
ev.pull('key_up')
end
end
function App:stop(reload)
if reload then
self:present(self.window, utils.bind(self.stop, self))
return
end
self.runloop:stop()
end
function App:present(window, handler)
window.app = self
self.runloop:enqueueIdle(
'App_UI',
function()
window.dismissHandler = handler
window:onLoad()
-- layout
local w, h, x, y = term.getViewport()
if window.preferredSize then
local newW = math.min(w, window.preferredSize.w)
local newH = math.min(h, window.preferredSize.h)
local newX = (x + 1) + (w - newW) // 2
local newY = (y + 1) + (h - newH) // 2
window.ui.rect = {x = newX, y = newY, w = newW, h = newH }
else
window.ui.rect = { x = x + 1, y = y + 1, w = w, h = h }
end
window.ui:layout()
window.ui:moveToWindow(window)
window:initSelection()
window.ui:setNeedUpdate()
end
)
end
function App:handleEvent(name, ...)
if not name then return end
debugLog(name, ...)
self.window:handleEvent(name, ...)
end
function App:enqueueUpdate()
if self.hasUpdates then
return
end
self.hasUpdates = true
self.runloop:enqueueIdle(
'App_UI',
function()
self.hasUpdates = false
if self.window then
self.window:update(term.gpu())
end
end
)
end
-- MARK: - Subclass overrides
function App:app_setup()
end
-- MARK: - Exports
return App
end)
__bundle_register("core.runloop", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local computer = palRequire('computer')
local event = palRequire('event')
-- Maximum loop execution time between system yields, in seconds.
--
-- A system yield will be inserted if runloop exceeds this time when
-- running.
--
-- GT:NH sets it to 5.0 seconds, we slightly tune this down for safety.
local kSystemYield = 3.0
local kTaskQueueEmpty = 0
local kTaskExecuted = 1
local kTaskNeedWait = 2
local Runloop = class()
function Runloop:init(super)
-- scheduled task queue
self.tq = {}
-- idle task queue
self.iq = {}
-- event handlers
self.eventHandlers = {}
self.running = false
end
function Runloop:run()
local deadline = kSystemYield
local startTime = computer.uptime()
self.running = true
while self.running or #self.tq > 0 or #self.iq > 0 do
local result, timeout = self:runOneTask()
if result == kTaskExecuted then
local endTime = computer.uptime()
if endTime - startTime >= kSystemYield then
os.sleep(0)
startTime = computer.uptime()
end
else
local e = table.pack(event.pull(timeout))
startTime = computer.uptime()
self:handleEvent(table.unpack(e))
end
end
end
function Runloop:stop()
self.running = false
end
function Runloop:addEventHandler(handler)
table.insert(self.eventHandlers, handler)
end
function Runloop:enqueueScheduled(name, target, fn)
local idx = #self.tq
while idx > 0 do
if self.tq[idx].target <= target then
break
end
idx = idx - 1
end
local task = {
name = name,
run = fn,
target = target
}
table.insert(self.tq, idx + 1, task)
end
function Runloop:enqueueIdle(name, fn)
local task = {
name = name,
run = fn,
}
table.insert(self.iq, task)
end
function Runloop:enqueueIdleYieldable(name, fn)
local co = coroutine.create(fn)
local function wrapped()
if coroutine.resume(co) then
self:enqueueIdle(name, wrapped)
end
end
self:enqueueIdle(name, wrapped)
end
function Runloop:runOneTask()
Runloop.current = self
local task
local nextScheduleTime = nil
if #self.tq > 0 then
if computer.uptime() >= self.tq[1].target then
task = table.remove(self.tq, 1)
task.run()
return kTaskExecuted
else
nextScheduleTime = self.tq[1].target - computer.uptime()
end
end
if #self.iq > 0 then
task = table.remove(self.iq, 1)
task.run()
return kTaskExecuted
end
return kTaskQueueEmpty, nextScheduleTime
end
function Runloop:handleEvent(name, ...)
for _, handler in pairs(self.eventHandlers) do
handler:handleEvent(name, ...)
end
end
return Runloop
end)
__bundle_register("reactor.window_setup", function(require, _LOADED, __bundle_register, __bundle_modules)
local term = palRequire('term')
local keyboard = palRequire('keyboard')
local class = require("core.class")
local App = require("ui.app")
local utils = require("core.utils")
local i18n = require("core.i18n")
local Window = require("ui.window")
local Label = require("ui.label")
local Button = require("ui.button")
local Grid = require("ui.grid")
local Row = require("ui.row")
local Column = require("ui.column")
local Tabs = require("ui.tabs")
local Table = require("ui.table")
local appVersion = require("version")
local config = require("reactor.config")
local builtins = require("reactor.builtins")
local Alert = require("ui.window_alert")
local SelectWindow = require("ui.window_select")
local RedstoneWindow = require("reactor.window_redstone")
local InstanceWindow = require("reactor.window_instance")
local SchemaWindow = require("reactor.window_schema")
local reactorUtils = require("reactor.utils")
local function makeDefaultConfig()
return {
lang = i18n.langList.default,
schemas = {},
instances = {},
}
end
local function getCopyName(name, listOfNameOwner)
local names = {}
for _, entry in pairs(listOfNameOwner) do
names[entry.name] = true
end
local namePrefix = string.format("%s %s", name, _T('name_copy'))
local suffix = 1
local newName = namePrefix
while names[newName] do
newName = string.format("%s%d", namePrefix, suffix)
suffix = suffix + 1
end
return newName
end
local SetupWindow = class(Window)
function SetupWindow:onLoad()
local cfg = config.get()
if not cfg then
cfg = makeDefaultConfig()
config.set(cfg)
end
self.config = cfg
term.clear()
local ui = self:buildUI()
local root = ui.root
self.tabs = ui.tabs
self.ui = root
end
function SetupWindow:buildGeneralTab()
local tableContents = {
{
{ display = _T('language') },
{ display = i18n.current, action = 'editI18n' }
},
{
{ display = _T('global_control') },
{ display = reactorUtils.redstoneDescription(self.config.global_control), action = 'editGlobalControl' }
},
-- {
-- { display = _T('sync_shutdown') },
-- { display = 'On' }
-- }
}
local tablecfg = {
showBorders = false,
rows = {
n = #tableContents
},
columns = {
n = 2,
defaultWidth = 20,
[1] = {
selectable = false,
width = 16
}
}
}
return Table(tableContents, tablecfg)
end
function SetupWindow:calcSchemasTabContent()
local tableContents = {}
local nextSectionRowIndex = 2
table.insert(tableContents, {
{ display = _T('builtin_schemas') },
})
local builtinSchemas = builtins.schemas
for i, schema in ipairs(builtinSchemas) do
table.insert(tableContents, {
{ display = schema.displayName or schema.name or 'Unnamed' },
{ display = _T('view'), selectable = true, action = 'editSchema', value = { builtin = true, i = i } },
{ display = _T('copy'), selectable = true, action = 'copySchema', value = { builtin = true, i = i } },
})
nextSectionRowIndex = nextSectionRowIndex + 1
end
table.insert(tableContents, {
{ display = _T('user_schemas') }
})
for i, schema in ipairs(self.config.schemas or {}) do
table.insert(tableContents, {
{ display = schema.displayName or schema.name or 'Unnamed' },
{ display = _T('edit'), selectable = true, action = 'editSchema', value = { builtin = false, i = i } },
{ display = _T('copy'), selectable = true, action = 'copySchema', value = { builtin = false, i = i } },
{ display = _T('delete'), selectable = true, action = 'deleteSchema', value = i },
})
end
table.insert(tableContents, {
{ display = _T('create_new'), selectable = true, action = 'newSchema' }
})
local tableCfg = {
showBorders = false,
rows = {
n = #tableContents,
},
columns = {
n = 4,
defaultWidth = 8,
[1] = {
width = 30,
}
}
}
return tableContents, tableCfg
end
function SetupWindow:buildSchemasTab()
local tableContents, tableCfg = self:calcSchemasTabContent()
return Table(tableContents, tableCfg):makeSelectable(false)
end
function SetupWindow:refreshSchemas()
local tableContents, tableCfg = self:calcSchemasTabContent()
self.schemasTabTable.contents = tableContents
self.schemasTabTable.config = tableCfg
self.schemasTabTable:reload()
end
function SetupWindow:calcReactorsTabContent()
local tableContents = {}
for i, instance in ipairs(self.config.instances or {}) do
table.insert(tableContents, {
{ display = instance.name or 'Unnamed' },
{ display = _T('edit'), selectable = true, action = 'editInstance', value = i },
{ display = _T('copy'), selectable = true, action = 'copyInstance', value = i },
{ display = _T('delete'), selectable = true, action = 'deleteInstance', value = i },
})
end
table.insert(tableContents, {
{ display = _T('create_new'), selectable = true, action = 'newInstance' }
})
local tableCfg = {
showBorders = false,
rows = {
n = #tableContents
},
columns = {
n = 4,
defaultWidth = 8,
[1] = {
width = 30
}
}
}
return tableContents, tableCfg
end
function SetupWindow:buildReactorsTab()
local tableContents, tableCfg = self:calcReactorsTabContent()
return Table(tableContents, tableCfg):makeSelectable(false)
end
function SetupWindow:refreshInstances()
local tableContents, tableCfg = self:calcReactorsTabContent()
self.reactorsTabTable.contents = tableContents
self.reactorsTabTable.config = tableCfg
self.reactorsTabTable:reload()
end
function SetupWindow:buildSaveTab()
return Table({
{
{ display = _T('save_and_exit'), action = 'saveAndExit' }
},
{
{ display = _T('discard_and_exit'), action = 'discardAndExit' }
}
}, {
showBorders = false,
rows = {
n = 2,
},
columns = {
n = 1,
defaultWidth = 30
}
})
end
function SetupWindow:buildUI()
local title = Label(string.format(_T('title_config_app'), appVersion)):size(nil, 1)
self.generalTabTable = self:buildGeneralTab()
self.schemasTabTable = self:buildSchemasTab()
self.reactorsTabTable = self:buildReactorsTab()
local save = self:buildSaveTab()
local main = Tabs({
{ _T('tab_general'), self.generalTabTable },
{ _T('tab_schemas'), self.schemasTabTable },
{ _T('tab_reactors'), self.reactorsTabTable },
{ _T('tab_save'), save },
})
local status = Label(_T('keyboard_tips_setup')):size(nil, 1)
local root = Column({
title,
main,
status
})
return {
root = root,
tabs = main,
}
end
function SetupWindow:on_key_down(device, key, keycode)
if keycode == keyboard.keys.q then
self:discardAndExit()
else
Window.on_key_down(self, device, key, keycode)
end
end
function SetupWindow:editI18n()
local languages = {}
for name, _ in pairs(i18n.langList) do
if name ~= 'default' then
table.insert(languages, name)
end
end
local win = SelectWindow:new(_T('language'), languages)
self:present(
win,
function(idx)
local langCode = i18n.langList[languages[idx]]
self.config.lang = langCode
self:reloadLanguage(langCode)
end
)
end
function SetupWindow:editGlobalControl()
local rs = self.config.global_control
local win = RedstoneWindow:new(rs)
self:present(
win,
function(editOk, newConfig)
if editOk then
self.config.global_control = newConfig
self.generalTabTable.contents[2][2].display = reactorUtils.redstoneDescription(self.config.global_control)
self.generalTabTable:setNeedUpdate()
end
end
)
end
function SetupWindow:newSchema()
table.insert(self.config.schemas, {
name = string.format('%s%d', _T('schema'), #self.config.schemas + 1),
size = {
w = 9,
h = 6,
},
count = 54,
layout = {},
})
self:refreshSchemas()
end
function SetupWindow:editSchema(info)
local schema
if info.builtin then
schema = builtins.schemas[info.i]
else
schema = self.config.schemas[info.i]
end
if not schema then
return
end
local win = SchemaWindow:new(schema, info.builtin)
self:present(
win,
function(editOk, newSchema)
if editOk and not info.builtin then
self.config.schemas[info.i] = newSchema
self:refreshSchemas()
end
end
)
end
function SetupWindow:copySchema(info)
local schema
if info.builtin then
schema = builtins.schemas[info.i]
else
schema = self.config.schemas[info.i]
end
if not schema then
return
end
schema = utils.copy(schema)
schema.name = getCopyName(schema.name, self.config.schemas)
schema.displayName = nil
table.insert(self.config.schemas, schema)
self:refreshSchemas()
end
function SetupWindow:deleteSchema(index)
local schema = self.config.schemas[index]
local alert = Alert:new(
_T('confirm_delete'),
string.format(_T('prompt_confirm_delete'), schema.name),
Alert.Ok | Alert.Cancel
)
self:present(
alert,
function(result)
if result == Alert.Ok then
table.remove(self.config.schemas, index)
self:refreshSchemas()
end
end
)
end
function SetupWindow:newInstance()
table.insert(self.config.instances, {
name = string.format('%s #%d', _T('reactor'), #self.config.instances + 1),
enabled = true,
heat_max = 0,
heat_min = 0,
components = {},
profiles = {},
})
self:refreshInstances()
end
function SetupWindow:editInstance(index)
local instance = self.config.instances[index]
local win = InstanceWindow:new(instance, self.config.schemas)
self:present(
win,
function(editOk, newInstance)
if editOk then
self.config.instances[index] = newInstance
self:refreshInstances()
end
end
)
end
function SetupWindow:copyInstance(index)
local instance = self.config.instances[index]
instance = utils.copy(instance)
instance.name = getCopyName(instance.name, self.config.instances)
table.insert(self.config.instances, instance)
self:refreshInstances()
end
function SetupWindow:deleteInstance(index)
local instance = self.config.instances[index]
local alert = Alert:new(
_T('confirm_delete'),
string.format(_T('prompt_confirm_delete'), instance.name),
Alert.Ok | Alert.Cancel
)
self:present(
alert,
function(result)
if result == Alert.Ok then
table.remove(self.config.instances, index)
self:refreshInstances()
end
end
)
end
function SetupWindow:saveAndExit()
local backupName = config.backup()
config.set(self.config)
config.save()
if backupName then
local alert = Alert:new(
_T('backup_saved'),
string.format(_T('prompt_backup'), backupName)
)
self:present(
alert,
function(result)
self:dismiss()
end
)
else
self:dismiss()
end
end
function SetupWindow:discardAndExit()
local alert = Alert:new(
_T('confirm_exit'),
string.format(_T('prompt_confirm_exit')),
Alert.Ok | Alert.Cancel
)
self:present(
alert,
function(result)
if result == Alert.Ok then
self:dismiss()
end
end
)
end
function SetupWindow:reloadLanguage(code)
i18n.reload(code)
builtins.setup()
self:dismiss(true)
end
return SetupWindow
end)
__bundle_register("reactor.utils", function(require, _LOADED, __bundle_register, __bundle_modules)
local utils = require("core.utils")
local builtins = require("reactor.builtins")
local function redstoneDescription(cfg)
if not cfg then
return _T('redstone_mode_none')
end
local desc = _T('redstone_mode_'..cfg.mode)
if cfg.mode == 'vanilla' or cfg.mode == 'bundled' then
desc = desc .. ',' .. _T(utils.sideDescription(cfg.side))
end
return desc
end
local function transposerDescription(cfg)
if not cfg then
return _T('not_configured')
end
return cfg.address
end
local function profileDescription(cfg)
if not cfg then
return _T('not_configured')
end
local schemaName = cfg.schema
for _, schema in pairs(builtins.schemas) do
if schema.name == schemaName then
schemaName = schema.displayName
break
end
end
return schemaName
end
local function changeConditionDescription(cfg)
if cfg then
if cfg.change == 'none' then
return _T('change_condition_none')
elseif cfg.change == 'damage_less' then
return _T('change_condition_damage_less')
end
end
return _T('not_configured')
end
return {
redstoneDescription = redstoneDescription,
transposerDescription = transposerDescription,
profileDescription = profileDescription,
changeConditionDescription = changeConditionDescription,
}
end)
__bundle_register("reactor.window_schema", function(require, _LOADED, __bundle_register, __bundle_modules)
local keyboard = palRequire('keyboard')
local class = require("core.class")
local utils = require("core.utils")
local Window = require("ui.window")
local Column = require("ui.column")
local Row = require("ui.row")
local Frame = require("ui.frame")
local Button = require("ui.button")
local Table = require("ui.table")
local Separator = require("ui.separator")
local Select = require("ui.window_select")
local InputWindow = require("ui.window_input")
require("reactor.window+select_component")
local function makeDefaultConfig()
return {
name = 'new_config',
size = { w = 9, h = 6 },
count = 54,
layout = {}
}
end
local SchemaWindow = class(Window)
function SchemaWindow:init(super, config, isBuiltin)
super.init()
self.config = utils.copy(config) or makeDefaultConfig()
self.isBuiltin = isBuiltin
self.tblCfgOptions = {
showBorders = false,
columns = {
n = 2,
defaultWidth = 16,
[1] = {
width = 8,
selectable = false,
},
},
rows = {
n = 2
}
}
self.tblLayoutOptions = {
showBorders = true,
columns = {
n = config.size.w,
defaultWidth = 2,
},
rows = {
n = config.size.h,
}
}
self.tblLayoutContents = {}
self:makeTableContents()
end
function SchemaWindow:onLoad()
local left = Table(self.tblCfgContents, self.tblCfgOptions):size(24)
self.left = left
local right = Table(self.tblLayoutContents, self.tblLayoutOptions)
self.right = right
if self.isBuiltin then
left:makeSelectable(false)
right:makeSelectable(false)
end
self.ui = Frame(
_T('schema_config'),
Column({
Row({
left,
Separator.vertical(),
right,
}),
Separator.horizontal(),
Row({
Button(_T('ok')):action('clickedOk'),
Button(_T('cancel')):action('clickedCancel'),
}):size(nil, 1)
})
)
self.preferredSize = { w = 60, h = 17 }
end
function SchemaWindow:makeTableContents()
self.tblCfgContents = {
{
{ display = _T('name') },
{ display = self.config.name, action = 'editName' }
},
{
{ display = _T('width') },
{ display = _T(tostring(self.config.size.w)), action = 'editWidth' }
},
{
{ display = _T('height') },
{ display = tostring(self.config.size.h), action = 'editHeight' }
},
}
self.tblCfgOptions.rows.n = #self.tblCfgContents
local w = self.config.size.w
local h = self.config.size.h
local layout = self.config.layout
for i = 1, h do
self.tblLayoutContents[i] = {}
for j = 1, w do
local idx = (i - 1) * w + j
self.tblLayoutContents[i][j] = {
display = layout[idx]
}
end
end
self.tblLayoutOptions.columns.n = w
self.tblLayoutOptions.rows.n = h
end
function SchemaWindow:editName()
local win = InputWindow:new(_T('schema_name'), _T('input_prompt_schema_name'))
self:present(
win,
function(result)
self.config.name = result
self:makeTableContents()
self.left.contents = self.tblCfgContents
self.left:reload()
end
)
end
function SchemaWindow:editWidth()
local win = InputWindow:new(_T('schema_width'), _T('input_prompt_schema_width'))
self:present(
win,
function(result)
self.config.size.w = tonumber(result)
self:makeTableContents()
self.left.contents = self.tblCfgContents
self.left:reload()
self.right:reload()
end
)
end
function SchemaWindow:editHeight()
local win = InputWindow:new(_T('schema_height'), _T('input_prompt_schema_height'))
self:present(
win,
function(result)
self.config.size.h = tonumber(result)
self:makeTableContents()
self.left.contents = self.tblCfgContents
self.left:reload()
self.right:reload()
end
)
end
function SchemaWindow:clickedOk()
self:dismiss(true, self.config)
end
function SchemaWindow:clickedCancel()
self:dismiss(false)
end
function SchemaWindow:editTableInplace(char)
local idx = (self.right.selectedRow - 1) * self.config.size.w + self.right.selectedColumn
self.config.layout[idx] = char
self:makeTableContents()
-- no changes to table config, no reload needed
self.right:setNeedUpdate()
end
function SchemaWindow:on_key_down(device, key, keycode)
if self.selectedElement == self.right then
if (key >= 0x41 and key <= 0x5a) or (key >= 0x61 and key <= 0x7a) then
-- Alphabet keys inside table
self:editTableInplace(string.char(key):upper())
return
elseif keycode == keyboard.keys.back then
self:editTableInplace(nil)
return
end
end
Window.on_key_down(self, device, key, keycode)
end
return SchemaWindow
end)
__bundle_register("reactor.window+select_component", function(require, _LOADED, __bundle_register, __bundle_modules)
local component = palRequire('component')
local Window = require("ui.window")
local Select = require("ui.window_select")
function Window:selectComponent(name, callback)
local devices = component.list(name)
local choices = {}
for k, _ in pairs(devices) do
table.insert(choices, k)
end
if #choices == 0 then
-- TODO: alert
return
end
local selectAddress = Select:new(_T('component_address'), choices)
self:present(
selectAddress,
function(result)
callback(choices[result])
end
)
end
end)
__bundle_register("ui.window_select", function(require, _LOADED, __bundle_register, __bundle_modules)
local keyboard = palRequire('keyboard')
local unicode = palRequire('unicode')
local class = require("core.class")
local Window = require("ui.window")
local Frame = require("ui.frame")
local Table = require("ui.table")
local Select = class(Window)
function Select:init(super, title, options)
super.init()
self.title = title or _T('select_title')
self.options = options
end
function Select:onLoad()
local maxWidth = unicode.wlen(self.title)
for _, option in ipairs(self.options) do
maxWidth = math.max(maxWidth, unicode.wlen(option))
end
local tableContents = self:makeTableContents()
local tableCfg = {
showBorders = false,
columns = {
n = 1,
defaultWidth = maxWidth
},
rows = {
n = #self.options
}
}
local main = Table(tableContents, tableCfg)
self.main = main
self.ui = Frame(
self.title,
main
)
self.preferredSize = { w = maxWidth + 3, h = #self.options + 2 }
end
function Select:on_key_down(device, key, keycode)
if keycode == keyboard.keys.enter then
self:dismiss(self.main.selectedRow)
else
Window.on_key_down(self, device, key, keycode)
end
end
function Select:makeTableContents()
local tableContents = {}
for _, option in ipairs(self.options) do
table.insert(tableContents, {
{ display = option }
})
end
return tableContents
end
return Select
end)
__bundle_register("ui.table", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local Navigation = require("ui.navigation")
local uiUtils = require("ui.utils")
local wrap = require("ui.wrap_class")
local Element = require("ui.element").class
local unicode = palRequire('unicode')
local symbols = {
tl = unicode.char(0x250c),
tm = unicode.char(0x252c),
tr = unicode.char(0x2510),
ml = unicode.char(0x251c),
mm = unicode.char(0x253c),
mr = unicode.char(0x2524),
bl = unicode.char(0x2514),
bm = unicode.char(0x2534),
br = unicode.char(0x2518),
hl = unicode.char(0x2500),
vl = unicode.char(0x2502),
arrowLeft = unicode.char(0x25c0),
arrowRight = unicode.char(0x25b6),
arrowUp = unicode.char(0x25b2),
arrowDown = unicode.char(0x25bc),
barHorz = unicode.char(0x2588),
barVert = unicode.char(0x2588),
}
local Table = class(Element)
function Table:init(super, contents, config)
super.init()
self.contents = contents
self.config = config
self.selectable = true
self.offset = {
x = 0,
y = 0,
}
self:calculatePositions()
end
function Table:reload()
self:calculatePositions()
self:setNeedUpdate()
end
function Table:initSelection(navFrom)
-- TODO: handle navFrom
for i = 1, self.config.rows.n do
for j = 1, self.config.columns.n do
if self:cellSelectableAt(i, j) then
self.selectedRow = i
self.selectedColumn = j
return self
end
end
end
return nil
end
function Table:handleNavigation(nav)
-- TODO: cell level refresh
local nextRow, nextCol = self:findNextSelectablePos(nav)
if nextRow and nextCol then
self:selectCell(nextRow, nextCol)
return self
end
local next = Element.handleNavigation(self, nav)
if next then
self.selectedRow = nil
self.selectedColumn = nil
self:setNeedUpdate()
end
return next
end
function Table:cellSelectableAt(row, col)
-- cell's config is at top priority
local row = self.contents[row]
local cell = row and row[col]
local cellSelectable = cell and cell.selectable
if cellSelectable ~= nil then
return cellSelectable
end
-- follow the row and column's config
local rowCfg = self.config.rows[row]
local rowSelectable = rowCfg and rowCfg.selectable
if rowSelectable ~= nil then
return rowSelectable
end
local colCfg = self.config.columns[col]
local colSelectable = colCfg and colCfg.selectable
if colSelectable ~= nil then
return colSelectable
end
return self.selectable
end
function Table:findNextSelectablePos(nav)
local fCfgMain, fCfgCross, fSelMain, fSelCross, delta
if nav == Navigation.up then
fCfgMain = 'rows'
fCfgCross = 'columns'
fSelMain = 'selectedRow'
fSelCross = 'selectedColumn'
delta = -1
elseif nav == Navigation.down then
fCfgMain = 'rows'
fCfgCross = 'columns'
fSelMain = 'selectedRow'
fSelCross = 'selectedColumn'
delta = 1
elseif nav == Navigation.left then
fCfgMain = 'columns'
fCfgCross = 'rows'
fSelMain = 'selectedColumn'
fSelCross = 'selectedRow'
delta = -1
elseif nav == Navigation.right then
fCfgMain = 'columns'
fCfgCross = 'rows'
fSelMain = 'selectedColumn'
fSelCross = 'selectedRow'
delta = 1
else
error("bug: unexpected nav direction")
end
local nextPos = {
selectedRow = self.selectedRow,
selectedColumn = self.selectedColumn
}
local nextMain = self[fSelMain] + delta
while nextMain >= 1 and nextMain <= self.config[fCfgMain].n do
nextPos[fSelMain] = nextMain
if self:cellSelectableAt(nextPos.selectedRow, nextPos.selectedColumn) then
return nextPos.selectedRow, nextPos.selectedColumn
end
-- skip if the row/column disable selection
local mAxisCfg = self.config[fCfgMain][nextMain]
if (mAxisCfg and mAxisCfg.selectable) ~= false then
-- search a cell inside cross direction
for deltaCross = 1, -1, -2 do
nextPos[fSelCross] = self[fSelCross]
local nextCross = self[fSelCross] + deltaCross
while nextCross >= 1 and nextCross <= self.config[fCfgCross].n do
nextPos[fSelCross] = nextCross
if self:cellSelectableAt(nextPos.selectedRow, nextPos.selectedColumn) then
return nextPos.selectedRow, nextPos.selectedColumn
end
nextCross = nextCross + deltaCross
end
end
end
nextMain = nextMain + delta
end
return nil
end
function Table:selectCell(row, col)
self.selectedRow = row
self.selectedColumn = col
local yStart = self.rowPositions[self.selectedRow]
local xStart = self.colPositions[self.selectedColumn]
local yEnd = self.rowPositions[self.selectedRow + 1] - 1
local xEnd = self.colPositions[self.selectedColumn + 1] - 1
-- adjust offset based on cell position
if self.offset.x > xStart then
self.offset.x = xStart
elseif self.offset.x + self.viewportW <= xEnd then
local nextOffsetX = xEnd - self.viewportW + 1
self.offset.x = math.min(xStart, nextOffsetX)
end
if self.offset.y > yStart then
self.offset.y = yStart
elseif self.offset.y + self.viewportH <= yEnd then
local nextOffsetY = yEnd - self.viewportH + 1
self.offset.y = math.min(yStart, nextOffsetY)
end
self:setNeedUpdate()
end
function Table:selectedContent()
if self.selectedRow and self.selectedColumn then
local row = self.contents[self.selectedRow]
return row and row[self.selectedColumn]
end
return nil
end
function Table:getAction()
local selected = self:selectedContent()
if selected then
return selected.action, selected.value
end
return nil
end
function Table:draw(gpu)
self:clear(gpu)
-- The whole painting area, include scroll bars.
local w = self.rect.w
local h = self.rect.h
-- The content viewport area, is painting area removing scroll bars.
local viewportW = w
local viewportH = h
local totalRows = self.config.rows.n
local totalCols = self.config.columns.n
-- The inner scrollable content size
local contentW = self.colPositions[#self.colPositions]
local contentH = self.rowPositions[#self.rowPositions]
local showBorders = self.config.showBorders
local shouldShowHorizontalScroll = false
local shouldShowVerticalScroll = false
if contentW > w then
shouldShowHorizontalScroll = true
viewportH = viewportH - 1
if contentH >= h then
shouldShowVerticalScroll = true
viewportW = viewportW - 1
end
elseif contentH > h then
shouldShowVerticalScroll = true
viewportW = viewportW - 1
if contentW >= w then
shouldShowHorizontalScroll = true
viewportH = viewportH - 1
end
end
self.viewportW = viewportW
self.viewportH = viewportH
-- paint contents
-- decide which cell to start draw
local rowPaintStartIndex = 1
local columnPaintStartIndex = 1
while self.colPositions[columnPaintStartIndex + 1] <= self.offset.x do
columnPaintStartIndex = columnPaintStartIndex + 1
end
while self.rowPositions[rowPaintStartIndex + 1] <= self.offset.y do
rowPaintStartIndex = rowPaintStartIndex + 1
end
local rowPaintIndex = rowPaintStartIndex
-- paint every row
local currentPaintOffsetY = self.offset.y
while currentPaintOffsetY < self.offset.y + viewportH and rowPaintIndex <= totalRows do
local columnPaintIndex = columnPaintStartIndex
local availableHeight = math.min(self.rowPositions[rowPaintIndex + 1], self.offset.y + viewportH) - currentPaintOffsetY
-- paint every column
local currentPaintOffsetX = self.offset.x
while currentPaintOffsetX < self.offset.x + viewportW and columnPaintIndex <= totalCols do
local availableWidth = math.min(self.colPositions[columnPaintIndex + 1], self.offset.x + viewportW) - currentPaintOffsetX
local cellAvailableHeight = availableHeight
local sx, sy = self:screenPos(currentPaintOffsetX - self.offset.x, currentPaintOffsetY - self.offset.y)
-- borders
if showBorders then
-- topleft
local borderTopleft = symbols.mm
if columnPaintIndex == 1 then
if rowPaintIndex == 1 then
borderTopleft = symbols.tl
else
borderTopleft = symbols.ml
end
elseif rowPaintIndex == 1 then
borderTopleft = symbols.tm
end
gpu.set(sx, sy, borderTopleft)
sx = sx + 1
-- top
gpu.set(sx, sy, symbols.hl:rep(availableWidth - 1))
sy = sy + 1
cellAvailableHeight = cellAvailableHeight - 1
-- left
if cellAvailableHeight > 0 then
gpu.set(sx - 1, sy, symbols.vl:rep(cellAvailableHeight), true)
end
end
if cellAvailableHeight > 0 then
local row = self.contents[rowPaintIndex]
local cell = row and row[columnPaintIndex]
local displayText = cell and cell.display or ' '
if unicode.wlen(displayText) > availableWidth then
displayText = unicode.wtrunc(displayText, availableWidth)
end
local shouldHighlight = self.selectedRow == rowPaintIndex and self.selectedColumn == columnPaintIndex
if shouldHighlight then
uiUtils.setHighlight(gpu)
end
gpu.set(sx, sy, displayText)
if shouldHighlight then
uiUtils.setNormal(gpu)
end
end
columnPaintIndex = columnPaintIndex + 1
currentPaintOffsetX = self.colPositions[columnPaintIndex]
end
-- try draw the rightmost border
if showBorders and currentPaintOffsetX < self.offset.x + viewportW then
-- topright
local sx, sy = self:screenPos(currentPaintOffsetX - self.offset.x, currentPaintOffsetY - self.offset.y)
if rowPaintIndex == 1 then
gpu.set(sx, sy, symbols.tr)
else
gpu.set(sx, sy, symbols.mr)
end
if availableHeight > 1 then
gpu.set(sx, sy + 1, symbols.vl:rep(availableHeight - 1), true)
end
end
rowPaintIndex = rowPaintIndex + 1
currentPaintOffsetY = self.rowPositions[rowPaintIndex]
end
-- try draw the bottommost border
if showBorders and currentPaintOffsetY < self.offset.y + viewportH then
local columnPaintIndex = columnPaintStartIndex
local currentPaintOffsetX = self.offset.x
while currentPaintOffsetX < self.offset.x + viewportW and columnPaintIndex <= totalCols do
local availableWidth = math.min(viewportW, self.colPositions[columnPaintIndex + 1]) - currentPaintOffsetX
local sx, sy = self:screenPos(currentPaintOffsetX - self.offset.x, currentPaintOffsetY - self.offset.y)
-- bottom left
if columnPaintIndex == 1 then
gpu.set(sx, sy, symbols.bl)
else
gpu.set(sx, sy, symbols.bm)
end
-- bottom
gpu.set(sx + 1, sy, symbols.hl:rep(availableWidth - 1))
columnPaintIndex = columnPaintIndex + 1
currentPaintOffsetX = self.colPositions[columnPaintIndex]
end
-- bottom right
if currentPaintOffsetX < self.offset.x + viewportW then
local sx, sy = self:screenPos(currentPaintOffsetX - self.offset.x, currentPaintOffsetY - self.offset.y)
gpu.set(sx, sy, symbols.br)
end
end
-- paint scroll bars
if shouldShowHorizontalScroll then
local sx, sy = self:screenPos(0, self.rect.h - 1)
gpu.set(sx, sy, symbols.arrowLeft)
local scrollLength = self.rect.w - 2
if shouldShowVerticalScroll then
scrollLength = self.rect.w - 3
end
local barLength = math.max(math.floor(viewportW / contentW * scrollLength), 1)
local barOffset = math.floor(self.offset.x / contentW * scrollLength)
gpu.set(sx + 1, sy, symbols.hl:rep(scrollLength))
gpu.set(sx + 1 + barOffset, sy, symbols.barHorz:rep(barLength))
gpu.set(sx + scrollLength + 1, sy, symbols.arrowRight)
end
if shouldShowVerticalScroll then
local sx, sy = self:screenPos(self.rect.w - 1, 0)
gpu.set(sx, sy, symbols.arrowUp)
local scrollLength = self.rect.h - 2
if shouldShowHorizontalScroll then
scrollLength = self.rect.h - 3
end
local barLength = math.max(math.floor(viewportH / contentH * scrollLength), 1)
local barOffset = math.floor(self.offset.y / contentH * scrollLength)
gpu.set(sx, sy + 1, symbols.vl:rep(scrollLength), true)
gpu.set(sx, sy + 1 + barOffset, symbols.barVert:rep(barLength), true)
gpu.set(sx, sy + scrollLength + 1, symbols.arrowDown)
end
end
function Table:calculatePositions()
local totalRows = self.config.rows.n
local totalCols = self.config.columns.n
local showBorders = self.config.showBorders
local pos = 0
local colPositions = {}
for i = 1, totalCols do
table.insert(colPositions, pos)
local colConfig = self.config.columns[i]
pos = pos + (colConfig and colConfig.width or self.config.columns.defaultWidth or 10)
if showBorders then
pos = pos + 1
end
end
table.insert(colPositions, pos)
pos = 0
local rowPositions = {}
for i = 1, totalRows do
table.insert(rowPositions, pos)
local rowConfig = self.config.rows[i]
pos = pos + (rowConfig and rowConfig.height or self.config.rows.defaultHeight or 1)
if showBorders then
pos = pos + 1
end
end
table.insert(rowPositions, pos)
self.colPositions = colPositions
self.rowPositions = rowPositions
end
return wrap(Table)
end)
__bundle_register("ui.window_input", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local Window = require("ui.window")
local Frame = require("ui.frame")
local Column = require("ui.column")
local Label = require("ui.label")
local Input = require("ui.input")
local InputWindow = class(Window)
function InputWindow:init(super, title, message, text)
super.init()
self.title = title or _T('input_title')
self.message = message or _T('input_message')
self.text = text or ''
end
function InputWindow:onLoad()
self.input = Input(self.text)
self.ui = Frame(self.title, Column({
Label(self.message),
self.input,
}))
self.preferredSize = { w = 60, h = 4 }
end
function InputWindow:update(gpu)
Window.update(self, gpu)
local result = self.input:editText(gpu)
self:dismiss(result)
end
return InputWindow
end)
__bundle_register("ui.input", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local wrap = require("ui.wrap_class")
local UIElement = require("ui.element").class
local utils = require("ui.utils")
local term = palRequire('term')
local Input = class(UIElement)
function Input:init(super, text)
super.init()
self.text = text
end
function Input:draw(gpu)
if self.selected then
utils.setHighlight(gpu)
end
self:clear(gpu)
-- TODO: support line wrapping
local x, y = self:screenPos(0, 0)
gpu.set(x, y, self.text)
if self.selected then
utils.setNormal(gpu)
end
end
function Input:setText(t)
if self.text ~= t then
self.text = t
self:setNeedUpdate()
end
end
function Input:editText(gpu)
if self.selected then
utils.setHighlight(gpu)
end
local x, y = self:screenPos(0, 0)
term.setCursor(x, y)
term.setCursorBlink(true)
local res = term.read():gsub('\n', '')
term.setCursorBlink(false)
if self.selected then
utils.setNormal(gpu)
end
return res
end
return wrap(Input)
end)
__bundle_register("ui.separator", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local wrap = require("ui.wrap_class")
local Element = require("ui.element").class
local unicode = palRequire('unicode')
local HSeparator = class(Element)
function HSeparator:init(super)
super.init()
self.intrinsicSize.h = 1
end
function HSeparator:draw(gpu)
local sx, sy = self:screenPos(0, 0)
gpu.fill(sx, sy, self.rect.w, 1, unicode.char(0x2500))
end
local VSeparator = class(Element)
function VSeparator:init(super)
super.init()
self.intrinsicSize.w = 1
end
function VSeparator:draw(gpu)
local sx, sy = self:screenPos(0, 0)
gpu.fill(sx, sy, 1, self.rect.h, unicode.char(0x2502))
end
return {
horizontal = wrap(HSeparator),
vertical = wrap(VSeparator),
}
end)
__bundle_register("reactor.window_instance", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local utils = require("core.utils")
local reactorUtils = require("reactor.utils")
local Window = require("ui.window")
local Column = require("ui.column")
local Row = require("ui.row")
local Frame = require("ui.frame")
local Button = require("ui.button")
local Table = require("ui.table")
local Separator = require("ui.separator")
local Select = require("ui.window_select")
local InputWindow = require("ui.window_input")
local RedstoneWindow = require("reactor.window_redstone")
local TransposerWindow = require("reactor.window_transposer")
local ProfileWindow = require("reactor.window_profile")
require("reactor.window+select_component")
local InstanceWindow = class(Window)
function InstanceWindow:init(super, config, schemas)
super.init()
self.config = utils.copy(config)
self.schemas = schemas
self.tableCfg = {
showBorders = false,
columns = {
n = 2,
defaultWidth = 20,
[1] = {
selectable = false,
}
},
rows = {
n = 1
}
}
self:makeTableContents()
end
function InstanceWindow:onLoad()
local list = Table(self.tableContents, self.tableCfg)
self.list = list
self.ui = Frame(
_T('instance_config'),
Column({
self.list,
Separator.horizontal(),
Row({
Button(_T('ok')):action('clickedOk'),
Button(_T('cancel')):action('clickedCancel'),
}):size(nil, 1)
})
)
self.preferredSize = { w = 50, h = 14 }
end
function InstanceWindow:makeTableContents()
self.tableContents = {
{
{ display = _T('name') },
{ display = self.config.name, action = 'editName' }
},
-- {
-- { display = _T('enabled') },
-- { display = _T(tostring(self.config.enabled)), action = 'editEnabled' }
-- },
{
{ display = _T('min_heat_temp') },
{ display = tostring(self.config.heat_min), action = 'editMinHeat' }
},
{
{ display = _T('max_heat_temp') },
{ display = tostring(self.config.heat_max), action = 'editMaxHeat' }
},
{
{ display = _T('reactor_address') },
{ display = self.config.components.reactor, action = 'editReactorAddress' }
},
{
{ display = _T('transposer') },
{ display = reactorUtils.transposerDescription(self.config.components.transposer), action = 'editTransposer' }
},
{
{ display = _T('redstone_control') },
{ display = reactorUtils.redstoneDescription(self.config.components.redstone), action = 'editRedstone' }
},
{
{ display = _T('profile_working') },
{ display = reactorUtils.profileDescription(self.config.profiles.working), action = 'editProfile', value = 'working' }
},
{
{ display = _T('profile_heatup') },
{ display = reactorUtils.profileDescription(self.config.profiles.heatup), action = 'editProfile', value = 'heatup' }
},
{
{ display = _T('profile_cooldown') },
{ display = reactorUtils.profileDescription(self.config.profiles.cooldown), action = 'editProfile', value = 'cooldown' }
},
}
self.tableCfg.rows.n = #self.tableContents
end
function InstanceWindow:makeRefreshingCallback(fn)
return function(...)
fn(...)
self:makeTableContents()
self.list.contents = self.tableContents
self.list:reload()
end
end
function InstanceWindow:editName()
local win = InputWindow:new(_T('instance_name'), _T('input_prompt_instance_name'))
self:present(
win,
self:makeRefreshingCallback(
function(result)
self.config.name = result
end
)
)
end
function InstanceWindow:editEnabled()
local selectEnabled = Select:new(_T('enabled'), {
_T('enabled'),
_T('disabled'),
})
self:present(
selectEnabled,
self:makeRefreshingCallback(
function(result)
self.config.enabled = result == 1
end
)
)
end
function InstanceWindow:editMaxHeat()
local win = InputWindow:new(_T('max_heat_temp'), _T('input_prompt_max_heat'))
self:present(
win,
self:makeRefreshingCallback(
function(result)
self.config.heat_max = tonumber(result)
end
)
)
end
function InstanceWindow:editMinHeat()
local win = InputWindow:new(_T('min_heat_temp'), _T('input_prompt_min_heat'))
self:present(
win,
self:makeRefreshingCallback(
function(result)
self.config.heat_min = tonumber(result)
end
)
)
end
function InstanceWindow:editReactorAddress()
self:selectComponent(
'reactor',
self:makeRefreshingCallback(
function(address)
self.config.components.reactor = address
end
)
)
end
function InstanceWindow:editTransposer()
local tp = self.config.components.transposer
local win = TransposerWindow:new(tp)
self:present(
win,
self:makeRefreshingCallback(
function(editOk, newConfig)
if editOk then
self.config.components.transposer = newConfig
end
end
)
)
end
function InstanceWindow:editRedstone()
local rs = self.config.components.redstone
local win = RedstoneWindow:new(rs)
self:present(
win,
self:makeRefreshingCallback(
function(editOk, newConfig)
if editOk then
self.config.components.redstone = newConfig
end
end
)
)
end
function InstanceWindow:editProfile(name)
local profile = self.config.profiles[name]
local win = ProfileWindow:new(profile, self.schemas)
self:present(
win,
self:makeRefreshingCallback(
function(editOk, newConfig)
if editOk then
self.config.profiles[name] = newConfig
end
end
)
)
end
function InstanceWindow:clickedOk()
self:dismiss(true, self.config)
end
function InstanceWindow:clickedCancel()
self:dismiss(false)
end
return InstanceWindow
end)
__bundle_register("reactor.window_profile", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local utils = require("core.utils")
local Window = require("ui.window")
local Column = require("ui.column")
local Row = require("ui.row")
local Frame = require("ui.frame")
local Button = require("ui.button")
local Table = require("ui.table")
local Separator = require("ui.separator")
local Select = require("ui.window_select")
local ItemWindow = require("reactor.window_item")
local builtins = require("reactor.builtins")
local function makeDefaultConfig()
return {
item = {}
}
end
local ProfileWindow = class(Window)
function ProfileWindow:init(super, config, schemas)
super.init()
self.config = utils.copy(config) or makeDefaultConfig()
local schemasByKey = {}
for _, schema in ipairs(builtins.schemas) do
schemasByKey[schema.name] = schema
end
for _, schema in ipairs(schemas or {}) do
schemasByKey[schema.name] = schema
end
self.schemasByKey = schemasByKey
self.schema = self.config.schema and schemasByKey[self.config.schema]
self.tblCfgOptions = {
showBorders = false,
columns = {
n = 2,
defaultWidth = 8,
[1] = {
selectable = false,
},
[2] = {
width = 10,
}
},
rows = {
n = 2
}
}
self.tblLayoutOptions = {
showBorders = true,
columns = {
n = 1,
defaultWidth = 2,
},
rows = {
n = 1,
}
}
self.tblLayoutContents = {}
self:makeTableContents()
end
function ProfileWindow:onLoad()
local left = Table(self.tblCfgContents, self.tblCfgOptions):size(24)
self.left = left
local right = Table(self.tblLayoutContents, self.tblLayoutOptions):makeSelectable(false)
self.right = right
self.ui = Frame(
_T('profile_config'),
Column({
Row({
left,
Separator.vertical(),
right,
}),
Separator.horizontal(),
Row({
Button(_T('ok')):action('clickedOk'),
Button(_T('cancel')):action('clickedCancel'),
}):size(nil, 1)
})
)
self.preferredSize = { w = 60, h = 17 }
end
function ProfileWindow:makeTableContents()
local schema = self.schema
local schemaDisplayName = schema and (schema.displayName or schema.name) or _T('not_configured')
local tblCfgContents = {
{
{ display = _T('schema') },
{ display = schemaDisplayName, action = 'editSchema' }
},
}
self.tblCfgContents = tblCfgContents
if not schema then
self.tblCfgOptions.rows.n = #tblCfgContents
-- tricks to 'hide' layout
self.tblLayoutOptions.showBorders = false
self.tblLayoutOptions.rows.n = 1
self.tblLayoutOptions.columns.n = 1
self.tblLayoutContents = {}
return
end
local uniqueLayoutVariables = {}
for _, var in pairs(schema.layout or {}) do
uniqueLayoutVariables[var] = true
end
local sortedLayoutVariables = {}
for var, _ in pairs(uniqueLayoutVariables) do
table.insert(sortedLayoutVariables, var)
end
table.sort(sortedLayoutVariables)
for _, var in pairs(sortedLayoutVariables) do
local item = self.config.item[var]
local itemDisplay = item and (item.name and _T(item.name)) or _T('not_configured')
table.insert(tblCfgContents, {
{ display = var },
{ display = itemDisplay, action = 'editItem', value = var }
})
end
self.tblCfgOptions.rows.n = #tblCfgContents
-- layout table contents
local w = schema.size.w
local h = schema.size.h
local layout = schema.layout
for i = 1, h do
self.tblLayoutContents[i] = {}
for j = 1, w do
local idx = (i - 1) * w + j
self.tblLayoutContents[i][j] = {
display = layout[idx]
}
end
end
self.tblLayoutOptions.showBorders = true
self.tblLayoutOptions.columns.n = w
self.tblLayoutOptions.rows.n = h
end
function ProfileWindow:editSchema()
local schemaList = { _T('not_configured') }
local backed = { 1 } -- placeholder
for name, schema in pairs(self.schemasByKey) do
table.insert(schemaList, schema.displayName or schema.name)
table.insert(backed, name)
end
local win = Select:new(_T('select_schema'), schemaList)
self:present(
win,
function(result)
local schemaName = nil
if result ~= 1 then
schemaName = backed[result]
end
if self.config.schema ~= schemaName then
self.config.item = {}
end
self.config.schema = schemaName
self.schema = schemaName and self.schemasByKey[schemaName]
self:makeTableContents()
self.left.contents = self.tblCfgContents
self.left:reload()
self.right.contents = self.tblLayoutContents
self.right:reload()
end
)
end
function ProfileWindow:editItem(varName)
local win = ItemWindow:new(self.config.item[varName])
self:present(
win,
function(editOk, newItem)
if editOk then
self.config.item[varName] = newItem
self:makeTableContents()
self.left.contents = self.tblCfgContents
self.left:reload()
end
end
)
end
function ProfileWindow:clickedOk()
self:dismiss(true, self.config)
end
function ProfileWindow:clickedCancel()
self:dismiss(false)
end
return ProfileWindow
end)
__bundle_register("reactor.window_item", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local utils = require("core.utils")
local Window = require("ui.window")
local Column = require("ui.column")
local Row = require("ui.row")
local Frame = require("ui.frame")
local Button = require("ui.button")
local Table = require("ui.table")
local Separator = require("ui.separator")
local Select = require("ui.window_select")
local InputWindow = require("ui.window_input")
local builtins = require("reactor.builtins")
local reactorUtils = require("reactor.utils")
local function makeDefaultConfig()
return {
change = 'none'
}
end
local ItemWindow = class(Window)
function ItemWindow:init(super, config)
super.init()
self.config = utils.copy(config) or makeDefaultConfig()
self.tableCfg = {
showBorders = false,
columns = {
n = 2,
defaultWidth = 20,
[1] = {
selectable = false,
}
},
rows = {
n = 1
}
}
self:makeTableContents()
end
function ItemWindow:onLoad()
local list = Table(self.tableContents, self.tableCfg)
self.list = list
self.ui = Frame(
_T('item_config'),
Column({
self.list,
Separator.horizontal(),
Row({
Button(_T('ok')):action('clickedOk'),
Button(_T('cancel')):action('clickedCancel'),
}):size(nil, 1)
})
)
self.preferredSize = { w = 50, h = 8 }
end
function ItemWindow:makeTableContents()
local tableContents = {
{
{ display = _T('item_name') },
{ display = _T(self.config.name or 'not_configured'), action = 'editItem' }
},
{
{ display = _T('change_condition') },
{ display = reactorUtils.changeConditionDescription(self.config), action = 'editChangeCondition' }
},
}
self.tableContents = tableContents
if self.config.change == 'damage_less' then
table.insert(tableContents, {
{ display = _T('threshold') },
{ display = tostring(self.config.threshold), action = 'editThreshold' }
})
self.tableCfg.rows.n = 3
else
self.tableCfg.rows.n = 2
end
end
function ItemWindow:editItem()
local itemList = {}
for _, item in ipairs(builtins.items) do
table.insert(itemList, _T(item.id))
end
local win = Select:new(_T('item_name'), itemList)
self:present(
win,
function(result)
self.config.name = builtins.items[result].id
self:makeTableContents()
self.list.contents = self.tableContents
self.list:reload()
end
)
end
function ItemWindow:editChangeCondition()
local conditions = {
'none',
'damage_less',
}
local selectMode = Select:new(_T('redstone_mode'), {
_T('change_condition_none'),
_T('change_condition_damage_less'),
})
self:present(
selectMode,
function(result)
self.config.change = conditions[result]
if self.config.change == 'damage_less' then
self.config.threshold = 0.1
end
self:makeTableContents()
self.list.contents = self.tableContents
self.list:reload()
end
)
end
function ItemWindow:editThreshold()
local win = InputWindow:new(_T('threshold'), _T('input_prompt_threshold'))
self:present(
win,
function(result)
self.config.threshold = tonumber(result)
self:makeTableContents()
self.list.contents = self.tableContents
self.list:reload()
end
)
end
function ItemWindow:clickedOk()
self:dismiss(true, self.config)
end
function ItemWindow:clickedCancel()
self:dismiss(false)
end
return ItemWindow
end)
__bundle_register("reactor.window_transposer", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local utils = require("core.utils")
local Window = require("ui.window")
local Column = require("ui.column")
local Row = require("ui.row")
local Frame = require("ui.frame")
local Button = require("ui.button")
local Table = require("ui.table")
local Separator = require("ui.separator")
local Select = require("ui.window_select")
require("reactor.window+select_component")
local function makeDefaultConfig()
return {}
end
local TransposerWindow = class(Window)
function TransposerWindow:init(super, config)
super.init()
self.config = utils.copy(config) or makeDefaultConfig()
self.tableCfg = {
showBorders = false,
columns = {
n = 2,
defaultWidth = 20,
[1] = {
selectable = false,
}
},
rows = {
n = 1
}
}
self:makeTableContents()
end
function TransposerWindow:onLoad()
local list = Table(self.tableContents, self.tableCfg)
self.list = list
self.ui = Frame(
_T('transposer_config'),
Column({
self.list,
Separator.horizontal(),
Row({
Button(_T('ok')):action('clickedOk'),
Button(_T('cancel')):action('clickedCancel'),
}):size(nil, 1)
})
)
self.preferredSize = { w = 50, h = 8 }
end
function TransposerWindow:makeTableContents()
local tableContents = {
{
{ display = _T('component_address') },
{ display = self.config.address or _T('not_configured'), action = 'editAddress' }
},
{
{ display = _T('item_input_side') },
{ display = _T(utils.sideDescription(self.config.item_in or 0)), action = 'editDirection', value = 'item_in' }
},
{
{ display = _T('item_output_side') },
{ display = _T(utils.sideDescription(self.config.item_out or 0)), action = 'editDirection', value = 'item_out' }
},
{
{ display = _T('item_reactor_side') },
{ display = _T(utils.sideDescription(self.config.item_reactor or 0)), action = 'editDirection', value = 'item_reactor' }
}
}
self.tableContents = tableContents
self.tableCfg.rows.n = #tableContents
end
function TransposerWindow:editAddress()
self:selectComponent(
'transposer',
function(result)
self.config.address = result
self:makeTableContents()
self.list.contents = self.tableContents
self.list:reload()
end
)
end
function TransposerWindow:editDirection(field)
local selectSide = Select:new(_T('item_direction'), {
_T('down'),
_T('up'),
_T('north'),
_T('south'),
_T('west'),
_T('east')
})
self:present(
selectSide,
function(result)
self.config[field] = result - 1
self:makeTableContents()
self.list.contents = self.tableContents
self.list:reload()
end
)
end
function TransposerWindow:clickedOk()
self:dismiss(true, self.config)
end
function TransposerWindow:clickedCancel()
self:dismiss(false)
end
return TransposerWindow
end)
__bundle_register("reactor.window_redstone", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local utils = require("core.utils")
local Window = require("ui.window")
local Column = require("ui.column")
local Row = require("ui.row")
local Frame = require("ui.frame")
local Button = require("ui.button")
local Table = require("ui.table")
local Separator = require("ui.separator")
local Select = require("ui.window_select")
require("reactor.window+select_component")
local RedstoneWindow = class(Window)
function RedstoneWindow:init(super, config)
super.init()
self.config = utils.copy(config)
self.tableCfg = {
showBorders = false,
columns = {
n = 2,
defaultWidth = 20,
[1] = {
selectable = false,
}
},
rows = {
n = 1
}
}
self:makeTableContents()
end
function RedstoneWindow:onLoad()
local list = Table(self.tableContents, self.tableCfg)
self.list = list
self.ui = Frame(
_T('redstone_config'),
Column({
self.list,
Separator.horizontal(),
Row({
Button(_T('ok')):action('clickedOk'),
Button(_T('cancel')):action('clickedCancel'),
}):size(nil, 1)
})
)
self.preferredSize = { w = 50, h = 8 }
end
function RedstoneWindow:makeTableContents()
local tableContents = {}
self.tableContents = tableContents
-- mode
if not self.config then
table.insert(tableContents, {
{ display = _T('redstone_mode') },
{ display = _T('redstone_mode_none'), action = 'editRedstoneMode' }
})
self.tableCfg.rows.n = 1
return
end
table.insert(tableContents, {
{ display = _T('redstone_mode') },
{ display = _T('redstone_mode_' .. self.config.mode), action = 'editRedstoneMode' }
})
table.insert(tableContents, {
{ display = _T('component_address') },
{ display = self.config.address or _T('not_configured'), action = 'editRedstoneAddress' }
})
table.insert(tableContents, {
{ display = _T('redstone_direction') },
{ display = _T(utils.sideDescription(self.config.side)) or _T('not_configured'), action = 'editRedstoneSide' }
})
if self.config.mode == 'bundled' then
table.insert(tableContents, {
{ display = _T('redstone_color') },
{ display = _T(utils.colorDescription(self.config.color)) or _T('not_configured'), action = 'editRedstoneColor' }
})
self.tableCfg.rows.n = 4
else
self.tableCfg.rows.n = 3
end
end
function RedstoneWindow:editRedstoneMode()
local modes = {
'none',
'vanilla',
'bundled'
}
local selectMode = Select:new(_T('redstone_mode'), {
_T('redstone_mode_none'),
_T('redstone_mode_vanilla'),
_T('redstone_mode_bundled'),
})
self:present(
selectMode,
function(result)
if result == 1 then
self.config = nil
else
if not self.config then
self.config = {}
end
self.config.mode = modes[result]
end
self:makeTableContents()
self.list.contents = self.tableContents
self.list:reload()
end
)
end
function RedstoneWindow:editRedstoneAddress()
self:selectComponent(
'redstone',
function(result)
self.config.address = result
self:makeTableContents()
self.list.contents = self.tableContents
self.list:reload()
end
)
end
function RedstoneWindow:editRedstoneSide()
local selectSide = Select:new(_T('redstone_direction'), {
_T('down'),
_T('up'),
_T('north'),
_T('south'),
_T('west'),
_T('east')
})
self:present(
selectSide,
function(result)
self.config.side = result - 1
self:makeTableContents()
self.list.contents = self.tableContents
self.list:reload()
end
)
end
function RedstoneWindow:editRedstoneColor()
local selectColor = Select:new(_T('redstone_direction'), {
_T('white'),
_T('orange'),
_T('magenta'),
_T('lightblue'),
_T('yellow'),
_T('lime'),
_T('pink'),
_T('gray'),
_T('silver'),
_T('cyan'),
_T('purple'),
_T('blue'),
_T('brown'),
_T('green'),
_T('red'),
_T('black'),
})
self:present(
selectColor,
function(result)
self.config.color = result - 1
self:makeTableContents()
self.list.contents = self.tableContents
self.list:reload()
end
)
end
function RedstoneWindow:clickedOk()
self:dismiss(true, self.config)
end
function RedstoneWindow:clickedCancel()
self:dismiss(false)
end
return RedstoneWindow
end)
__bundle_register("ui.window_alert", function(require, _LOADED, __bundle_register, __bundle_modules)
local class = require("core.class")
local Window = require("ui.window")
local Frame = require("ui.frame")
local Column = require("ui.column")
local Row = require("ui.row")
local Button = require("ui.button")
local Label = require("ui.label")
local Alert = class(Window)
Alert.Ok = 0
Alert.Cancel = 1
function Alert:init(super, title, message, buttons)
super.init()
self.title = title or _T('alert_title')
self.message = message or _T('alert_message')
self.buttons = buttons or 0
end
function Alert:onLoad()
local buttons = Row()
if self.buttons & Alert.Ok == Alert.Ok then
buttons:addSubview(Button(_T('ok')):action('clickedOk'))
end
if self.buttons & Alert.Cancel == Alert.Cancel then
buttons:addSubview(Button(_T('cancel')):action('clickedCancel'))
end
self.ui = Frame(self.title, Column({
Label(self.message),
buttons
}))
self.preferredSize = { w = 50, h = 7 }
end
function Alert:clickedOk()
self:dismiss(Alert.Ok)
end
function Alert:clickedCancel()
self:dismiss(Alert.Cancel)
end
return Alert
end)
__bundle_register("core.i18n", function(require, _LOADED, __bundle_register, __bundle_modules)
local i18n = {}
local langTable = {}
function _T(k)
return langTable[k] or k
end
local baseModule
function i18n.reload(lang)
local langCode = lang or i18n.langList.default
for name, code in pairs(i18n.langList) do
if name ~= 'default' and code == langCode then
langCode = code
i18n.current = name
break
end
end
langTable = require(baseModule..'/lang_'..langCode)
end
function i18n.setup(base, lang)
baseModule = base
i18n.langList = require(base)
i18n.reload(lang)
end
return i18n
end)
__bundle_register("core.pal", function(require, _LOADED, __bundle_register, __bundle_modules)
local isOpenOS = false
if _G._OSVERSION then
if type(_OSVERSION) == 'string' then
if string.sub(_OSVERSION, 1, 6) == 'OpenOS' then
isOpenOS = true
end
end
end
palRequire = require
if not isOpenOS then
palRequire = function(moduleName)
return require('pal.' .. moduleName)
end
os.sleep = function() end
end
debugLog = function() end
end)
return __bundle_require("__root")