From 621e13d8bed7215a076e505eba998c7a0ec38519 Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Sun, 15 Nov 2020 13:37:32 -0500 Subject: New program system; Capitalized folders --- Programs/Enchat.bup/Contents/Info.meta | 1 + .../Contents/Resources/resources_here.txt | 1 + Programs/Enchat.bup/Contents/bits-UI/enchat3.lua | 2479 ++++++++++++++++++++ 3 files changed, 2481 insertions(+) create mode 100644 Programs/Enchat.bup/Contents/Info.meta create mode 100644 Programs/Enchat.bup/Contents/Resources/resources_here.txt create mode 100644 Programs/Enchat.bup/Contents/bits-UI/enchat3.lua (limited to 'Programs/Enchat.bup/Contents') diff --git a/Programs/Enchat.bup/Contents/Info.meta b/Programs/Enchat.bup/Contents/Info.meta new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/Programs/Enchat.bup/Contents/Info.meta @@ -0,0 +1 @@ + diff --git a/Programs/Enchat.bup/Contents/Resources/resources_here.txt b/Programs/Enchat.bup/Contents/Resources/resources_here.txt new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/Programs/Enchat.bup/Contents/Resources/resources_here.txt @@ -0,0 +1 @@ + diff --git a/Programs/Enchat.bup/Contents/bits-UI/enchat3.lua b/Programs/Enchat.bup/Contents/bits-UI/enchat3.lua new file mode 100644 index 0000000..b66549e --- /dev/null +++ b/Programs/Enchat.bup/Contents/bits-UI/enchat3.lua @@ -0,0 +1,2479 @@ +--[[ + Enchat 3.0 + Get with: + wget https://github.com/LDDestroier/enchat/raw/master/enchat3.lua enchat3.lua + +This is a stable release. You fool! +--]] + +local scr_x, scr_y = term.getSize() +CHATBOX_SAFEMODE = nil + +-- non-changable settings +enchat = { + connectToSkynet = true, + version = 3.0, + isBeta = false, + port = 11000, + skynetPort = "enchat3-default", + url = "https://github.com/LDDestroier/enchat/raw/master/enchat3.lua", + betaurl = "https://github.com/LDDestroier/enchat/raw/beta/enchat3.lua", + ignoreModem = false, + dataDir = "/.enchat", + useChatbox = false, + disableChatboxWithRedstone = false, +} + +-- changable settings +local enchatSettings = { -- DEFAULT settings. + animDiv = 4, -- divisor of text animation speed (scrolling from left) + doAnimate = true, -- whether or not to animate text moving from left side of screen + reverseScroll = false, -- whether or not to make scrolling up really scroll down + redrawDelay = 0.1, -- delay between redrawing + useSetVisible = false, -- whether or not to use term.current().setVisible(), which has performance and flickering improvements + pageKeySpeed = 8, -- how far PageUP or PageDOWN should scroll + doNotif = true, -- whether or not to use oveerlay glasses for notifications, if possible + doKrazy = true, -- whether or not to add &k obfuscation + useSkynet = true, -- whether or not to use gollark's Skynet in addition to modem calls + extraNewline = true, -- adds an extra newline after every message since setting to true + acceptPictoChat = true, -- whether or not to allow tablular enchat input, which is what /picto uses + noRepeatNames = true, -- whether or not to display the username in two or more consecutive messages by the same user +} + +-- colors for various elements +palette = { + bg = colors.black, + txt = colors.white, + promptbg = colors.gray, + prompttxt = colors.white, + scrollMeter = colors.lightGray, + chevron = colors.black, + title = colors.lightGray, + titlebg = colors.gray, +} + +-- UI adjustments, used to emulate the appearance of other chat programs +UIconf = { + promptY = 1, + chevron = ">", + chatlogTop = 1, + title = "Enchat 3", + doTitle = false, + titleY = 1, + nameDecolor = false, + centerTitle = true, + prefix = "<", + suffix = "> " +} + +-- Attempt to get some slight optimization through localizing basic functions. +local mathmax, mathmin, mathrandom = math.max, math.min, math.random +local termblit, termwrite = term.blit, term.write +local termsetCursorPos, termgetCursorPos, termsetCursorBlink = term.setCursorPos, term.getCursorPos, term.setCursorBlink +local termsetTextColor, termsetBackgroundColor = term.setTextColor, term.setBackgroundColor +local termgetTextColor, termgetBackgroundColor = term.getTextColor, term.getBackgroundColor +local termclear, termclearLine = term.clear, term.clearLine +local tableinsert, tableremove, tableconcat = table.insert, table.remove, table.concat +local textutilsserialize, textutilsunserialize = textutils.serialize, textutils.unserialize +local stringsub, stringgsub, stringrep = string.sub, string.gsub, string.rep +local unpack = unpack +-- This better do something. + +local initcolors = { + bg = termgetBackgroundColor(), + txt = termgetTextColor() +} + +local tArg = {...} + +local yourName = tArg[1] +local encKey = tArg[2] + +local setEncKey = function(newKey) + encKey = newKey +end + +local saveSettings = function() + local file = fs.open(fs.combine(enchat.dataDir, "settings"), "w") + file.write( + textutilsserialize({ + enchatSettings = enchatSettings, + palette = palette, + UIconf = UIconf, + }) + ) + file.close() +end + +local loadSettings = function() + local contents + if not fs.exists(fs.combine(enchat.dataDir, "settings")) then + saveSettings() + end + local file = fs.open(fs.combine(enchat.dataDir, "settings"), "r") + contents = file.readAll() + file.close() + local newSettings = textutilsunserialize(contents) + if newSettings then + for k,v in pairs(newSettings.enchatSettings) do + enchatSettings[k] = v + end + for k,v in pairs(newSettings.palette) do + palette[k] = v + end + for k,v in pairs(newSettings.UIconf) do + UIconf[k] = v + end + else + saveSettings() + end +end + +local updateEnchat = function(doBeta) + local pPath = shell.getRunningProgram() + local h = http.get((doBeta or enchat.isBeta) and enchat.betaurl or enchat.url) + if not h then + return false, "Could not connect." + else + local content = h.readAll() + local file = fs.open(pPath, "w") + file.write(content) + file.close() + return true, "Updated!" + end +end + +-- disables chat screen updating +local pauseRendering = true + +-- primarily for use when using the pallete command, hoh hoh +local colors_strnames = { + ["white"] = colors.white, + ["pearl"] = colors.white, + ["silver"] = colors.white, + ["aryan"] = colors.white, + ["#f0f0f0"] = colors.white, + + ["orange"] = colors.orange, + ["carrot"] = colors.orange, + ["fuhrer"] = colors.orange, + ["pumpkin"] = colors.orange, + ["#f2b233"] = colors.orange, + + ["magenta"] = colors.magenta, + ["hotpink"] = colors.magenta, + ["lightpurple"] = colors.magenta, + ["light purple"] = colors.magenta, + ["#e57fd8"] = colors.magenta, + + ["lightblue"] = colors.lightBlue, + ["light blue"] = colors.lightBlue, + ["skyblue"] = colors.lightBlue, + ["#99b2f2"] = colors.lightBlue, + + ["yellow"] = colors.yellow, + ["piss"] = colors.yellow, + ["pee"] = colors.yellow, + ["lemon"] = colors.yellow, + ["spongebob"] = colors.yellow, + ["cowardice"] = colors.yellow, + ["#dede6c"] = colors.yellow, + + ["lime"] = colors.lime, + ["lightgreen"] = colors.lime, + ["light green"] = colors.lime, + ["slime"] = colors.lime, + ["radiation"] = colors.lime, + ["#7fcc19"] = colors.lime, + + ["pink"] = colors.pink, + ["lightishred"] = colors.pink, + ["lightish red"] = colors.pink, + ["communist"] = colors.pink, + ["commie"] = colors.pink, + ["patrick"] = colors.pink, + ["#f2b2cc"] = colors.pink, + + ["gray"] = colors.gray, + ["grey"] = colors.gray, + ["graey"] = colors.gray, + ["gunmetal"] = colors.gray, + ["#4c4c4c"] = colors.gray, + + ["lightgray"] = colors.lightGray, + ["lightgrey"] = colors.lightGray, + ["light gray"] = colors.lightGray, + ["light grey"] = colors.lightGray, + ["#999999"] = colors.lightGray, + + ["cyan"] = colors.cyan, + ["seawater"] = colors.cyan, + ["brine"] = colors.cyan, + ["#4c99b2"] = colors.cyan, + + ["purple"] = colors.purple, + ["purble"] = colors.purple, + ["obsidian"] = colors.purple, + ["diviner"] = colors.purple, + ["#b266e5"] = colors.purple, + + ["blue"] = colors.blue, + ["blu"] = colors.blue, + ["azure"] = colors.blue, + ["sapphire"] = colors.blue, + ["lapis"] = colors.blue, + ["volnutt"] = colors.blue, + ["blueberry"] = colors.blue, + ["x"] = colors.blue, + ["megaman"] = colors.blue, + ["#3366bb"] = colors.blue, + + ["brown"] = colors.brown, + ["shit"] = colors.brown, + ["dirt"] = colors.brown, + ["mud"] = colors.brown, + ["bricks"] = colors.brown, + ["#7f664c"] = colors.brown, + + ["green"] = colors.green, + ["grass"] = colors.green, + ["#57a64e"] = colors.green, + + ["red"] = colors.red, + ["crimson"] = colors.red, + ["vermillion"] = colors.red, + ["menstration"] = colors.red, + ["blood"] = colors.red, + ["marinara"] = colors.red, + ["zero"] = colors.red, + ["protoman"] = colors.red, + ["communism"] = colors.red, + ["#cc4c4c"] = colors.red, + + ["black"] = colors.black, + ["dark"] = colors.black, + ["darkness"] = colors.black, + ["space"] = colors.black, + ["coal"] = colors.black, + ["onyx"] = colors.black, + ["#191919"] = colors.black, +} + +local toblit = { + [0] = " ", + [1] = "0", + [2] = "1", + [4] = "2", + [8] = "3", + [16] = "4", + [32] = "5", + [64] = "6", + [128] = "7", + [256] = "8", + [512] = "9", + [1024] = "a", + [2048] = "b", + [4096] = "c", + [8192] = "d", + [16384] = "e", + [32768] = "f" +} +local tocolors = {} +for k,v in pairs(toblit) do + tocolors[v] = k +end + +local codeNames = { + ["r"] = "reset", -- Sets either the text (&) or background (~) colors to their original color. + ["{"] = "stopFormatting", -- Toggles formatting text off + ["}"] = "startFormatting", -- Toggles formatting text on + ["k"] = "krazy" -- Makes the font kuh-razy! +} + +-- indicates which character should turn into which random &k character +local kraziez = { + ["l"] = { + "!", + "l", + "1", + "|", + "i", + "I", + ":", + ";", + }, + ["m"] = { + "M", + "W", + "w", + "m", + "X", + "N", + "_", + "%", + "@", + }, + ["all"] = {} +} + +for a = 1, #kraziez["l"] do + kraziez[kraziez["l"][a]] = kraziez["l"] +end +for k,v in pairs(kraziez) do + for a = 1, #v do + kraziez[kraziez[k][a]] = v + end +end + +-- check if using older CC version, and omit special characters if it's too old to avoid crash +if tonumber(_CC_VERSION or 0) >= 1.76 then + for a = 1, 255 do + if (a ~= 32) and (a ~= 13) and (a ~= 10) then + kraziez["all"][#kraziez["all"]+1] = string.char(a) + end + end +else + for a = 33, 126 do + kraziez["all"][#kraziez["all"]+1] = string.char(a) + end +end + +local makeRandomString = function(length, begin, stop) + local output = "" + for a = 1, length do + output = output .. string.char(math.random(begin or 1, stop or 255)) + end + return output +end + +local personalID = makeRandomString(64, 32, 128) + +local explode = function(div, str, replstr, includeDiv) + if (div == '') then + return false + end + local pos, arr = 0, {} + for st, sp in function() return string.find(str, div, pos, false) end do + tableinsert(arr, string.sub(replstr or str, pos, st - 1 + (includeDiv and #div or 0))) + pos = sp + 1 + end + tableinsert(arr, string.sub(replstr or str, pos)) + return arr +end + +local parseKrazy = function(c) + if kraziez[c] then + return kraziez[c][mathrandom(1, #kraziez[c])] + else + return kraziez.all[mathrandom(1, #kraziez.all)] + end +end + +-- my main man, the function that turns unformatted strings into formatted strings +local textToBlit = function(input, onlyString, initText, initBack, checkPos, useJSONformat) + if not input then return end + checkPos = checkPos or -1 + initText, initBack = initText or toblit[term.getTextColor()], initBack or toblit[term.getBackgroundColor()] + tcode, bcode = "&", "~" + local cpos, cx = 0, 0 + local skip, ignore, ex = nil, false, nil + local text, back, nex = initText, initBack, nil + + local charOut, textOut, backOut = {}, {}, {} + local JSONoutput = {} + + local krazy = false + local bold = false + local strikethrough = false + local underline = false + local italic = false + + local codes = {} + codes["r"] = function(prev) + if not ignore then + if prev == tcode then + text = initText + bold = false + strikethrough = false + underline = false + italic = false + elseif prev == bcode then + if useJSONformat then + return 0 + else + back = initBack + end + end + krazy = false + else + return 0 + end + end + codes["k"] = function(prev) + if not ignore then + krazy = not krazy + else + return 0 + end + end + codes["{"] = function(prev) + if not ignore then + ignore = true + else + return 0 + end + end + codes["}"] = function(prev) + if ignore then + ignore = false + else + return 0 + end + end + + if useJSONformat then + codes["l"] = function(prev) + bold = true + end + codes["m"] = function(prev) + strikethrough = true + end + codes["n"] = function(prev) + underline = true + end + codes["o"] = function(prev) + italic = true + end + end + + local sx, str = 0 + input = stringgsub(input, "(\\)(%d%d?%d?)", function(cap, val) + if tonumber(val) < 256 then + cpos = cpos - #val + return string.char(val) + else + return cap..val + end + end) + + local MCcolors = { + ["0"] = "white", + ["1"] = "gold", + ["2"] = "light_purple", + ["3"] = "aqua", + ["4"] = "yellow", + ["5"] = "green", + ["6"] = "light_purple", + ["7"] = "dark_gray", + ["8"] = "gray", + ["9"] = "dark_aqua", + ["a"] = "dark_purple", + ["b"] = "dark_blue", + ["c"] = "gold", + ["d"] = "dark_green", + ["e"] = "red", + ["f"] = "black", + } + + for cx = 1, #input do + str = stringsub(input,cx,cx) + if skip then + if tocolors[str] and not ignore then + if skip == tcode then + text = str == " " and initText or str + if sx < checkPos then + cpos = cpos - 2 + end + elseif skip == bcode then + back = str == " " and initBack or str + if sx < checkPos then + cpos = cpos - 2 + end + end + elseif codes[str] and not (ignore and str == "{") then + ex = codes[str](skip) or 0 + sx = sx + ex + if sx < checkPos then + cpos = cpos - ex - 2 + end + else + sx = sx + 1 + if useJSONformat then + JSONoutput[sx] = { + text = (skip..str), + color = onlyString and "f" or MCcolors[text], + bold = (not onlyString) and bold, + italic = (not onlyString) and italic, + underline = (not onlyString) and underline, + obfuscated = (not onlyString) and krazy, + strikethrough = (not onlyString) and strikethrough + } + else + charOut[sx] = krazy and parseKrazy(prev..str) or (skip..str) + textOut[sx] = stringrep(text,2) + backOut[sx] = stringrep(back,2) + end + end + skip = nil + else + if (str == tcode or str == bcode) and (codes[stringsub(input, 1+cx, 1+cx)] or tocolors[stringsub(input,1+cx,1+cx)]) then + skip = str + else + sx = sx + 1 + if useJSONformat then + JSONoutput[sx] = { + text = str, + color = onlyString and "f" or MCcolors[text], + bold = (not onlyString) and bold, + italic = (not onlyString) and italic, + underline = (not onlyString) and underline, + obfuscated = (not onlyString) and krazy, + strikethrough = (not onlyString) and strikethrough + } + else + charOut[sx] = krazy and parseKrazy(str) or str + textOut[sx] = text + backOut[sx] = back + end + end + end + end + if useJSONformat then + return textutils.serializeJSON(JSONoutput) + else + if onlyString then + return tableconcat(charOut), (checkPos > -1) and cpos or nil + else +-- return {tableconcat(charOut), tableconcat(textOut):gsub(" ", initText), tableconcat(backOut):gsub(" ", initBack)}, (checkPos > -1) and cpos or nil + return {tableconcat(charOut), tableconcat(textOut), tableconcat(backOut)}, (checkPos > -1) and cpos or nil + end + end +end +_G.textToBlit = textToBlit + +-- convoluted read function that renders color codes as they are written. +-- struggles with \123 codes, but hey, fuck you +local colorRead = function(maxLength, _history) + local output = "" + local history, _history = {}, _history or {} + for a = 1, #_history do + history[a] = _history[a] + end + history[#history+1] = "" + local hPos = #history + local cx, cy = termgetCursorPos() + local x, xscroll = 1, 1 + local ctrlDown = false + termsetCursorBlink(true) + local evt, key, bout, xmod, timtam + while true do + termsetCursorPos(cx, cy) + bout, xmod = textToBlit(output, false, nil, nil, x) + for a = 1, #bout do + bout[a] = stringsub(bout[a], xscroll, xscroll + scr_x - cx) + end + termblit(unpack(bout)) + termwrite((" "):rep(scr_x - cx)) + termsetCursorPos(cx + x + xmod - xscroll, cy) + evt = {os.pullEvent()} + if evt[1] == "char" or evt[1] == "paste" then + output = (output:sub(1, x-1)..evt[2]..output:sub(x)):sub(1, maxLength or -1) + x = mathmin(x + #evt[2], #output+1) + elseif evt[1] == "key" then + key = evt[2] + if key == keys.leftCtrl then + ctrlDown = true + elseif key == keys.left then + x = mathmax(x - 1, 1) + elseif key == keys.right then + x = mathmin(x + 1, #output+1) + elseif key == keys.backspace then + if x > 1 then + repeat + output = output:sub(1,x-2)..output:sub(x) + x = x - 1 + until output:sub(x-1,x-1) == " " or (not ctrlDown) or (x == 1) + end + elseif key == keys.delete then + if x < #output+1 then + repeat + output = output:sub(1,x-1)..output:sub(x+1) + until output:sub(x,x) == " " or (not ctrlDown) or (x == #output+1) + end + elseif key == keys.enter then + termsetCursorBlink(false) + return output + elseif key == keys.home then + x = 1 + elseif key == keys["end"] then + x = #output+1 + elseif key == keys.up then + if history[hPos-1] then + hPos = hPos - 1 + output = history[hPos] + x = #output+1 + end + elseif key == keys.down then + if history[hPos+1] then + hPos = hPos + 1 + output = history[hPos] + x = #output+1 + end + end + elseif evt[1] == "key_up" then + if evt[2] == keys.leftCtrl then + ctrlDown = false + end + end + if hPos > 1 then + history[hPos] = output + end + if x+cx-xscroll+xmod > scr_x then + xscroll = x-(scr_x-cx)+xmod + elseif x-xscroll+xmod < 0 then + repeat + xscroll = xscroll - 1 + until x-xscroll-xmod >= 0 + end + xscroll = math.max(1, xscroll) + end +end +_G.colorRead = colorRead + +local checkValidName = function(_nayme) + local nayme = textToBlit(_nayme,true) + if type(nayme) ~= "string" then + return false + else + return (#nayme >= 2 and #nayme <= 32 and nayme:gsub(" ","") ~= "") + end +end + +if tArg[1] == "update" then + local res, message = updateEnchat(tArg[2] == "beta") + return print(message) +end + +local prettyClearScreen = function(start, stop) + termsetTextColor(colors.lightGray) + termsetBackgroundColor(colors.gray) + if _VERSION then + for y = start or 1, stop or scr_y do + termsetCursorPos(1,y) + if y == (start or 1) then + termwrite(("\135"):rep(scr_x)) + elseif y == (stop or scr_y) then + termsetTextColor(colors.gray) + termsetBackgroundColor(colors.lightGray) + termwrite(("\135"):rep(scr_x)) + else + termclearLine() + end + end + else + termclear() + end +end + +local cwrite = function(text, y) + local cx, cy = termgetCursorPos() + termsetCursorPos((scr_x/2) - math.ceil(#text/2), y or cy) + return write(text) +end + +local prettyCenterWrite = function(text, y) + local words = explode(" ", text, nil, true) + local buff = "" + local lines = 0 + for w = 1, #words do + if #buff + #words[w] > scr_x then + cwrite(buff, y + lines) + buff = "" + lines = lines + 1 + end + buff = buff..words[w] + end + cwrite(buff, y + lines) + return lines +end + +local prettyPrompt = function(prompt, y, replchar, doColor) + local cy, cx = termgetCursorPos() + termsetBackgroundColor(colors.gray) + termsetTextColor(colors.white) + local yadj = 1 + prettyCenterWrite(prompt, y or cy) + termsetCursorPos(1, y + yadj) + termsetBackgroundColor(colors.lightGray) + termclearLine() + local output + if doColor then + output = colorRead() + else + output = read(replchar) + end + return output +end + +local fwrite = function(text) + local b = textToBlit(text) + return termblit(unpack(b)) +end + +local cfwrite = function(text, y) + local cx, cy = termgetCursorPos() + termsetCursorPos((scr_x/2) - math.ceil(#textToBlit(text,true)/2), y or cy) + return fwrite(text) +end + +-- execution start! + +if not checkValidName(yourName) then -- not so fast, evildoers + yourName = nil +end + +local currentY = 2 + +if not (yourName and encKey) then + prettyClearScreen() +end + +if not yourName then + cfwrite("&8~7Text = &, Background = ~", scr_y-3) + cfwrite("&8~7&{Krazy = &k, Reset = &r", scr_y-2) + cfwrite("&7~00~11~22~33~44~55~66&8~77&7~88~99~aa~bb~cc~dd~ee~ff", scr_y-1) + yourName = prettyPrompt("Enter your name.", currentY, nil, true) + if not checkValidName(yourName) then + while true do + yourName = prettyPrompt("That name isn't valid. Enter another.", currentY, nil, true) + if checkValidName(yourName) then + break + end + end + end + currentY = currentY + 3 +end + +if not encKey then + setEncKey(prettyPrompt("Enter an encryption key.", currentY, "*")) + currentY = currentY + 3 +end + +-- prevents terminating. it is reversed upon exit. +local oldePullEvent = os.pullEvent +os.pullEvent = os.pullEventRaw + +local bottomMessage = function(text) + termsetCursorPos(1,scr_y) + termsetTextColor(colors.gray) + termclearLine() + termwrite(text) +end + +loadSettings() +saveSettings() + +termsetBackgroundColor(colors.black) +termclear() + +local getAPI = function(apiname, apipath, apiurl, doDoFile, doScroll) + apipath = fs.combine(fs.combine(enchat.dataDir,"api"), apipath) + if (not fs.exists(apipath)) then + if doScroll then term.scroll(1) end + bottomMessage(apiname .. " API not found! Downloading...") + local prog = http.get(apiurl) + if not prog then + if doScroll then term.scroll(1) end + bottomMessage("Failed to download " .. apiname .. " API. Abort.") + termsetCursorPos(1,1) + return + end + local file = fs.open(apipath,"w") + file.write(prog.readAll()) + file.close() + end + if doDoFile then + return dofile(apipath) + else + os.loadAPI(apipath) + end + if not _ENV[fs.getName(apipath)] then + if doScroll then term.scroll(1) end + bottomMessage("Failed to load " .. apiname .. " API. Abort.") + termsetCursorPos(1,1) + return + else + return _ENV[fs.getName(apipath)] + end +end + +local skynet, aes, bigfont +-- _G.skynet_CBOR_path = fs.combine(enchat.dataDir,"/api/cbor") +aes = getAPI("AES", "aes", "http://pastebin.com/raw/9E5UHiqv", false, false) +if enchat.connectToSkynet and http.websocket then + skynet = getAPI("Skynet", "skynet", "https://raw.githubusercontent.com/LDDestroier/CC/master/API/skynet.lua", true, true) +end +bigfont = getAPI("BigFont", "bigfont", "https://pastebin.com/raw/3LfWxRWh", false, true) + +if encKey and skynet and enchat.connectToSkynet then + bottomMessage("Connecting to Skynet...") + local success = parallel.waitForAny( + function() + skynet.open(enchat.skynetPort) + end, + function() + sleep(3) + end + ) + if success == 2 then + term.scroll(1) + bottomMessage("Failed to connect to skynet.") + skynet = nil + sleep(0.5) + end +end + +local log = {} -- Records all sorts of data on text. +local renderlog = {} -- Only records straight terminal output. Generated from 'log' +local IDlog = {} -- Really only used with skynet, will prevent duplicate messages. + +local scroll = 0 +local maxScroll = 0 + +local getModem = function() + if enchat.ignoreModem then + return nil + else + local modems = {peripheral.find("modem")} + return modems[1] + end +end + +local getChatbox = function() + if enchat.useChatbox then + if commands then -- oh baby, a command computer, now we're talkin' + -- mind you, you still need a chatbox to get chat input... + return { + say = function(text) + commands.tellraw("@a", textToBlit(text, false, "0", "f", nil, true)) + end, + tell = function(player, text) + commands.tellraw(player, textToBlit(text, false, "0", "f", nil, true)) + end + } + else + local cb = chatbox or peripheral.find("chat_box") + if cb then + if cb.setName then -- Computronics + cb.setName(yourName) + return { + say = cb.say, + tell = cb.say -- why is there no tell command??? + } + else -- whatever whackjob mod SwitchCraft uses I forget + return { + say = function(text, block) + if CHATBOX_SAFEMODE then +-- if CHATBOX_SAFEMODE ~= block then + cb.tell(CHATBOX_SAFEMODE, text) +-- end + else + local players = cb.getPlayerList() + for i = 1, #players do + if players[i] ~= block then + cb.tell(players[i], text) + end + end + end + end, + tell = cb.tell + } + end + else + return nil + end + end + else + return nil + end +end + +local modem = getModem() +local chatbox = getChatbox() + +if (not modem) and (not enchat.ignoreModem) then + if ccemux and (not enchat.ignoreModem) then + ccemux.attach("top", "wireless_modem") + modem = getModem() + elseif not skynet then + error("You should get a modem.") + end +end + +if modem then modem.open(enchat.port) end + +local modemTransmit = function(freq, repfreq, message) + if modem then + modem.transmit(freq, repfreq, message) + end +end + +local encrite = function(input) -- standardized encryption function + if not input then return input end + return aes.encrypt(encKey, textutilsserialize(input)) +end + +local decrite = function(input) -- redundant comments cause tuberculosis + if not input then return input end + return textutilsunserialize(aes.decrypt(encKey, input) or "") +end + +local dab = function(func, ...) -- "do and back", not...never mind + local x, y = termgetCursorPos() + local b, t = termgetBackgroundColor(), termgetTextColor() + local output = {func(...)} + termsetCursorPos(x,y) + termsetTextColor(t) + termsetBackgroundColor(b) + return unpack(output) +end + +local splitStr = function(str, maxLength) + local output = {} + for l = 1, #str, maxLength do + output[#output+1] = str:sub(l,l+maxLength+-1) + end + return output +end + +local splitStrTbl = function(tbl, maxLength) + local output, tline = {} + for w = 1, #tbl do + tline = splitStr(tbl[w], maxLength) + for t = 1, #tline do + output[#output+1] = tline[t] + end + end + return output +end + +-- same as term.blit, but wraps by-word. +local blitWrap = function(char, text, back, noWrite) -- where ALL of the onscreen wrapping is done + local cWords = splitStrTbl(explode(" ",char,nil, true), scr_x) + local tWords = splitStrTbl(explode(" ",char,text,true), scr_x) + local bWords = splitStrTbl(explode(" ",char,back,true), scr_x) + + local ox,oy = termgetCursorPos() + local cx,cy,ty = ox,oy,1 + local output = {} + local length = 0 + local maxLength = 0 + for a = 1, #cWords do + length = length + #cWords[a] + maxLength = mathmax(maxLength, length) + if ((cx + #cWords[a]) > scr_x) then + cx = 1 + length = 0 + if (cy == scr_y) then + term.scroll(1) + end + cy = mathmin(cy+1, scr_y) + ty = ty + 1 + end + if not noWrite then + termsetCursorPos(cx,cy) + termblit(cWords[a],tWords[a],bWords[a]) + end + cx = cx + #cWords[a] + output[ty] = output[ty] or {"","",""} + output[ty][1] = output[ty][1]..cWords[a] + output[ty][2] = output[ty][2]..tWords[a] + output[ty][3] = output[ty][3]..bWords[a] + end + return output, maxLength +end + +-- simple picture drawing function, for /picto +local pictochat = function(xsize, ysize) + local output = {{},{},{}} + local maxWidth, minMargin = 0, math.huge + for y = 1, ysize do + output[1][y] = {} + output[2][y] = {} + output[3][y] = {} + for x = 1, xsize do + output[1][y][x] = " " + output[2][y][x] = " " + output[3][y][x] = " " + end + end + + termsetBackgroundColor(colors.gray) + termsetTextColor(colors.black) + for y = 1, scr_y do + termsetCursorPos(1, y) + termwrite(("/"):rep(scr_x)) + end + cwrite(" [ENTER] to finish. ", scr_y) + cwrite("Push a key to change char.", scr_y-1) + + local cx, cy = math.floor((scr_x/2)-(xsize/2)), math.floor((scr_y/2)-(ysize/2)) + + local allCols = "0123456789abcdef" + local tPos, bPos = 16, 1 + local char, text, back = " ", allCols:sub(tPos,tPos), allCols:sub(bPos,bPos) + + local render = function() + termsetTextColor(colors.white) + termsetBackgroundColor(colors.black) + local mx, my + for y = 1, ysize do + for x = 1, xsize do + mx, my = x+cx+-1, y+cy+-1 + termsetCursorPos(mx,my) + termblit(output[1][y][x], output[2][y][x], output[3][y][x]) + end + end + termsetCursorPos((scr_x/2)-5,ysize+cy+1) + termwrite("Char = '") + termblit(char, text, back) + termwrite("'") + end + local evt, butt, mx, my + local isShiftDown = false + + render() + + while true do + evt = {os.pullEvent()} + if evt[1] == "mouse_click" or evt[1] == "mouse_drag" then + butt, mx, my = evt[2], evt[3]-cx+1, evt[4]-cy+1 + if mx >= 1 and mx <= xsize and my >= 1 and my <= ysize then + if butt == 1 then + output[1][my][mx] = char + output[2][my][mx] = text + output[3][my][mx] = back + elseif butt == 2 then + output[1][my][mx] = " " + output[2][my][mx] = " " + output[3][my][mx] = " " + end + render() + end + elseif evt[1] == "mouse_scroll" then + local oldTpos, oldBpos = tPos, bPos + if isShiftDown then + tPos = mathmax(1, mathmin(16, tPos + evt[2])) + else + bPos = mathmax(1, mathmin(16, bPos + evt[2])) + end + text, back = stringsub(allCols,tPos,tPos), stringsub(allCols,bPos,bPos) + if oldTpos ~= tPos or oldBpos ~= bPos then + render() + end + elseif evt[1] == "key" then + if evt[2] == keys.enter then + for y = 1, ysize do + output[1][y] = table.concat(output[1][y]) + output[2][y] = table.concat(output[2][y]) + output[3][y] = table.concat(output[3][y]) + maxWidth = math.max(maxWidth, #stringgsub(output[3][y], " +$", "")) + minMargin = math.min(minMargin, output[3][y]:find("[^ ]") or math.huge) + end + --error(minMargin) + local croppedOutput = {} + local touched = false + local crY = 0 + for a = 1, ysize do + if output[1][1] == (" "):rep(xsize) and output[3][1] == (" "):rep(xsize) then + tableremove(output[1],1) + tableremove(output[2],1) + tableremove(output[3],1) + else + for y = #output[1], 1, -1 do + if output[1][y] == (" "):rep(xsize) and output[3][y] == (" "):rep(xsize) then + tableremove(output[1],y) + tableremove(output[2],y) + tableremove(output[3],y) + else + break + end + end + break + end + end + for y = 1, #output[1] do + output[1][y] = output[1][y]:sub(minMargin, maxWidth) + output[2][y] = output[2][y]:sub(minMargin, maxWidth) + output[3][y] = output[3][y]:sub(minMargin, maxWidth) + end + return output + elseif evt[2] == keys.leftShift then + isShiftDown = true + elseif evt[2] == keys.left or evt[2] == keys.right then + local oldTpos, oldBpos = tPos, bPos + if isShiftDown then + tPos = mathmax(1, mathmin(16, tPos + (evt[2] == keys.right and 1 or -1))) + else + bPos = mathmax(1, mathmin(16, bPos + (evt[2] == keys.right and 1 or -1))) + end + text, back = allCols:sub(tPos,tPos), allCols:sub(bPos,bPos) + if oldTpos ~= tPos or oldBpos ~= bPos then + render() + end + end + elseif evt[1] == "key_up" then + if evt[2] == keys.leftShift then + isShiftDown = false + end + elseif evt[1] == "char" then + if char ~= evt[2] then + char = evt[2] + render() + end + end + end +end + +-- notifications will only appear if you have plethora's neural connector and overlay glasses on your person + +local notif = {} +notif.alpha = 248 +notif.height = 10 +notif.width = 6 +notif.time = 40 +notif.wrapX = 350 +notif.maxNotifs = 15 +local nList = {} +local colorTranslate = { + [" "] = {240, 240, 240}, + ["0"] = {240, 240, 240}, + ["1"] = {242, 178, 51 }, + ["2"] = {229, 127, 216}, + ["3"] = {153, 178, 242}, + ["4"] = {222, 222, 108}, + ["5"] = {127, 204, 25 }, + ["6"] = {242, 178, 204}, + ["7"] = {76, 76, 76 }, + ["8"] = {153, 153, 153}, + ["9"] = {76, 153, 178}, + ["a"] = {178, 102, 229}, + ["b"] = {51, 102, 204}, + ["c"] = {127, 102, 76 }, + ["d"] = {87, 166, 78 }, + ["e"] = {204, 76, 76 }, + ["f"] = {25, 25, 25 } +} +local interface, canvas = peripheral.find("neuralInterface") +if interface then + if interface.canvas then + canvas = interface.canvas() + notif.newNotification = function(char, text, back, time) + if #nList > notif.maxNotifs then + tableremove(nList, 1) + end + nList[#nList+1] = {char,text,back,time,1} -- the last one is the alpha multiplier + end + notif.displayNotifications = function(doCountDown) + local adjList = { + ["i"] = -4, + ["l"] = -3, + ["I"] = -1, + ["t"] = -2, + ["k"] = -1, + ["!"] = -4, + ["|"] = -4, + ["."] = -4, + [","] = -4, + [":"] = -4, + [";"] = -4, + ["f"] = -1, + ["'"] = -3, + ["\""] = -1, + ["<"] = -1, + [">"] = -1, + } + local drawEdgeLine = function(y,alpha) + local l = canvas.addRectangle(notif.wrapX, 1+(y-1)*notif.height, 1, notif.height) + l.setColor(unpack(colorTranslate["0"])) + l.setAlpha(alpha / 2) + end + local getWordWidth = function(str) + local output = 0 + for a = 1, #str do + output = output + notif.width + (adjList[stringsub(str,a,a)] or 0) + end + return output + end + canvas.clear() + local xadj, charadj, wordadj, t, r + local x, y, words, txtwords, bgwords = 0, 0 + for n = 1, mathmin(#nList, notif.maxNotifs) do + xadj, charadj = 0, 0 + y = y + 1 + x = 0 + words = explode(" ",nList[n][1],nil,true) + txtwords = explode(" ",nList[n][1],nList[n][2],true) + bgwords = explode(" ",nList[n][1],nList[n][3],true) + local char, text, back + local currentX = 0 + for w = 1, #words do + char = words[w] + text = txtwords[w] + back = bgwords[w] + if currentX + getWordWidth(char) > notif.wrapX then + y = y + 1 + x = 2 + xadj = 0 + currentX = x * notif.width + end + for cx = 1, #char do + x = x + 1 + charadj = (adjList[stringsub(char,cx,cx)] or 0) + r = canvas.addRectangle(xadj+1+(x-1)*notif.width, 1+(y-1)*notif.height, charadj+notif.width, notif.height) + if stringsub(back,cx,cx) ~= " " then + r.setAlpha(notif.alpha * nList[n][5]) + r.setColor(unpack(colorTranslate[stringsub(back,cx,cx)])) + else + r.setAlpha(100 * nList[n][5]) + r.setColor(unpack(colorTranslate["7"])) + end + drawEdgeLine(y,notif.alpha * nList[n][5]) + t = canvas.addText({xadj+1+(x-1)*notif.width,2+(y-1)*notif.height}, stringsub(char,cx,cx)) + t.setAlpha(notif.alpha * nList[n][5]) + t.setColor(unpack(colorTranslate[stringsub(text,cx,cx)])) + xadj = xadj + charadj + currentX = currentX + charadj+notif.width + end + end + end + for n = mathmin(#nList, notif.maxNotifs), 1, -1 do + if doCountDown then + if nList[n][4] > 1 then + nList[n][4] = nList[n][4] - 1 + else + if nList[n][5] > 0 then + while true do + nList[n][5] = mathmax(nList[n][5] - 0.2, 0) + notif.displayNotifications(false) + if nList[n][5] == 0 then break else sleep(0.05) end + end + end + tableremove(nList,n) + end + end + end + end + end +end + +local darkerCols = { + ["0"] = "8", + ["1"] = "c", + ["2"] = "a", + ["3"] = "b", + ["4"] = "1", + ["5"] = "d", + ["6"] = "2", + ["7"] = "f", + ["8"] = "7", + ["9"] = "b", + ["a"] = "7", + ["b"] = "7", + ["c"] = "f", + ["d"] = "7", + ["e"] = "7", + ["f"] = "f" +} + +-- used for regular chat. they can be disabled if you hate fun +local animations = { + slideFromLeft = function(char, text, back, frame, maxFrame, length) + return { + stringsub(char, (length or #char) - ((frame/maxFrame)*(length or #char))), + stringsub(text, (length or #text) - ((frame/maxFrame)*(length or #text))), + stringsub(back, (length or #back) - ((frame/maxFrame)*(length or #back))) + } + end, + fadeIn = function(char, text, back, frame, maxFrame, length) + -- a good example: + -- &1what &2in &3the &4world &5are &6you &7doing &8in &9my &aswamp + for i = 1, 3 - math.ceil(frame/maxFrame * 3) do + text = stringgsub(text, ".", darkerCols) + end + return { + char, + text, + back + } + end, + flash = function(char, text, back, frame, maxFrame, length) + local t = palette.txt + if frame ~= maxFrame then + t = (frame % 2 == 0) and t or palette.bg + end + return { + char, + toblit[t]:rep(#text), + (frame % 2 == 0) and back or (" "):rep(#back) + } + end, + none = function(char, text, back, frame, maxFrame, length) + return { + char, + text, + back + } + end +} + +local inAnimate = function(animType, buff, frame, maxFrame, length) + local char, text, back = buff[1], buff[2], buff[3] + if enchatSettings.doAnimate and (frame >= 0) and (maxFrame > 0) then + return animations[animType or "slideFromleft"](char, text, back, frame, maxFrame, length) + else + return {char,text,back} + end +end + +local genRenderLog = function() + local buff, prebuff, maxLength, lastUser + local scrollToBottom = scroll == maxScroll + renderlog = {} + local dcName, dcMessage + for a = 1, #log do + if not ((lastUser == log[a].personalID and log[a].personalID) and log[a].name == "" and log[a].message == " ") then + termsetCursorPos(1,1) + if UIconf.nameDecolor then + if lastUser == log[a].personalID and log[a].personalID then + dcName = "" + else + dcName = textToBlit(table.concat({log[a].prefix,log[a].name,log[a].suffix}), true, toblit[palette.txt], toblit[palette.bg]) + end + dcMessage = textToBlit(log[a].message, false, toblit[palette.txt], toblit[palette.bg]) + prebuff = { + dcName..dcMessage[1], + toblit[palette.chevron]:rep(#dcName)..dcMessage[2], + toblit[palette.bg]:rep(#dcName)..dcMessage[3] + } + else + if lastUser == log[a].personalID and log[a].personalID then + prebuff = textToBlit(" " .. log[a].message, false, toblit[palette.txt], toblit[palette.bg]) + else + prebuff = textToBlit(table.concat({ + log[a].prefix, + "&}&r~r", + log[a].name, + "&}&r~r", + log[a].suffix, + "&}&r~r", + log[a].message + }), + false, toblit[palette.txt], toblit[palette.bg]) + end + end + if log[a].message ~= " " and enchatSettings.noRepeatNames then + lastUser = log[a].personalID + end + if (log[a].frame == 0) and (canvas and enchatSettings.doNotif) then + if not (log[a].name == "" and log[a].message == " ") then + notif.newNotification(prebuff[1], prebuff[2], prebuff[3], notif.time * 4) + end + end + if log[a].maxFrame == true then + log[a].maxFrame = math.floor(mathmin(#prebuff[1], scr_x) / enchatSettings.animDiv) + end + if log[a].ignoreWrap then + buff, maxLength = {prebuff}, mathmin(#prebuff[1], scr_x) + else + buff, maxLength = blitWrap(prebuff[1], prebuff[2], prebuff[3], true) + end + -- repeat every line in multiline entries + for l = 1, #buff do + -- holy shit, two animations, lookit mr. roxas over here + if log[a].animType then + renderlog[#renderlog + 1] = inAnimate(log[a].animType, buff[l], log[a].frame, log[a].maxFrame, maxLength) + else + renderlog[#renderlog + 1] = inAnimate("fadeIn", inAnimate("slideFromLeft", buff[l], log[a].frame, log[a].maxFrame, maxLength), log[a].frame, log[a].maxFrame, maxLength) + end + end + if (log[a].frame < log[a].maxFrame) and log[a].frame >= 0 then + log[a].frame = log[a].frame + 1 + else + log[a].frame = -1 + end + end + end + maxScroll = mathmax(0, #renderlog - (scr_y - 2)) + if scrollToBottom then + scroll = maxScroll + end +end + +-- there is probably a much better way of doing this, but I don't care at the moment +local tsv = function(visible) + if term.current().setVisible and enchatSettings.useSetVisible then + return term.current().setVisible(visible) + end +end + +local renderChat = function(doScrollBackUp) + tsv(false) + termsetCursorBlink(false) + genRenderLog(log) + local ry + termsetBackgroundColor(palette.bg) + for y = UIconf.chatlogTop, (scr_y-UIconf.promptY) - 1 do + ry = (y + scroll - (UIconf.chatlogTop - 1)) + termsetCursorPos(1,y) + termclearLine() + if renderlog[ry] then + termblit(unpack(renderlog[ry])) + end + end + if UIconf.promptY ~= 0 then + termsetCursorPos(1,scr_y) + termsetTextColor(palette.scrollMeter) + termclearLine() + termwrite(scroll.." / "..maxScroll.." ") + end + + local _title = UIconf.title:gsub("YOURNAME", yourName.."&}&r~r"):gsub("ENCKEY", encKey.."&}&r~r"):gsub("PORT", tostring(enchat.port)) + if UIconf.doTitle then + termsetTextColor(palette.title) + term.setBackgroundColor(palette.titlebg) + if UIconf.nameDecolor then + if UIconf.centerTitle then + cwrite((" "):rep(scr_x)..textToBlit(_title, true)..(" "):rep(scr_x), UIconf.titleY or 1) + else + termsetCursorPos(1, UIconf.titleY or 1) + termwrite(textToBlit(_title, true)..(" "):rep(scr_x)) + end + else + local blTitle = textToBlit(_title) + termsetCursorPos(UIconf.centerTitle and ((scr_x/2) - math.ceil(#blTitle[1]/2)) or 1, UIconf.titleY or 1) + termclearLine() + termblit(unpack(blTitle)) + end + end + termsetCursorBlink(true) + tsv(true) +end + +local logadd = function(name, message, animType, maxFrame, ignoreWrap, _personalID) + log[#log + 1] = { + prefix = name and UIconf.prefix or "", + suffix = name and UIconf.suffix or "", + name = name or "", + message = message or " ", + ignoreWrap = ignoreWrap, + frame = 0, + maxFrame = maxFrame or true, + animType = animType, + personalID = _personalID + } +end + +local logaddTable = function(name, message, animType, maxFrame, ignoreWrap, _personalID) + if type(message) == "table" and type(name) == "string" then + if #message > 0 then + local isGood = true + for l = 1, #message do + if type(message[l]) ~= "string" then + isGood = false + break + end + end + if isGood then + logadd(name, message[1], animType, maxFrame, ignoreWrap, _personalID) + for l = 2, #message do + logadd(nil, message[l], animType, maxFrame, ignoreWrap, _personalID) + end + end + end + end +end + +local enchatSend = function(name, message, option, doLog, animType, maxFrame, crying, recipient, ignoreWrap, omitPersonalID) + option = option or {} + if option.doLog then + if type(message) == "string" then + logadd(name, message, option.animType, option.maxFrame, option.ignoreWrap, (not option.omitPersonalID) and personalID) + else + logaddTable(name, message, option.animType, option.maxFrame, option.ignoreWrap, (not option.omitPersonalID) and personalID) + end + end + local messageID = makeRandomString(64) + local outmsg = encrite({ + name = name, + message = message, + animType = option.animType, + maxFrame = option.maxFrame, + messageID = messageID, + recipient = option.recipient, + ignoreWrap = option.ignoreWrap, + personalID = (not option.omitPersonalID) and personalID, + cry = option.crying, + simCommand = option.simCommand, + simArgument = option.simArgument, + }) + IDlog[messageID] = true + if not enchat.ignoreModem then + modemTransmit(enchat.port, enchat.port, outmsg) + end + if skynet and enchatSettings.useSkynet then + skynet.send(enchat.skynetPort, outmsg) + end +end + +local cryOut = function(name, crying) + enchatSend(name, nil, {crying = crying}) +end + +local getPictureFile = function(path) -- ONLY NFP or NFT, fuck BLT + if not fs.exists(path) then + return false, "No such image." + else + local file = fs.open(path,"r") + local content = file.readAll() + file.close() + local output + if content:find("\31") and content:find("\30") then + output = explode("\n",content:gsub("\31","&"):gsub("\30","~"),nil,false) + else + if content:lower():gsub("[0123456789abcdef\n ]","") ~= "" then + return false, "Invalid image." + else + output = explode("\n",content:gsub("[^\n]","~%1 "),nil,false) + end + end + return output + end +end + +local getTableLength = function(tbl) + local output = 0 + for k,v in pairs(tbl) do + output = output + 1 + end + return output +end + +local userCryList = {} + +local commandInit = "/" +local commands = {} +local simmableCommands = { + big = true +} +-- Commands only have one argument, being a single string. +-- Separate arguments can be extrapolated with the explode() function. +commands.about = function() + if enchatSettings.extraNewline then + logadd(nil,nil) + end + logadd(nil,"Enchat "..enchat.version.." by LDDestroier.") + logadd(nil,"'Encrypted, decentralized, &1c&2o&3l&4o&5r&6i&7z&8e&9d&r chat program'") + logadd(nil,"Made in 2018, out of gum and procrastination.") + logadd(nil,nil) + logadd(nil,"AES Lua implementation made by SquidDev.") + logadd(nil,"'Skynet' (enables HTTP chat) belongs to gollark (osmarks).") +end +commands.exit = function() + enchatSend("*", "'"..yourName.."&}&r~r' buggered off. (disconnect)") + return "exit" +end +commands.me = function(msg) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if msg then + enchatSend("&2*", yourName.."~r&2 "..msg, {doLog = true}) + else + logadd("*",commandInit.."me [message]") + end +end +commands.tron = function() + local url = "https://raw.githubusercontent.com/LDDestroier/CC/master/tron.lua" + local prog, contents = http.get(url) + if prog then + enchatSend("*", yourName .. "&}&r~r has started a game of TRON.", {doLog = true}) + contents = prog.readAll() + pauseRendering = true + prog = load(contents, nil, nil, _ENV)(enchatSettings.useSkynet and "skynet", "quick", yourName) + else + logadd("*", "Could not download TRON.") + end + pauseRendering = false + doRender = true +end +commands.colors = function() + if enchatSettings.extraNewline then + logadd(nil,nil) + end + logadd("*", "&{Color codes: (use & or ~)&}") + logadd(nil, " &7~11~22~33~44~55~66~7&87~8&78~99~aa~bb~cc~dd~ee~ff") + logadd(nil, " &{Reset text/BG with &r and ~r.&}") + logadd(nil, " &{Use &k for krazy text.&}") +end +commands.update = function() + local res, message = updateEnchat() + if res then + enchatSend("*", yourName.."&}&r~r has updated and exited.") + termsetBackgroundColor(colors.black) + termsetTextColor(colors.white) + termclear() + termsetCursorPos(1,1) + print(message) + return "exit" + else + logadd("*", res) + end +end +commands.picto = function(filename) + local image, output, res + local isEmpty + if filename then + output, res = getPictureFile(filename) + if not output then + logadd("*",res) + logadd(nil,nil) + return + else + tableinsert(output,1,"") + end + else + isEmpty = true + output = {""} + pauseRendering = true + local image = pictochat(26,11) + pauseRendering = false + for y = 1, #image[1] do + output[#output+1] = "" + for x = 1, #image[1][1] do + output[#output] = table.concat({ + output[#output], + "&", + image[2][y]:sub(x,x), + "~", + image[3][y]:sub(x,x), + image[1][y]:sub(x,x) + }) + isEmpty = isEmpty and (image[1][y]:sub(x,x) == " " and image[3][y]:sub(x,x) == " ") + end + end + end + if not isEmpty then + enchatSend(yourName, output, {doLog = true, animType = "slideFromLeft", ignoreWrap = true}) + end +end +commands.list = function() + userCryList = {} + local tim = os.startTimer(0.5) + cryOut(yourName, true) + while true do + local evt = {os.pullEvent()} + if evt[1] == "timer" then + if evt[2] == tim then + break + end + end + end + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if getTableLength(userCryList) == 0 then + logadd(nil,"Nobody's there.") + else + for k,v in pairs(userCryList) do + logadd(nil,"+'"..k.."'") + end + end +end +commands.nick = function(newName) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if newName then + if checkValidName(newName) then + if newName == yourName then + logadd("*","But you're already called that!") + else + enchatSend("*", "'"..yourName.."&}&r~r' is now known as '"..newName.."&}&r~r'.", {doLog = true}) + yourName = newName + end + else + if #newName < 2 then + logadd("*", "That name is too damned small.") + elseif #newName > 32 then + logadd("*", "Woah there, that name is too large.") + end + end + else + logadd("*",commandInit.."nick [newName]") + end +end +commands.whoami = function(now) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if now == "now" then + logadd("*","You are still '"..yourName.."&}&r~r'!") + else + logadd("*","You are '"..yourName.."&}&r~r'!") + end +end +commands.key = function(newKey) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if newKey then + if newKey ~= encKey then + enchatSend("*", "'"..yourName.."&}&r~r' buggered off. (keychange)") + setEncKey(newKey) + logadd("*", "Key changed to '"..encKey.."&}&r~r'.") + enchatSend("*", "'"..yourName.."&}&r~r' has moseyed on over.", {omitPersonalID = true}) + else + logadd("*", "That's already the key, though.") + end + else + logadd("*","Key = '"..encKey.."&}&r~r'") + logadd("*","Channel = '"..enchat.port.."'") + end +end +commands.shrug = function(face) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + enchatSend(yourName, "¯\\_"..(face and ("("..face..")") or "\2").."_/¯", {doLog = true}) +end +commands.asay = function(_argument) + local sPoint = (_argument or ""):find(" ") + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if not sPoint then + logadd("*","Animation types:") + for k,v in pairs(animations) do + logadd(nil," '"..k.."'") + end + else + local animType = _argument:sub(1,sPoint-1) + local message = _argument:sub(sPoint+1) + local animFrameMod = { + flash = 8, + fadeIn = 4, + } + if animations[animType] then + if textToBlit(message,true):gsub(" ","") ~= "" then + enchatSend(yourName, message, {doLog = true, animType = animType, maxFrame = animFrameMod[animType]}) + else + logadd("*","That message is no good.") + end + else + logadd("*","Invalid animation type.") + end + end +end +commands.big = function(_argument, simUser) + local sPoint = (_argument or ""):find(" ") + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if not sPoint then + logadd("*",commandInit .. "big ") + else + local fontSize = tonumber(_argument:sub(1,sPoint-1)) + local message = _argument:sub(sPoint+1) + if not fontSize then + logadd("*","Size must be number between 0 and 2.") + elseif fontSize < 0 or fontSize > 2 then + logadd("*","Size must be number between 0 and 2.") + else + fontSize = math.floor(.5+fontSize) + local tOutput + if fontSize > 0 then + message = textToBlit(message, false, "0", "f") + local output = {{},{},{}} + local x, y = 1, 1 + local char + for i = 1, #message[1] do + char = bigfont.makeBlittleText( + fontSize, + stringsub(message[1],i,i), + stringsub(message[2],i,i), + stringsub(message[3],i,i) + ) + x = x + char.width + if x >= scr_x then + y = y + char.height + x = char.width + end + for charY = 1, char.height do + output[1][y+charY-1] = (output[1][y+charY-1] or " ") .. char[1][charY] + output[2][y+charY-1] = (output[2][y+charY-1] or " ") .. char[2][charY] + output[3][y+charY-1] = (output[3][y+charY-1] or " ") .. char[3][charY] + end + end + tOutput = {""} + local yy = 1 + for y = 1, #output[1] do + tOutput[#tOutput+1] = "" + for x = 1, #output[1][y] do + tOutput[#tOutput] = table.concat({tOutput[#tOutput],"&",output[2][yy]:sub(x,x),"~",output[3][yy]:sub(x,x),output[1][yy]:sub(x,x)}) + end + yy = yy + 1 + end + else + tOutput = message + end + if simUser then + logaddTable(simUser, tOutput) + else + logaddTable(yourName, tOutput) + enchatSend(yourName, nil, {simCommand = "big", simArgument = _argument}) + end + end + end +end +commands.msg = function(_argument) + local sPoint = (_argument or ""):find(" ") + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if not sPoint then + logadd("*",commandInit.."msg ") + else + local recipient = _argument:sub(1,sPoint-1) + local message = _argument:sub(sPoint+1) + if not message then + logadd("*","You got half of the arguments down pat, at least.") + else + if textToBlit(message,true):gsub(" ","") == "" then + logadd("*","That message is no good.") + else + enchatSend(yourName, message, {recipient = recipient}) + logadd("*","to '"..recipient.."': "..message) + end + end + end +end +commands.palette = function(_argument) + local argument = _argument or "" + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if argument:gsub("%s","") == "" then + local buff = "" + for k,v in pairs(palette) do + buff = buff..k..", " + end + buff = buff:sub(1,-3) + logadd("*",commandInit.."palette "..buff.." ") + else + argument = explode(" ",argument) + if #argument == 1 then + if argument[1]:gsub("%s",""):lower() == "reset" or argument[1]:gsub("%s",""):lower() == "enchat3" then + palette = { + bg = colors.black, + txt = colors.white, + promptbg = colors.gray, + prompttxt = colors.white, + scrollMeter = colors.lightGray, + chevron = colors.black, + title = colors.lightGray, + titlebg = colors.gray, + } + UIconf = { + promptY = 1, + chevron = ">", + chatlogTop = 1, + title = "Enchat 3", + doTitle = false, + titleY = 1, + nameDecolor = false, + centerTitle = true, + prefix = "<", + suffix = "> " + } + termsetBackgroundColor(palette.bg) + termclear() + logadd("*","You cleansed your palette.") + saveSettings() + elseif argument[1]:gsub("%s",""):lower() == "enchat2" then + palette = { + bg = colors.gray, + txt = colors.white, + promptbg = colors.white, + prompttxt = colors.black, + scrollMeter = colors.white, + chevron = colors.lightGray, + title = colors.yellow, + titlebg = colors.gray, + } + UIconf = { + promptY = 1, + chevron = ">", + chatlogTop = 1, + title = "Enchat 2", + doTitle = false, + titleY = 1, + nameDecolor = false, + centerTitle = false, + prefix = "<", + suffix = "> " + } + termsetBackgroundColor(palette.bg) + termclear() + logadd("*","Switched to the old Enchat2 palette.") + saveSettings() + elseif argument[1]:gsub("%s",""):lower() == "enchat1" then + logadd("*","We don't talk about that one.") + elseif argument[1]:gsub("%s",""):lower() == "enchat4" then + logadd("*","Let's leave that to future LDD.") + elseif argument[1]:gsub("%s",""):lower() == "chat.lua" then + palette = { + bg = colors.black, + txt = colors.white, + promptbg = colors.black, + prompttxt = colors.white, + scrollMeter = colors.white, + chevron = colors.yellow, + title = colors.yellow, + titlebg = colors.black, + } + UIconf = { + promptY = 0, + chevron = ": ", + chatlogTop = 2, + title = "YOURNAME on ENCKEY", + doTitle = true, + titleY = 1, + nameDecolor = true, + centerTitle = true, + prefix = "<", + suffix = "> " + } + termsetBackgroundColor(palette.bg) + termclear() + logadd("*","Switched to /rom/programs/rednet/chat.lua palette.") + saveSettings() + elseif argument[1]:gsub("%s",""):lower() == "talk" then + palette = { + bg = colors.black, + txt = colors.white, + promptbg = colors.black, + prompttxt = colors.white, + scrollMeter = colors.white, + chevron = colors.white, + title = colors.black, + titlebg = colors.white, + } + UIconf = { + promptY = 0, + chevron = "", + chatlogTop = 1, + title = " enchat v3.0 channel: ENCKEY:PORT", + titleY = scr_y - 1, + doTitle = true, + nameDecolor = false, + centerTitle = false, + prefix = "<", + suffix = "> " + } + termsetBackgroundColor(palette.bg) + termclear() + logadd("*","Switched to Talk palette.") + saveSettings() + elseif argument[1]:gsub("%s",""):lower() == "darkchat" then + palette = { + bg = colors.black, + txt = colors.white, + promptbg = colors.black, + prompttxt = colors.white, + scrollMeter = colors.white, + chevron = colors.white, + title = colors.white, + titlebg = colors.blue, + } + UIconf = { + promptY = 0, + chevron = "Message: ", + chatlogTop = 1, + title = " ", + titleY = scr_y - 1, + doTitle = true, + nameDecolor = false, + centerTitle = true, + prefix = "", + suffix = ": " + } + termsetBackgroundColor(palette.bg) + termclear() + logadd("*","Switched to DarkChat palette.") + saveSettings() + else + if not palette[argument[1]] then + logadd("*","There's no such palette option.") + else + logadd("*","'"..argument[1].."' = '"..toblit[palette[argument[1]]].."'") + end + end + else + if #argument > 2 then + argument = {argument[1], table.concat(argument," ",2)} + end + argument[1] = argument[1]:lower() + local newcol = argument[2]:lower() + if not palette[argument[1]] then + logadd("*","That's not a valid palette choice.") + else + if not (tocolors[newcol] or colors_strnames[newcol]) then + logadd("*","That isn't a valid color code. (0-f)") + else + palette[argument[1]] = (tocolors[newcol] or colors_strnames[newcol]) + logadd("*","Palette changed.",false) + saveSettings() + end + end + end + end +end +commands.clear = function() + log = {} + IDlog = {} +end +commands.ping = function(pong) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + logadd(nil, pong or "Pong!") +end +commands.set = function(_argument) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + argument = _argument or "" + local collist = { + ["string"] = function() return "0" end, + ["table"] = function() return "5" end, + ["number"] = function() return "0" end, + ["boolean"] = function(val) if val then return "d" else return "e" end end, + ["function"] = function() return "c" end, + ["nil"] = function() return "8" end, + ["thread"] = function() return "d" end, + ["userdata"] = function() return "c" end, -- ha + } + local custColorize = function(input) + return "&"..collist[type(input)](input) + end + local contextualQuote = function(judgetxt,txt) + if type(judgetxt) == "string" then + return table.concat({"'",txt,"'"}) + else + return txt + end + end + local arguments = explode(" ",argument) + if #argument == 0 then + for k,v in pairs(enchatSettings) do + logadd(nil,"&4'"..k.."'&r = "..contextualQuote(v,custColorize(v)..tostring(v).."&r")) + end + else + if enchatSettings[arguments[1]] ~= nil then + if #arguments >= 2 then + local newval = table.concat(arguments," ",2) + if tonumber(newval) then + newval = tonumber(newval) + elseif textutilsunserialize(newval) ~= nil then + newval = textutilsunserialize(newval) + end + if type(enchatSettings[arguments[1]]) == type(newval) then + enchatSettings[arguments[1]] = newval + logadd("*","Set '&4"..arguments[1].."&r' to &{"..contextualQuote(newval,textutilsserialize(newval).."&}").." ("..type(newval)..")") + saveSettings() + else + logadd("*","Wrong value type (it's "..type(enchatSettings[arguments[1]])..")") + end + else + logadd("*","'"..arguments[1].."' is set to "..contextualQuote(enchatSettings[arguments[1]],custColorize(enchatSettings[arguments[1]])..textutilsserialize(enchatSettings[arguments[1]]).."&r").." ("..type(enchatSettings[arguments[1]])..")") + end + else + logadd("*","No such setting.") + end + end + if enchatSettings.useSkynet and (not skynet) then + pauseRendering = true + termsetBackgroundColor(colors.black) + termclear() + pauseRendering = false + end +end +commands.help = function(cmdname) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if cmdname then + local helpList = { + exit = "Exits Enchat and returns to loader (most likely CraftOS)", + about = "Tells you a bit about this here Enchat.", + me = "Sends a message in the format of \"* yourName message\"", + colors = "Lists all the colors you can use.", + update = "Updates and overwrites Enchat, then exits if successful.", + list = "Lists all users in range using the same key.", + nick = "Give yourself a different username.", + whoami = "Tells you your current username.", + key = "Change the current encryption key. Tells you the key, if without argument.", + clear = "Clears the local chat log. Not your inventory, I swear.", + ping = "Pong. *sigh*", + shrug = "Sends out a shrugging emoticon.", + set = "Changes config options during the current session. Lists all options, if without argument.", + msg = "Sends a message that is only logged by a specific user.", + picto = "Opens an image maker and sends the result. Use the scroll wheel to change color, and hold left shift to change text color. If argument given, will look for an image at the given path and use that instead.", + tron = "Starts up a game of TRON.", + big = "Sends your message, but enlarged by a specified amount via Wojbie's BigFont API.", + help = "Shows every command, or describes a specific command.", + } + cmdname = cmdname:gsub(" ",""):gsub("/","") + if helpList[cmdname] then + logadd("*", helpList[cmdname]) + else + if commands[cmdname] then + logadd("*", "No help info for that command.") + else + logadd("*", "No such command to get help for.") + end + end + else + logadd("*","All commands:") + local output = "" + for k,v in pairs(commands) do + output = output.." "..commandInit..k.."," + end + logadd(nil, output:sub(1,-2)) + end +end +commandAliases = { + quit = commands.exit, + colours = commands.colors, + ls = commands.list, + cry = commands.list, + nickname = commands.nick, + channel = commands.key, + palate = commands.palette, + tell = commands.msg, + whisper = commands.msg, + ["?"] = commands.help, + porn = function() logadd("*","Yeah, no.") end, + whoareyou = function() logadd("*", "I'm Enchat. But surely, you know this?") end, + fuck = function() logadd("*","A mind is a terrible thing to waste.") end, + hello = function() logadd("*","Hey.") end, + hi = function() logadd("*","Hiya.") end, + hey = function() logadd("*","That's for horses.") end, + bye = function() logadd("*","You know, you can use /exit.") end, + die = function() logadd("*","You wish.") end, + nap = function() logadd("*","The time for napping has passed.") end, + sorry = function() logadd("*","That's okay.") end, + jump = function() logadd("*","Sorry. This program is in a NO JUMPING zone.") end, + enchat = function() logadd("*","At your service!") end, + win = function() logadd("*","Naturally!") end, + lose = function() logadd("*","Preposterous!") end, + xyzzy = function() logadd("*","A hollow voice says \"Fool.\"") end, + wait = function() logadd("*","Time passes...") end, + stop = function() logadd("*","Hammertime!","fadeIn") end, + shit = function() logadd("*","Man, you're telling me!") end, + eat = function() logadd("*","You're not hungry.") end, + what = function() logadd("*","What indeed.") end, + ldd = function() logadd(nil,"& that's me") end, + OrElseYouWill = function() + enchatSend("*", "'"..yourName.."&}&r~r' buggered off. (disconnect)") + error("DIE") + end +} + +local checkIfCommand = function(input) + if input:sub(1,#commandInit) == commandInit then + return true + else + return false + end +end + +local parseCommand = function(input) + local sPos1, sPos2 = input:find(" ") + local cmdName, cmdArgs + if sPos1 then + cmdName = input:sub(#commandInit+1, sPos1-1) + cmdArgs = input:sub(sPos2+1) + else + cmdName = input:sub(#commandInit+1) + cmdArgs = nil + end + + local res + local CMD = commands[cmdName] or commandAliases[cmdName] + if CMD then + res = CMD(cmdArgs) + if res == "exit" then + return "exit" + end + else + logadd("*", "No such command.") + end +end + +local main = function() + termsetBackgroundColor(palette.bg) + termclear() + os.queueEvent("render_enchat") + local mHistory = {} + + while true do + + termsetCursorPos(1, scr_y-UIconf.promptY) + termsetBackgroundColor(palette.promptbg) + termclearLine() + termsetTextColor(palette.chevron) + termwrite(UIconf.chevron) + termsetTextColor(palette.prompttxt) + + local input = colorRead(nil, mHistory) + if textToBlit(input,true):gsub(" ","") ~= "" then -- people who send blank messages in chat programs deserve to die + if checkIfCommand(input) then + local res = parseCommand(input) + if res == "exit" then + return "exit" + end + else + if enchatSettings.extraNewline then + logadd(nil,nil,nil,nil,nil,personalID) -- readability is key + end + enchatSend(yourName, input, {doLog = true}) + end + if mHistory[#mHistory] ~= input then + mHistory[#mHistory+1] = input + end + elseif input == "" then + logadd(nil,nil,nil,nil,nil,personalID) + end + os.queueEvent("render_enchat") + + end + +end + +local handleReceiveMessage = function(user, message, animType, maxFrame, _personalID) + if enchatSettings.extraNewline then + logadd(nil,nil,nil,nil,nil,_personalID) -- readability is still key + end + logadd(user, message, animations[animType] and animType or nil, (type(maxFrame) == "number") and maxFrame or nil, nil, _personalID) + os.queueEvent("render_enchat") +end + +local adjScroll = function(distance) + scroll = mathmin(maxScroll, mathmax(0, scroll + distance)) +end + +local checkRSinput = function() + return ( + rs.getInput("front") or + rs.getInput("back") or + rs.getInput("left") or + rs.getInput("right") or + rs.getInput("top") or + rs.getInput("bottom") + ) +end + +local handleEvents = function() + local oldScroll + local keysDown = {} + while true do + local evt = {os.pullEvent()} + if evt[1] == "enchat_receive" then + if type(evt[2]) == "string" and type(evt[3]) == "string" then + handleReceiveMessage(evt[2], evt[3]) + end + elseif evt[1] == "chat" and ((not checkRSinput()) or (not enchat.disableChatboxWithRedstone)) then + if enchat.useChatbox then + if enchatSettings.extraNewline then + logadd(nil,nil) -- readability is key + end + enchatSend(evt[2], evt[3], {doLog = true}) + end + elseif evt[1] == "chat_message" and ((not checkRSinput()) or (not enchat.disableChatboxWithRedstone)) then -- computronics + if enchat.useChatbox then + if enchatSettings.extraNewline then + logadd(nil,nil) -- readability is key + end + enchatSend(evt[3], evt[4], {doLog = true}) + end + elseif (evt[1] == "modem_message") or (evt[1] == "skynet_message" and enchatSettings.useSkynet) then + local side, freq, repfreq, msg, distance + if evt[1] == "modem_message" then + side, freq, repfreq, msg, distance = evt[2], evt[3], evt[4], evt[5], evt[6] + else + freq, msg = evt[2], evt[3] + end + if (freq == enchat.port) or (freq == enchat.skynetPort) then + msg = decrite(msg) + if type(msg) == "table" then + if (type(msg.name) == "string") then + if #msg.name <= 32 then + if msg.messageID and (not IDlog[msg.messageID]) then + userCryList[msg.name] = true + IDlog[msg.messageID] = true + if ((not msg.recipient) or (msg.recipient == yourName or msg.recipient == textToBlit(yourName,true))) then + if type(msg.message) == "string" then + handleReceiveMessage(msg.name, tostring(msg.message), msg.animType, msg.maxFrame, msg.personalID) + if chatbox and enchat.useChatbox and ((not checkRSinput()) or (not enchat.disableChatboxWithRedstone)) then + chatbox.say(UIconf.prefix .. msg.name .. UIconf.suffix .. msg.message, msg.name) + end + elseif type(msg.message) == "table" and enchatSettings.acceptPictoChat and #msg.message <= 64 then + logaddTable(msg.name, msg.message, msg.animType, msg.maxFrame, msg.ignoreWrap, msg.personalID) + if enchatSettings.extraNewline then + logadd(nil,nil) + end + elseif commands[msg.simCommand or false] and type(msg.simArgument) == "string" then + if simmableCommands[msg.simCommand or false] then + commands[msg.simCommand](msg.simArgument, msg.name) + end + end + end + if (msg.cry == true) then + cryOut(yourName, false) + end + end + end + end + end + end + elseif evt[1] == "mouse_scroll" and (not pauseRendering) then + local dist = evt[2] + oldScroll = scroll + adjScroll(enchatSettings.reverseScroll and -dist or dist) + if scroll ~= oldScroll then + dab(renderChat) + end + elseif evt[1] == "key" and (not pauseRendering) then + local key = evt[2] + keysDown[key] = true + oldScroll = scroll + local pageSize = (scr_y-UIconf.promptY) - UIconf.chatlogTop + if key == keys.pageUp then + adjScroll(-(keysDown[keys.leftCtrl] and pageSize or enchatSettings.pageKeySpeed)) + elseif key == keys.pageDown then + adjScroll(keysDown[keys.leftCtrl] and pageSize or enchatSettings.pageKeySpeed) + end + if scroll ~= oldScroll then + dab(renderChat) + end + elseif evt[1] == "key_up" then + local key = evt[2] + keysDown[key] = nil + elseif (evt[1] == "render_enchat") and (not pauseRendering) then + dab(renderChat) + elseif (evt[1] == "tron_complete") then + if evt[3] then + if enchatSettings.extraNewline then + logadd(nil,nil) + end + if evt[2] == "win" then + enchatSend("*", yourName .. "&}&r~r beat " .. (evt[4] or "someone") .. "&}&r~r in TRON!", {doLog = true}) + elseif evt[2] == "lose" then + enchatSend("*", (evt[4] or "Someone") .. "&}&r~r beat " .. yourName .. "&}&r~r in TRON!", {doLog = true}) + elseif evt[2] == "tie" then + enchatSend("*", yourName .. "&}&r~r tied with " .. (evt[4] or "someone") .. "&}&r~r in TRON!", {doLog = true}) + end + elseif evt[2] == "timeout" then + if enchatSettings.extraNewline then + logadd(nil,nil) + end + enchatSend("*", yourName .. "&}&r~r timed out against " .. (evt[4] or "someone") .. "&}&r~r in TRON...", {doLog = true}) + end + elseif evt[1] == "terminate" then + return "exit" + end + end +end + +local keepRedrawing = function() + while true do + sleep(enchatSettings.redrawDelay) + if not pauseRendering then + os.queueEvent("render_enchat") + end + end +end + +local handleNotifications = function() + while true do + os.pullEvent("render_enchat") + if canvas and enchatSettings.doNotif then + notif.displayNotifications(true) + end + end +end + +getModem() + +enchatSend("*", "'"..yourName.."&}&r~r' has moseyed on over.", {doLog = true, omitPersonalID = true}) + +local funky = { + main, + handleEvents, + keepRedrawing, + handleNotifications +} + +if skynet then + funky[#funky+1] = function() + while true do + if skynet then + pcall(skynet.listen) + local success, msg = pcall(skynet.open, enchat.skynetPort) + if not success then + skynet = nil + end + end + sleep(5) + end + end +end + +pauseRendering = false + +local res, outcome = pcall(function() + return parallel.waitForAny(unpack(funky)) +end) + +os.pullEvent = oldePullEvent +if skynet then + if skynet.socket then + skynet.socket.close() + end +end + +if canvas then + canvas.clear() +end + +tsv(true) -- in case it's false, y'know + +if not res then + prettyClearScreen(1,scr_y-1) + termsetTextColor(colors.white) + termsetBackgroundColor(colors.gray) + cwrite("There was an error.",2) + cfwrite("Report this to &3@LDDestroier#2901&r",3) + cwrite("on Discord,",4) + cwrite("if you feel like it.",5) + termsetCursorPos(1,7) + printError(outcome) + termsetTextColor(colors.lightGray) + cwrite("I'll probably fix it, maybe.",10) +end + +termsetCursorPos(1, scr_y) +termsetBackgroundColor(initcolors.bg) +termsetTextColor(initcolors.txt) +termclearLine() -- cgit v1.2.3