--[[ tutel host __ .,-;-;-,. /'_\ _/_/_/_|_\_\) / '-<_><_><_><_>=/\ `/_/====/_/-'\_\ "" "" "" ]] --[[ network 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) --[[ 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 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 if logOffset < 0 then logOffset = 0 end if logOffset >= #log.buffer then logOffset = #log.buffer - 1 end end registerEventListener("key", scrollLogs) --[[ console ]] local commandBuffer = "" function drawConsole(y) end function handleConsoleInput(char) commandBuffer = commandBuffer..char end registerEventListener("char", handleConsoleInput) function handleConsoleKeys(key) if key == GLFW_KEY_ENTER then -- executeCommand(commandBuffer) log.info("Executed command: %s", commandBuffer) commandBuffer = "" end end registerEventListener("key", handleConsoleKeys) --[[ drawing functions ]] function drawLogs(x0, y0, x1, y1) local w = x1 - x0 local h = y1 - y0 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 = term.getBackgroundColour() local prevColourFg = term.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] term.setCursorPos(1, y1 - drawnLines) if lineIdx == 1 then term.setBackgroundColor(format.BG) term.setTextColor(format.FG) term.write(prefix) else term.setBackgroundColor(messageBG) term.write((" "):rep(prefixLength)) end term.setBackgroundColor(messageBG) term.setTextColor(messageFG) term.write(" ") term.write(line) local remaining = w - (prefixLength + #line) if remaining > 0 then term.write((" "):rep(remaining)) end drawnLines = drawnLines + 1 end term.setBackgroundColor(prevColourBg) term.setTextColor(prevColourFg) end end function drawStatusBar(y) term.setCursorPos(1, y) term.setBackgroundColor(colors.gray) term.setTextColor(colors.white) term.clearLine() local str = ("turtles: %d"):format(#turtles) term.write(str) end function drawCommandBar(w, y) term.setCursorPos(0, y) term.clearLine() term.setBackgroundColor(colors.white) term.setTextColor(colors.black) term.write("$ ") local startingIndex = commandBuffer:len() - w - 3 if startingIndex < 0 then startingIndex = 0 end term.write(commandBuffer:sub(startingIndex)) end --[[ main loop functions ]] function updateScreen() local w, h = term.getSize() local function drawTextCentered(text) local x = (w / 2) - (text:len() / 2) local _, curY = term.getCursorPos() term.setCursorPos(x, curY) term.write(text) end -- reset state and clear screen term.setBackgroundColor(colors.black) term.clear() -- -- title bar term.setCursorPos(1, 1) term.setBackgroundColor(colors.lightGray) term.setTextColour(colors.black) term.clearLine() drawTextCentered("tutel host controller") term.setBackgroundColor(colors.black) term.setTextColour(colors.white) drawLogs(1, 2, w, h - 2) 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("tutel host init") log.info("broadcasting SCAN") sendToAll("SCAN", { ["hostChannel"] = HOST_CHANNEL }) while true do updateScreen() pollEvents() end