diff --git a/README.md b/README.md index df07188..ef6420d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,12 @@ -# cc-rail-router -Scripts for Computercraft-managed minecart railways. +# CC-Rail-Router + +A system for using Computercraft to control minecart junctions and routing. It works by having a traveling portable computer send out messages as it's traveling, so that nearby managed switches are set automatically. + +The portable computer, usually held by the traveling player, will send out a message containing its route as a list of strings, like so: +```lua +{ "A", "C", "D", "V" } +``` + +The switch controller will reply then look for any pairs of branch segments that it has control of, and will switch them accordingly. So suppose we have a switch that connects line `C` to line `D`, then upon receiving the above message, it'll change its signal outputs to route trains accordingly from `C` to `D`. + +With this design, the switch controller and portable computer are completely stateless, and no two-way communication is needed. diff --git a/router.lua b/router.lua new file mode 100644 index 0000000..8571aae --- /dev/null +++ b/router.lua @@ -0,0 +1,22 @@ +--[[ +This program should be installed on a portable computer with a wireless +modem, to act as a routing beacon in conjunction with managed switches. +]]-- +local modem = peripheral.wrap("back") or error("Missing modem.") + +local args = {...} + +local route = args +print("Routing via:") +for _, branch in pairs(route) do + print(" "..branch) +end + +local function sendRoute(route) + modem.transmit(0, 42, route) +end + +while true do + sendRoute(route) + os.sleep(1) +end diff --git a/switcher.lua b/switcher.lua new file mode 100644 index 0000000..2416baf --- /dev/null +++ b/switcher.lua @@ -0,0 +1,202 @@ +--[[ +This program is to be installed on a computer that controls a single rail +junction. As a player with a portable computer approaches, that portable +computer will be sending out a signal indicating their preferred switching +configuration (the branch they're coming from, and the one they want to go to), +and the junction's computer will then send a success reply. +]] + +local CONFIG_FILE = "switch_config.tbl" +local CHANNEL = 0 +local config = nil + +local modem = peripheral.wrap("top") or error("Missing modem") +if not modem.isWireless() then error("Wireless modem required") end +modem.open(CHANNEL) + +term.clear() +term.setCursorPos(1, 1) +print("Receiving routing commands on channel 0") + +-- Series of guided inputs for building a configuration file from user input. +local function configSetupWizard() + local cfg = {range = 32, switches = {}} + term.clear() + term.setCursorPos(1, 1) + print("Switch Controller Config Setup Wizard") + print("-------------------------------------") + print("What range (blocks) does this switch have? Defaults to 32.") + local rangeStr = io.read() + if rangeStr then + local rangeInt = tonumber(rangeStr) + if rangeInt == nil or rangeInt < 1 or rangeInt > 128 then + print("Invalid range value. Should be a positive integer number. Got "..rangeStr) + return nil + end + end + print("How many switch configurations are there? Usually 1 for each possible path of travel.") + local switchCountStr = io.read() + if not switchCountStr then + print("Invalid switch configuration count.") + return nil + end + local switchCount = tonumber(switchCountStr) + if switchCount == nil or switchCount < 2 or switchCount > 32 then + print("Invalid switch configuration count. Expected a positive integer between 2 and 32.") + return nil + end + for i = 1, switchCount do + local sw = {from = nil, to = nil, controls = {}} + print("Switch Configuration #"..i..":") + print("What is name of the branch traffic comes from?") + sw.from = io.read() + print("What is the name of the branch traffic goes to?") + sw.to = io.read() + print("How many control points are needed for this switch configuration?") + local controlCountStr = io.read() + local controlCount = 0 + if controlCountStr then controlCount = tonumber(controlCountStr) end + if not controlCount or controlCount < 1 then + print("Invalid control point count. Expected a positive integer.") + return nil + end + for c = 1, controlCount do + local ctl = {type = "redstone", side = "top", state = true} + print(" Control Point #"..c..":") + print(" What type of control is this?") + print(" 1. redstone") + print(" 2. redstoneIntegrator") + local typeStr = io.read() + if not typeStr or typeStr == "1" or typeStr == "redstone" then + ctl.type = "redstone" + print(" What side is redstone output on?") + ctl.side = io.read() + print(" What state should the output be? [T/F]") + local stateStr = io.read() + ctl.state = stateStr == "T" or stateStr == "True" or stateStr == "t" or stateStr == "true" + elseif typeStr == "2" or typeStr == "redstoneIntegrator" then + ctl.type = "redstoneIntegrator" + print(" What is the peripheral name?") + ctl.name = io.read() + print(" What side is output on?") + ctl.side = io.read() + print(" What state should the output be? [T/F]") + local stateStr = io.read() + ctl.state = stateStr == "T" or stateStr == "True" or stateStr == "t" or stateStr == "true" + else + print(" Unsupported control type.") + return nil + end + table.insert(sw.controls, ctl) + end + table.insert(config.switches, sw) + end + local f = io.open(CONFIG_FILE, "w") + f:write(textutils.serialize(cfg)) + f:close() + return cfg +end + +local function loadConfig() + if fs.exists(CONFIG_FILE) then + local f = io.open(CONFIG_FILE, "r") + local cfg = textutils.unserialize(f:read("*a")) + f:close() + print("Loaded configuration from file:") + print(" "..tostring(#config.switches).." switch configurations.") + print(" "..tostring(config.range).." block range.") + return cfg + else + print("File "..CONFIG_FILE.." doesn't exist. Start setup wizard? [y/n]") + local response = io.read() + if response == "y" or response == "Y" or response == "yes" then + return configSetupWizard() + else + return nil + end + end +end + +local config = {range=32, switches={}} +if fs.exists(CONFIG_FILE) then + local f = io.open(CONFIG_FILE, "r") + config = textutils.unserialize(f:read("*a")) + f:close() + print("Loaded config:") + print(" "..tostring(#config.switches).." switch configurations") + print(" Range of "..tostring(config.range).." blocks") +else + local f = io.open(CONFIG_FILE, "w") + f:write(textutils.serialize(config)) + f:close() + print("Saved config to "..CONFIG_FILE) +end + +local function findSwitchConfiguration(cfg, from, to) + if from == nil or to == nil then return nil end + for _, sw in pairs(cfg.switches) do + if sw.from == from and sw.to == to then return sw end + end + return nil end +end + +local function isSwitchConfigurationActive(sw) + for _, control in pairs(sw.controls) do + if control.type == "redstone" then + local state = redstone.getOutput(control.side) + if state ~= control.state then return false end + elseif control.type == "redstoneIntegrator" then + local state = peripheral.call(control.name, "getOutput", control.side) + if state ~= control.state then return false end + else + error("Invalid control type: "..control.type) + end + end + return true +end + +local function activateSwitchConfiguration(sw) + print("Activating switch: FROM="..sw.from..", TO="..sw.to) + for _, control in pairs(sw.controls) do + if control.type == "redstone" then + redstone.setOutput(control.side, control.state) + elseif control.type == "redstoneIntegrator" then + peripheral.call(control.name, "setOutput", control.side, control.state) + else + error("Invalid control type: "..control.type) + end + end +end + +-- Handles incoming rail messages that consist of a list of branch names +-- that the user would like to traverse. +local function handleModemMsg(replyChannel, msg) + -- Ignore invalid messages. + if not msg or #msg < 2 then return end + -- Find the switch configuration(s) that pertain to this route. + for i = 1, #msg - 1 do + local sw = findSwitchConfiguration(config, msg[i], msg[i+1]) + if sw and not isSwitchConfigurationActive(sw) then + activateSwitchConfiguration(sw) + end + end +end + +config = loadConfig() +if not config then + print("Unable to load configuration. Exiting.") + return +end + +while true do + local event, p1, p2, p3, p4, p5 = os.pullEvent() + if event == "modem_message" then + local channel = p2 + local replyChannel = p3 + local msg = p4 + local dist = p5 + if channel == CHANNEL and dist ~= nil and dist < config.range and msg ~= nil then + handleModemMsg(replyChannel, msg) + end + end +end \ No newline at end of file