--[[ tutel host __ .,-;-;-,. /'_\ _/_/_/_|_\_\) / '-<_><_><_><_>=/\ `/_/====/_/-'\_\ "" "" "" ]] --[[ TO-DO List: * Add argument / funcion type checks on callback styled register functions * Make log screen scrolling work based on drawn lines not log indexes * Rename network commands to messages, to avoid confusion with console commands * Periodically check if turtle connections are valid ]] --[[ general variables ]] local HOST_CHANNEL = 1337 local TURTLE_CHANNEL_BASE = 1338 local MSG_HEADER = 0x747574656C local modem = peripheral.find("modem") or error("No modems found", 0) modem.open(HOST_CHANNEL) local monitor = peripheral.find("monitor") local scr = monitor or term local shouldRun = true --[[ helper functions ]] function wrapText(text, maxWidth) if not text then return {} end if maxWidth <= 0 then return { text } end local lines = {} local currentLine = "" for word, newline in text:gmatch("([^%s\n]*)(%s*\n?)") do if newline:find("\n") then if currentLine ~= "" then table.insert(lines, currentLine) currentLine = "" end if word ~= "" then table.insert(lines, word) else table.insert(lines, "") end elseif #currentLine + #word <= maxWidth then if currentLine == "" then currentLine = word else currentLine = currentLine .. " " .. word end else if currentLine ~= "" then table.insert(lines, currentLine) currentLine = word else while #word > maxWidth do table.insert(lines, word:sub(1, maxWidth)) word = word:sub(maxWidth + 1) end currentLine = word end end end if currentLine ~= "" then table.insert(lines, currentLine) end return lines end --[[ logging ]] local logTypes = { ["general"] = { ["BG"] = colours.green, ["FG"] = colours.white }, ["info"] = { ["BG"] = colours.blue, ["FG"] = colours.white }, ["warning"] = { ["BG"] = colours.orange, ["FG"] = colours.black, ["AffectContent"] = true }, ["error"] = { ["BG"] = colours.red, ["FG"] = colours.black, ["AffectContent"] = true } } local log = {} log.buffer = {} for typeName, format in pairs(logTypes) do log[typeName] = function(fmt, ...) local entry = { message = string.format(fmt, ...), type = typeName } table.insert(log.buffer, entry) end end --[[ turtle tracking ]] local turtles = {} function registerTurtle(pos, fuel, id) local channel = TURTLE_CHANNEL_BASE + #turtles local turt = { ["channel"] = channel, ["index"] = #turtles, ["position"] = pos, ["fuel"] = fuel, ["status"] = "IDLE", ["id"] = id } table.insert(turtles, turt) return turt end --[[ helper networking functions ]] function sendToAll(name, msgData) local message = { ["header"] = MSG_HEADER, ["name"] = name, ["data"] = msgData or {} } modem.transmit(0, 0, message) end --[[ command handling ]] local commands = {} function declareCommandHandler(name, handler, requiredKeys) commands[name] = { ["keys"] = requiredKeys or {}, ["handler"] = handler } end -- scan function handleScan(keys, id) local turt = registerTurtle({ keys.positionX, keys.positionY, keys.positionZ }, keys.fuel, id) log.info("Received turtle info (pos: %d, %d, %d | fuel: %d | channel: %d | id: %d)", turt.position[1], turt.position[2], turt.position[3], turt.fuel, turt.channel, id) sendToAll("REGISTER", { ["id"] = id, ["turtleChannel"] = turt.channel }) end declareCommandHandler("SCAN", handleScan, { ["positionX"] = "number", ["positionY"] = "number", ["positionZ"] = "number", ["fuel"] = "number" }) --[[ event handling ]] local eventListeners = {} function registerEventListener(name, listener) if not eventListeners[name] then eventListeners[name] = {} end table.insert(eventListeners[name], listener) end function dispatchEvents(event, ...) local eventTbl = eventListeners[event] if not eventTbl then return end for i = 1, #eventTbl, 1 do eventTbl[i](...) end end function handleMessages(side, channel, replyChannel, message, dist) if channel ~= HOST_CHANNEL then return end -- msg validation if type(message) ~= "table" then return end if not message.header or message.header ~= MSG_HEADER then return end if not message.data then message.data = {} end -- cmd validation if not message.name then log.warning("received nameless command?") return end if not commands[message.name] then log.error("unhandled command (%s)", message.name) return end local cmd = commands[message.name] for i, v in pairs(cmd.keys) do if type(message.data[i]) ~= v and v ~= "optional" then log.error("command %s requires key %s of type %s (was %s)", message.name, i, v, type(message[i])) return end end -- cmd execution local success, err = pcall(cmd.handler, message.data, message.id) if not success then log.error("error occurred while executing command %s: %s", message.name, err) end log.info("processed command (%s)", message.name) end registerEventListener("modem_message", handleMessages) --[[ key handling ]] local GLFW_KEY_DOWN = 264 local GLFW_KEY_UP = 265 local GLFW_KEY_ENTER = 257 local GLFW_KEY_BACKSPACE = 259 local logOffset = 0 function scrollLogs(key, isHeld) if key == GLFW_KEY_DOWN then logOffset = logOffset - 1 elseif key == GLFW_KEY_UP then logOffset = logOffset + 1 end end registerEventListener("key", scrollLogs) --[[ console ]] local consoleCommands = {} local commandBuffer = "" function registerConsoleCommand(name, handler, desc) local cmd = { ["name"] = name, ["handler"] = handler, ["desc"] = desc or "" } consoleCommands[name] = cmd end function processCommand() local cmd local args = {} for word in commandBuffer:gmatch("%S+") do if not cmd then cmd = word else table.insert(args, word) end end if consoleCommands[cmd] then log.info("> %s %s", cmd, table.concat(args, " ")) consoleCommands[cmd].handler(args) else log.warning("Unknown command: %s", cmd) end commandBuffer = "" end function handleConsoleInput(char) commandBuffer = commandBuffer .. char end registerEventListener("char", handleConsoleInput) function handleConsoleKeys(key) if key == GLFW_KEY_ENTER and commandBuffer:len() > 0 then processCommand() elseif key == GLFW_KEY_BACKSPACE then local len = commandBuffer:len() if len >= 1 then commandBuffer = commandBuffer:sub(0, len - 1) end end end registerEventListener("key", handleConsoleKeys) --[[ console commands ]] function echoCommand(args) log.info(table.concat(args, " ")) end registerConsoleCommand("echo", echoCommand, "Prints your message") function exitCommand(args) shouldRun = false end registerConsoleCommand("exit", exitCommand, "Exits the program") function scanCommand(args) log.info("broadcasting SCAN") sendToAll("SCAN", { ["hostChannel"] = HOST_CHANNEL }) end registerConsoleCommand("scan", scanCommand, "Sends out a scan command to pair unlinked turtles") function clearCommand() log.buffer = {} end registerConsoleCommand("clear", clearCommand, "Clears the console") function turtlesCommand() if #turtles == 0 then log.info("no turtles registered") return end for i = 1, #turtles, 1 do local turt = turtles[i] local turtleMessage = ("coords: %d, %d, %d - fuel: %d - channel: %d - id: %d - status: %s"):format( turt.position[1], turt.position[2], turt.position[3], turt.fuel, turt.channel, turt.id, turt.status) log.info(turtleMessage) end end registerConsoleCommand("turtles", turtlesCommand, "Lists all the linked turtles") function helpCommand() log.info("----- commands -----") for _, cmd in pairs(consoleCommands) do log.info("%s - %s", cmd.name, cmd.desc) end end registerConsoleCommand("help", helpCommand, "Prints out all the console commands") --[[ drawing functions ]] function drawLogs(x0, y0, x1, y1) local w = x1 - x0 local h = y1 - y0 if #log.buffer == 0 then return end if logOffset < 0 then logOffset = 0 end if logOffset >= #log.buffer then logOffset = #log.buffer - 1 end if #log.buffer == 0 then logOffset = 0 end local drawnLines = 0 for i = #log.buffer - logOffset, 1, -1 do if drawnLines > h then break end local entry = log.buffer[i] local format = logTypes[entry.type] local prevColourBg = scr.getBackgroundColour() local prevColourFg = scr.getTextColour() local prefix = ("[%s]"):format(entry.type) local prefixLength = #prefix local maxMessageWidth = math.max(1, w - prefixLength) local wrappedMessage = wrapText(tostring(entry.message), maxMessageWidth) local messageBG, messageFG if format.AffectContent then messageBG = format.BG messageFG = format.FG else messageBG = prevColourBg messageFG = prevColourFg end for lineIdx = #wrappedMessage, 1, -1 do if drawnLines > h then break end local line = wrappedMessage[lineIdx] scr.setCursorPos(1, y1 - drawnLines) if lineIdx == 1 then scr.setBackgroundColor(format.BG) scr.setTextColor(format.FG) scr.write(prefix) else scr.setBackgroundColor(messageBG) scr.write((" "):rep(prefixLength)) end scr.setBackgroundColor(messageBG) scr.setTextColor(messageFG) scr.write(" ") scr.write(line) local remaining = w - (prefixLength + #line) if remaining > 0 then scr.write((" "):rep(remaining)) end drawnLines = drawnLines + 1 end scr.setBackgroundColor(prevColourBg) scr.setTextColor(prevColourFg) end end function drawStatusBar(y) scr.setCursorPos(1, y) scr.setBackgroundColor(colors.gray) scr.setTextColor(colors.white) scr.clearLine() local str = ("turtles: %d"):format(#turtles) scr.write(str) end function drawCommandBar(w, y) scr.setCursorPos(1, y) scr.setBackgroundColor(colors.gray) scr.setTextColor(colors.white) scr.clearLine() scr.write("$ ") local startingIndex = commandBuffer:len() - w + 4 if startingIndex < 0 then startingIndex = 0 end scr.write(commandBuffer:sub(startingIndex)) scr.setCursorBlink(true) end --[[ main loop functions ]] function updateScreen() local w, h = scr.getSize() local function drawTextCentered(text) local x = (w / 2) - (text:len() / 2) local _, curY = scr.getCursorPos() scr.setCursorPos(x, curY) scr.write(text) end -- reset state and clear screen scr.setBackgroundColor(colors.black) scr.clear() -- -- title bar scr.setCursorPos(1, 1) scr.setBackgroundColor(colors.lightGray) scr.setTextColour(colors.black) scr.clearLine() drawTextCentered("tutel host controller") scr.setBackgroundColor(colors.black) scr.setTextColour(colors.white) drawLogs(1, 2, w, h - 1) drawStatusBar(2) drawCommandBar(w, h) end function pollEvents() local eventData = { os.pullEvent() } local eventName = eventData[1] table.remove(eventData, 1) dispatchEvents(eventName, table.unpack(eventData)) end --[[ main ]] log.general("init") while shouldRun do updateScreen() pollEvents() end scr.clear() scr.setCursorBlink(false)