Files
scripts/lua/CC Tweaked/tutel/tutel-host.lua
T
2025-05-08 13:51:59 -03:00

498 lines
11 KiB
Lua

--[[
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
]]
--[[
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)
consoleCommands[name] = handler
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](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)
function exitCommand(args)
shouldRun = false
end
registerConsoleCommand("exit", exitCommand)
function scanCommand(args)
log.info("broadcasting SCAN")
sendToAll("SCAN", { ["hostChannel"] = HOST_CHANNEL })
end
registerConsoleCommand("scan", scanCommand)
function clearCommand()
log.buffer = {}
end
registerConsoleCommand("clear", clearCommand)
--[[
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)