diff --git a/Quarry-Receiver.lua b/Quarry-Receiver.lua new file mode 100644 index 0000000..4f16808 --- /dev/null +++ b/Quarry-Receiver.lua @@ -0,0 +1,1824 @@ +https://pastebin.com/raw/7Ksx4qUJ + +--Quarry Receiver Version 3.6.5 +--Made by Civilwargeeky +--[[ +Recent Changes: + Fixed bugs with hardcoded keys for CC: Tweaked +]] + + +--Config +local doDebug = false --For testing purposes +local ySizes = 3 --There are 3 different Y Screen Sizes right now +local quadEnabled = false --This is for the quadrotors mod by Lyqyd +local autoRestart = false --If true, will reset screens instead of turning them off. For when reusing turtles. + +--Initializing Program-Wide Variables +local expectedMessage = "Civil's Quarry" --Expected initial message +local expectedFingerprint = "quarry" +local replyMessage = "Turtle Quarry Receiver" --Message to respond to handshake with +local replyFingerprint = "quarryReceiver" +local stopMessage = "stop" +local expectedFingerprint = "quarry" +local themeFolder = "quarryResources/receiverThemes/" +local modemSide --User can specify a modem side, but it is not necessary +local modem --This will be the table for the modem +local computer --The main screen is special. It gets defined first :3 +local continue = true --This keeps the main while loop going +local quadDirection = "north" +local quadDirections = {n = "north", s = "south", e = "east", w = "west"} +local quadBase, computerLocation +local tArgs = {...} +--These two are used by controller in main loop +local commandString = "" --This will be a command string sent to turtle. This var is stored for display +local lastCommand --If a command needs to be sent, this gets set +local defaultSide +local defaultCommand +local stationsList = {} + +for i=1, #tArgs do --Parameters that must be set before rest of program for proper debugging + local val = tArgs[i]:lower() + if val == "-v" or val == "-verbose" then + doDebug = true + end + if val == "-q" or val == "-quiet" then + doDebug = false + end +end + +local keyMap = {[keys.space] = " ", [keys.minus] = "_", [keys.period] = ".", [keys.numPadDecimal] = "."} --This is for command string +keyMap[keys.numPad0] = "0" +keyMap[keys.numPad1] = "1" +keyMap[keys.numPad2] = "2" +keyMap[keys.numPad3] = "3" +keyMap[keys.numPad4] = "4" +keyMap[keys.numPad5] = "5" +keyMap[keys.numPad6] = "6" +keyMap[keys.numPad7] = "7" +keyMap[keys.numPad8] = "8" +keyMap[keys.numPad9] = "9" + +keyMap[keys.zero] = "0" +keyMap[keys.one] = "1" +keyMap[keys.two] = "2" +keyMap[keys.three] = "3" +keyMap[keys.four] = "4" +keyMap[keys.five] = "5" +keyMap[keys.six] = "6" +keyMap[keys.seven] = "7" +keyMap[keys.eight] = "8" +keyMap[keys.nine] = "9" + +for a,b in pairs(keys) do --Add all letters from keys api + if #a == 1 then + keyMap[b] = a:upper() + end +end +keyMap[keys.enter] = "enter" +keyMap[keys.numPadEnter] = "enter" +keyMap[keys.backspace] = "backspace" +keyMap[keys.up] = "up" +keyMap[keys.down] = "down" +keyMap[keys.left] = "left" +keyMap[keys.right] = "right" + +local helpResources = { --$$ is a new page +main = [[$$Hello and welcome to Quarry Receiver Help! + +This goes over everything there is to know about the receiver + +Use the arrow keys to navigate! +Press '0' to come back here! +Press 'q' to quit! + +Press a section number at any time to go the beginning of that section + 1. Basic Use + 2. Parameters + 3. Commands + 4. Turtle Commands + +$$A secret page! +You found it! Good job :) +]], +[[$$Your turtle and you! + +To use this program, you need a wireless modem on both this computer and the turtle + +Make sure they are attached to both the turtle and this computer +$$Using your new program! + +Once you have done that, start the turtle and when it says "Rednet?", say "Yes" + Optionally, you can use the parameter "-rednet true" +Then remember the channel it tells you to open. + +Come back to this computer, and run the program. Follow onscreen directions. + Optionally, you can use the parameter "-receiveChannel" + +Check out the other help sections for more parameters +$$Adding Screens! +You can add screens with the "-screen" parameter or the "SCREEN" command +An example would be "SCREEN LEFT 2 BLUE" for a screen on the left side with channel 2 and blue theme + +You can connect screens over wired modems. Attach a modem to the computer and screen, then right click the modem attached to the screen. +Then you can say "SCREEN MONITOR_0" or whatever it says + +]], +[[$$Parameters! + note: <> means required, [] means optional + +-help/help/-?/?/-usage/usage: That's this! + +-autoRestart [t/f]: If true, the receiver will not exit when all quarries are done and will automagically reconnect to new quarries + With no argument, this is set to true. + +-receiveChannel/channel : Sets the main screen's receive channel + +-theme : sets the "default" theme that screens use when they don't have a set theme +$$Parameters! + note: <> means required, [] means optional + +-screen [channel] [theme]: makes a new screen on the given side with channel and theme + example: -screen left 10 blue This adds a new screen on the left receiving channel 10 with a blue theme. + +-station [side]: makes the screen a "station" that monitors all screens. + if no side, uses computer + +-auto [channel list]: This finds all attached monitors and initializes them (with channels) + example: -auto 1 2 5 9 finds screens and gives them channels +$$Parameters! + note: <> means required, [] means optional + +-colorEditor: makes the main screen a color editor that just prints the current colors. Good for theme making +current typeColors: default title, subtitle, pos, dim, extra, error, info, inverse, command, help, background + +-modem : Sets the modem side to side + +-v/-verbose: turns on debug + +-q/-quiet: turns off debug +]], +[[$$Commands! + +COMMAND [screen] [text]: Sends text to the connected turtle. See turtle commands for valid commands + +SCREEN [side] [channel] [theme]: Adds a screen. You can also specify the channel and theme of the screen. + +REMOVE [screen]: Removes the selected screen (cannot remove the main screen) + +THEME [screen] [name]: Sets the theme of the given screen. THEME [screen] resets the screen to default theme +$$Commands! + +THEME [name]: Sets the default theme. + +RECEIVE [screen] [channel]: Changes the receive channel of the given screen + +SEND [screen] [channel]: Changes the send channel of the given screen (for whatever reason) + +STATION [screen] [channel]: Sets the given screen to/from a station. If changing from a station, will set the screen's channel +$$Commands! + +SET [text]: Sets a default command that can be backspaced. Useful for color editing or command sending + Use SET with nothing after to remove text + +SIDE [screen]: Sets a default screen for "sided" commands. + Any command that takes a [screen] is sided + +EXIT/QUIT: Quits the program gracefully +$$Commands! + +COLOR [themeName] [typeColor] [textColor] [backColor]: Sets the the text and background colors of the given typeColor of the given theme. See notes on "colorEditor" parameter for more info + +SAVETHEME [themeName] [fileName]: Saves the given theme as fileName for later use + +SAVETHEME [screen] [fileName]: Same as above but for a screen's theme + +AUTO [channelList]: Automatically searches for nearby screens, providing them sequentially with channels if a channel list is given + Example Use: AUTO 1 2 5 9 +$$Commands! + +HELP: Displays this again! + +VERBOSE: Turns debug on + +QUIET: Turns debug off + +]], +[[$$Turtle Commands! + +Stop: Stops the turtle where it is + +Return: The turtle will return to its starting point, drop off its load, and stop + +Drop: Turtle will immediately go and drop its inventory + +Pause: Pauses the turtle + +Resume: Resumes paused turtles + +Refuel: Turtle will schedule an emergency refuel + This could take from fuelChest, or quadCopter + or fuel in inventory (in that order) +]] +} + +--Generic Functions-- +local function debug(...) + --if doDebug then return print(...) end --Basic + if doDebug then + print("\nDEBUG: ",...) + os.pullEvent("char") + end +end +local function clearScreen(x,y, periph) + periph, x, y = periph or term, x or 1, y or 1 + periph.clear() + periph.setCursorPos(x,y) +end + +local function swapKeyValue(tab) + for a,b in pairs(tab) do + tab[b] = a + end + return tab +end +local function copyTable(tab) + local toRet = {} + for a,b in pairs(tab) do + toRet[a] = b + end + return toRet +end +local function checkChannel(num) + num = tonumber(num) + if not num then return false end + if 1 <= num and num <= 65535 then + return num + end + return false +end +local function truncate(text, xDim) + if #text <= xDim then return text end + return #text >= 4 and text:sub(1,xDim-3).."..." or text:sub(1,3) +end +local function align(text, xDim, direction, trunc) + text = tostring(text or "None") + if trunc == nil then trunc = true end + if #text >= xDim and trunc then return truncate(text,xDim) end + for i=1, xDim-#text do + if direction == "right" then + text = " "..text + elseif direction == "left" then + text = text.." " + end + end + return text +end +local function alignR(text, xDim, trunc) + return align(text, xDim, "right", trunc) +end +local function alignL(text, xDim, trunc) + return align(text, xDim, "left", trunc) +end +local function center(text, xDim, char) + if not xDim then error("Center: No dim given",2) end + char = char or " " + local a = (xDim-#text)/2 + for i=1, a do + text = char..text..char + end + return #text == xDim and text or text..char --If not full length, add a space +end +local function leftRight(first, second, dim) + return alignL(tostring(first),dim-#tostring(second))..tostring(second) +end +local function roundNegative(num) --Rounds numbers up to 0 + if num >= 0 then return num else return 0 end +end + + +local function testPeripheral(periph, periphFunc) + if type(periph) ~= "table" then return false end + if type(periph[periphFunc]) ~= "function" then return false end + if periph[periphFunc]() == nil then --Expects string because the function could access nil + return false + end + return true +end + +local function initModem() --Sets up modem, returns true if modem exists + if not testPeripheral(modem, "isWireless") then + if modemSide then + if peripheral.getType(modemSide) == "modem" then + modem = peripheral.wrap(modemSide) + if modem.isWireless and not modem.isWireless() then --Apparently this is a thing + modem = nil + return false + end + return true + end + end + if peripheral.find then + modem = peripheral.find("modem", function(side, obj) return obj.isWireless() end) + end + return modem and true or false + end + return true +end + +--COLOR/THEME RELATED +for a, b in pairs(colors) do --This is so commands color commands can be entered in one case + colors[a:lower()] = b +end +colors.none = 0 --For adding things + +local requiredColors = {"default","title", "subtitle", "pos", "dim", "extra", "error", "info", "inverse", "command", "help", "background"} + +local function checkColor(name, text, back) --Checks if a given color works + local flag = false + for a, b in ipairs(requiredColors) do + if b == name then + flag = true + break + end + end + if not flag or not (tonumber(text) or colors[text]) or not (tonumber(back) or colors[back]) then return false end + return true +end + + +local themes = {} --Loaded themes, gives each one a names +local function newTheme(name) + name = name:lower() or "none" + local self = {name = name} + self.addColor = function(self, colorName, text, back) --Colors are optional. Will default to "default" value. Make sure default is a color + if colorName == "default" and (not text or not back) then return self end + if not text then text = 0 end + if not back then back = 0 end + if not checkColor(colorName, text, back) then debug("Color check failed: ",name," ",text," ",back); return self end --Back or black because optional + colorName = colorName or "none" + self[colorName] = {text = text, background = back} + return self --Allows for chaining :) + end + themes[name] = self + return self +end + +local function parseTheme(file) + local addedTheme = newTheme(file:match("^.-\n") or "newTheme") --Initializes the new theme to the first line + file:sub(file:find("\n") or 1) --If there is a newLine, this cuts everything before it. I don't care that the newLine is kept + for line in file:gmatch("[^\n]+\n") do --Go through all the color lines (besides first one) + local args = {} + for word in line:gmatch("%S+") do + table.insert(args,word) + end + addedTheme:addColor(args[1]:match("%a+") or "nothing", tonumber(args[2]) or colors[args[2]], tonumber(args[3]) or colors[args[3]]) --"nothing" will never get used, so its just lazy error prevention + end + local flag = true --Make sure a theme has all required elements + for a,b in ipairs(requiredColors) do + if not addedTheme[b] then + flag = false + debug("Theme is missing color '",b,"'") + end + end + if not flag then + themes[addedTheme.name] = nil + debug("Failed to load theme") + return false + end + return addedTheme +end +--This is how adding colors will work +--regex for adding from file: +--(\w+) (\w+) (\w+) +-- \:addColor\(\"\1\"\, \2\, \3\) + + +newTheme("default") + :addColor("default",colors.white, colors.black) + :addColor("title", colors.green, colors.gray) + :addColor("subtitle", colors.white, colors.black) + :addColor("pos", colors.green, colors.black) + :addColor("dim", colors.lightBlue, colors.black) + :addColor("extra", colors.lightGray, colors.black) + :addColor("error", colors.red, colors.white) + :addColor("info", colors.blue, colors.lightGray) + :addColor("inverse", colors.yellow, colors.blue) + :addColor("command", colors.lightBlue, colors.black) + :addColor("help", colors.cyan, colors.black) + :addColor("background", colors.none, colors.none) + +newTheme("blue") + :addColor("default",colors.white, colors.blue) + :addColor("title", colors.lightBlue, colors.gray) + :addColor("subtitle", 1, 2048) + :addColor("pos", 16, 2048) + :addColor("dim", colors.lime, 0) + :addColor("extra", 8, 2048) + :addColor("error", 8, 16384) + :addColor("info", 2048, 256) + :addColor("inverse", 2048, 1) + :addColor("command", 2048, 8) + :addColor("help", 16384, 1) + :addColor("background", 1, 2048) + +newTheme("seagle") + :addColor("default",colors.white, colors.black) + :addColor("title", colors.white, colors.black) + :addColor("subtitle", colors.red, colors.black) + :addColor("pos", colors.gray, colors.black) + :addColor("dim", colors.lightBlue, colors.black) + :addColor("extra", colors.lightGray, colors.black) + :addColor("error", colors.red, colors.white) + :addColor("info", colors.blue, colors.lightGray) + :addColor("inverse", colors.yellow, colors.lightGray) + :addColor("command", colors.lightBlue, colors.black) + :addColor("help", colors.red, colors.white) + :addColor("background", colors.white, colors.black) + +newTheme("random") + :addColor("default",colors.white, colors.black) + :addColor("title", colors.pink, colors.blue) + :addColor("subtitle", colors.black, colors.white) + :addColor("pos", colors.green, colors.black) + :addColor("dim", colors.lightBlue, colors.black) + :addColor("extra", colors.lightGray, colors.lightBlue) + :addColor("error", colors.white, colors.yellow) + :addColor("info", colors.blue, colors.lightGray) + :addColor("inverse", colors.yellow, colors.lightGray) + :addColor("command", colors.green, colors.lightGray) + :addColor("help", colors.white, colors.yellow) + :addColor("background", colors.white, colors.red) + +newTheme("rainbow") + :addColor("dim", 32, 0) + :addColor("background", 16384, 0) + :addColor("extra", 2048, 0) + :addColor("info", 2048, 0) + :addColor("inverse", 32, 0) + :addColor("subtitle", 2, 0) + :addColor("title", 16384, 0) + :addColor("error", 1024, 0) + :addColor("default", 1, 512) + :addColor("command", 16, 0) + :addColor("pos", 16, 0) + :addColor("help", 2, 0) + +newTheme("green") + :addColor("dim", 16384, 0) + :addColor("background", 0, 0) + :addColor("extra", 2048, 0) + :addColor("info", 32, 256) + :addColor("inverse", 8192, 1) + :addColor("subtitle", 1, 0) + :addColor("title", 8192, 128) + :addColor("error", 16384, 32768) + :addColor("default", 1, 8192) + :addColor("command", 2048, 32) + :addColor("pos", 16, 0) + :addColor("help", 512, 32768) + + +--If you modify a theme a bunch and want to save it +local function saveTheme(theme, fileName) + if not theme or not type(fileName) == "string" then return false end + local file = fs.open(fileName,"w") + if not file then return false end + file.writeLine(fileName) + for a,b in pairs(theme) do + if type(b) == "table" then --If it contains color objects + file.writeLine(a.." "..tostring(b.text).." "..tostring(b.background)) + end + end + file.close() + return true +end + +--BUTTON CLASS +local button = {} + +button.checkPoint = function(buttons, pos) --Returns a command or nil + for a, b in pairs(buttons) do + if pos[2] == b.line then + if pos[1] >= b.xDim[1] and pos[1] <= b.xDim[2] then + return b.command + end + end + end +end + +button.makeLine = function(buttons, sep, xDim) + local toRet = "" + for a, b in ipairs(buttons) do + toRet = toRet..center(b.text, (b.xDim[2]-b.xDim[1]))..sep + end + return toRet:sub(1,-2).."" --Take off the last sep +end + +button.new = function(line, xStart, xEnd, command, display) + local toRet = {} + setmetatable(toRet, {__index = button}) + toRet.line = line + toRet.xDim = {math.min(xStart, xEnd), math.max(xStart, xEnd)} + toRet.command = command + toRet.text = display + return toRet +end + + +--==SCREEN CLASS FUNCTIONS== +local screenClass = {} --This is the class for all monitor/screen objects +screenClass.screens = {} --A simply numbered list of screens +screenClass.sides = {} --A mapping of screens by their side attached +screenClass.channels = {} --A mapping of receiving channels that have screens attached. Used for the receiver part +screenClass.sizes = {{7,18,29,39,50}, {5,12,19} , computer = {51, 19}, turtle = {39,13}, pocket = {26,20}} + +screenClass.setTextColor = function(self, color) --Accepts raw color + if color and self.term.isColor() then + self.textColor = color + self.term.setTextColor(color) + return true + end + return false +end +screenClass.setBackgroundColor = function(self, color) --Accepts raw color + if color and self.term.isColor() then + self.backgroundColor = color + self.term.setBackgroundColor(color) + return true + end + return false +end +screenClass.setColor = function(self, color) --Wrapper, accepts themecolor objects + if type(color) ~= "table" then error("Set color received a non-table",2) end + local text, back = color.text, color.background + if not text or text == 0 then text = self.theme.default.text end + if not back or back == 0 then back = self.theme.default.background end + return self:setTextColor(text) and self:setBackgroundColor(back) +end + +screenClass.themeName = "default" --Setting super for fallback +screenClass.theme = themes.default + +screenClass.rec = { --Initial values for all displayed numbers + label = "Quarry Bot", + id = 1, + percent = 0, + xPos = 0, + zPos = 0, + layersDone = 0, + x = 0, + z = 0, + layers = 0, + openSlots = 0, + mined = 0, + moved = 0, + chestFull = false, + isAtChest = false, + isGoingToNextLayer = false, + foundBedrock = false, + fuel = 0, + volume = 0, + distance = 0, + yPos = 0 +} + +screenClass.new = function(side, receive, themeFile) + local self = {} + setmetatable(self, {__index = screenClass}) --Establish Hierarchy + self.side = side + if side == "computer" then + self.term = term + else + self.term = peripheral.wrap(side) + if not (self.term and peripheral.getType(side) == "monitor") then --Don't create an object if it doesn't exist + if doDebug then + error("No monitor on side "..tostring(side)) + end + self = nil --Save memory? + return false + end + end + + --Channels and ids + self.receive = tonumber(receive) --Receive Channel + self.send = nil --Reply Channel, obtained in handshake + self.id = #screenClass.screens+1 + --Colors + self.themeName = nil --Will be set by setTheme + self.theme = nil + self.isColor = self.term.isColor() --Just for convenience + --Other Screen Properties + self.dim = {self.term.getSize()} --Raw dimensions + --Initializations + self.isDone = false --Flag for when the turtle is done transmitting + self.size = {} --Screen Size, assigned in setSize + self.textColor = colors.white --Just placeholders until theme is loaded and run + self.backColor = colors.black + self.toPrint = {} + self.isComputer = false + self.isTurtle = false + self.isPocket = false + self.acceptsInput = false + self.legacy = false --Whether it expects tables or strings + self.rec = copyTable(screenClass.rec) + + screenClass.screens[self.id] = self + screenClass.sides[self.side] = self + if self.receive then + modem.open(self.receive) --Modem should be defined by the time anything is open + screenClass.channels[self.receive] = self --If anyone ever asked, you could have multiple screens per channel, but its silly if no one ever needs it + end + self:setSize() --Finish Initialization + self:setTheme(themeFile) + return self +end + +screenClass.remove = function(tab) --Cleanup function + if type(tab) == "number" then --Expects table, can take id (for no apparent reason) + tab = screenClass.screens[tab] + end + tab:removeStation() + if tab.side == "REMOVED" then return end + if tab.side == "computer" then error("Tried removing computer screen",2) end --This should never happen + tab:reset() --Clear screen + tab:say("Removed", tab.theme.info, 1) --Let everyone know whats up + screenClass.screens[tab.id] = {side = "REMOVED"} --Not nil because screw up len() + screenClass.sides[tab.side] = nil + tab:removeChannel() +end + +--Init Functions +screenClass.removeChannel = function(self) + self.send = nil + if self.receive then + screenClass.channels[self.receive] = nil + if modem and modem.isOpen(self.receive) then + modem.close(self.receive) + end + self.receive = nil + end + self:setSize() +end + +screenClass.setChannel = function(self, channel) + if self.isStation then return false end --Don't want to set channel station + self:removeChannel() + if type(channel) == "number" then + self.receive = channel + screenClass.channels[self.receive] = self + if modem and not modem.isOpen(channel) then modem.open(channel) end + end + self:setSize() --Sets proper draw function +end + +screenClass.setStation = function(self) --Note: This only changes the "set" methods so that "update" methods remain intact per object :) + self:removeChannel() + if not self.isStation then --Just in case this gets called more than once + self.isStation = true + table.insert(stationsList,self) + end + self:setSize() +end + +screenClass.removeStation = function(self) + if self.isStation then + for i=1, #stationsList do --No IDs so have to do a linear traversal + if stationsList[i] == self then table.remove(stationsList, i) end + end + end + self.isStation = false + self:setSize() +end + +screenClass.setSize = function(self) --Sets screen size + if self.side ~= "computer" and not self.term then self.term = peripheral.wrap(self.side) end + if not self.term.getSize() then --If peripheral is having problems/not there. Don't go further than term, otherwise index nil (maybe?) + debug("There is no term...") + self.updateDisplay = function() end --Do nothing on screen update, overrides class + return true + elseif self.isStation then + self:setStationDisplay() + elseif not self.receive then + self:setBrokenDisplay() --This will prompt user to set channel + elseif self.send then --This allows for class inheritance + self:setNormalDisplay() --In case objects have special updateDisplay methods --Remove function in case it exists, defaults to super + else --If the screen needs to have a handshake display + self:setHandshakeDisplay() + end + self:resetButtons() + self.dim = { self.term.getSize()} + local tab = screenClass.sizes + for a=1, 2 do --Want x and y dim + for b=1, #tab[a] do --Go through all normal sizes, x and y individually + if tab[a][b] <= self.dim[a] then --This will set size higher until false + self.size[a] = b + end + end + end + local function isThing(toCheck, thing) --E.G. isThing(self.dim,"computer") + return toCheck[1] == tab[thing][1] and toCheck[2] == tab[thing][2] + end + self.isComputer = isThing(self.dim, "computer") + self.isTurtle = isThing(self.dim, "turtle") + self.isPocket = isThing(self.dim, "pocket") + self.acceptsInput = self.isComputer or self.isTurtle or self.isPocket + return self +end + +screenClass.setTheme = function(self, themeName, stopReset) + if not themes[themeName] then --If we don't have it already, try to load it + local fileName = themeName or ".." --.. returns false and I don't think you can name a file this + if fs.exists(themeFolder) then fileName = themeFolder..fileName end + if fs.exists(fileName) then + debug("Loading theme: ",fileName) + local file = fs.open(fileName, "r") + if not file then debug("Could not load theme '",themeName,"' file not found") end + parseTheme(file.readAll()) --Parses the text to make a theme, returns theme + file.close() + self.themeName = themeName:lower() --We can now set our themeName to the fileName + else + --Resets theme to super + if not stopReset then --This exists so its possible to set default theme without breaking world + self.themeName = nil + self.theme = nil + end + return false + end + else + self.themeName = themeName:lower() + end + self.theme = themes[self.themeName] --Now the theme is loaded or the function doesn't get here + return true +end + +--Adds text to the screen buffer +screenClass.tryAddRaw = function(self, line, text, color, ...) --This will try to add text if Y dimension is a certain size + local doAdd = {...} --booleans for small, medium, and large + if type(text) ~= "string" then error("tryAddRaw got "..type(text)..", expected string",2) end + if not text then + debug("tryAddRaw got no string on line ",line) + return false + end + if type(color) ~= "table" then error("tryAddRaw did not get a color",2) end + --color = color or {text = colors.white} + for i=1, ySizes do --As of now there are 3 Y sizes + local test = doAdd[i] + if test == nil then test = doAdd[#doAdd] end --Set it to the last known setting if doesn't exist + if test and self.size[2] == i then --If should add this text for this screen size and the monitor is this size + if #text <= self.dim[1] then + self.toPrint[line] = {text = text, color = color} + return true + else + debug("Tried adding '",text,"' on line ",line," but was too long: ",#text," vs ",self.dim[1]) + end + end + end + return false +end +screenClass.tryAdd = function(self, text, color,...) --Just a wrapper + return self:tryAddRaw(#self.toPrint+1, text, color, ...) +end +screenClass.tryAddC = function(self, text, color, ...) --Centered text + return self:tryAdd(center(text, self.dim[1]), color, ...) +end + +screenClass.reset = function(self,color) + color = color or self.theme.background + self:setColor(color) + self.term.clear() + self.term.setCursorPos(1,1) +end +screenClass.say = function(self, text, color, line) + local currColor = self.backgroundColor + color = color or debug("Printing ",text," but had no themeColor: ",self.theme.name) or {} --Set default for nice error, alert that errors occur + self:setColor(color) + local line = line or ({self.term.getCursorPos()})[2] or self:setSize() or 1 --If current yPos not found, sets screen size and moves cursor to 1 + if doDebug and #text > self.dim[1] then error("Tried printing: '"..text.."', but was too big") end + self.term.setCursorPos(1,line) + for i=1, self.dim[1]-#text do --This is so the whole line's background gets filled. + text = text.." " + end + self.term.write(text) + self.term.setCursorPos(1, line+1) +end +screenClass.pushScreenUpdates = function(self) + for i=1, self.dim[2] do + local tab = self.toPrint[i] + if tab then + self:say(tab.text, tab.color, i) + end + end + self.term.setCursorPos(1,self.dim[2]) --So we can see errors +end +screenClass.resetButtons = function(self) + self.buttons = {} +end +screenClass.addButton = function(self, button) + self.buttons[#self.buttons+1] = button +end + +screenClass.updateNormal = function(self) --This is the normal updateDisplay function + local str = tostring + self.toPrint = {} --Reset table + local message, theme, x = self.rec, self.theme, self.dim[1] + if not self.isDone then --Normally + + + if self.size[1] == 1 then --Small Width Monitor + if not self:tryAdd(message.label, theme.title, false, false, true) then --This will be a title, basically + self:tryAdd("Quarry!", theme.title, false, false, true) + end + + self:tryAdd("-Fuel-", theme.subtitle , false, true, true) + if not self:tryAdd(str(message.fuel), theme.extra, false, true, true) then --The fuel number may be bigger than the screen + self:tryAdd("A lot", theme.extra, false, true, true) + end + + self:tryAdd("--%%%--", theme.subtitle, false, true, true) + self:tryAdd(alignR(str(message.percent).."%", 7), theme.pos , false, true, true) --This can be an example. Print (receivedMessage).percent in blue on all different screen sizes + self:tryAdd(center(str(message.percent).."%", x), theme.pos, true, false) --I want it to be centered on 1x1 + + self:tryAdd("--Pos--", theme.subtitle, false, true, true) + self:tryAdd("X:"..alignR(str(message.xPos), 5), theme.pos, true) + self:tryAdd("Z:"..alignR(str(message.zPos), 5), theme.pos , true) + self:tryAdd("Y:"..alignR(str(message.layersDone), 5), theme.pos , true) + + if not self:tryAdd(str(message.x).."x"..str(message.z).."x"..str(message.layers), theme.dim , true, false) then --If you can't display the y, then don't + self:tryAdd(str(message.x).."x"..str(message.z), theme.dim , true, false) + end + self:tryAdd("--Dim--", theme.subtitle, false, true, true) + self:tryAdd("X:"..alignR(str(message.x), 5), theme.dim, false, true, true) + self:tryAdd("Z:"..alignR(str(message.z), 5), theme.dim, false, true, true) + self:tryAdd("Y:"..alignR(str(message.layers), 5), theme.dim, false, true, true) + + self:tryAdd("-Extra-", theme.subtitle, false, false, true) + self:tryAdd(alignR(textutils.formatTime(os.time()):gsub(" ","").."", 7), theme.extra, false, false, true) --Adds the current time, formatted, without spaces. + self:tryAdd("Used:"..alignR(str(16-message.openSlots),2), theme.extra, false, false, true) + self:tryAdd("Dug"..alignR(str(message.mined), 4), theme.extra, false, false, true) + self:tryAdd("Mvd"..alignR(str(message.moved), 4), theme.extra, false, false, true) + if message.status then + self:tryAdd(alignL(message.status, x), theme.info, false, false, true) + end + if message.chestFull then + self:tryAdd("ChstFll", theme.error, false, false, true) + end + + end + if self.size[1] == 2 then --Medium Monitor + if not self:tryAdd(message.label, theme.title, false, false, true) then --This will be a title, basically + self:tryAdd("Quarry!", theme.title, false, false, true) + end + + self:tryAdd(center("Fuel",x,"-"), theme.subtitle , false, true, true) + if not self:tryAdd(str(message.fuel), theme.extra, false, true, true) then --The fuel number may be bigger than the screen + self.toPrint[#self.toPrint] = nil + self:tryAdd("A lot", theme.extra, false, true, true) + end + + self:tryAdd(str(message.percent).."% Complete", theme.pos , true) --This can be an example. Print (receivedMessage).percent in blue on all different screen sizes + + self:tryAdd(center("Pos",x,"-"), theme.subtitle, false, true, true) + self:tryAdd(leftRight("X Coordinate:",message.xPos, x), theme.pos, true) + self:tryAdd(leftRight("Z Coordinate:",message.zPos, x), theme.pos , true) + self:tryAdd(leftRight("On Layer:",message.layersDone, x), theme.pos , true) + + if not self:tryAdd("Size: "..str(message.x).."x"..str(message.z).."x"..str(message.layers), theme.dim , true, false) then --This is already here... I may as well give an alternative for those people with 1000^3quarries + self:tryAdd(str(message.x).."x"..str(message.z).."x"..str(message.layers), theme.dim , true, false) + end + self:tryAdd(center("Dim",x,"-"), theme.subtitle, false, true, true) + self:tryAdd(leftRight("Total X:", message.x, x), theme.dim, false, true, true) + self:tryAdd(leftRight("Total Z:", message.z, x), theme.dim, false, true, true) + self:tryAdd(leftRight("Total Layers:", message.layers, x), theme.dim, false, true, true) + self:tryAdd(leftRight("Volume", message.volume, x), theme.dim, false, false, true) + + self:tryAdd(center("Extras",x,"-"), theme.subtitle, false, false, true) + self:tryAdd(leftRight("Time: ", textutils.formatTime(os.time()):gsub(" ","").."", x), theme.extra, false, false, true) --Adds the current time, formatted, without spaces. + self:tryAdd(leftRight("Used Slots:", 16-message.openSlots, x), theme.extra, false, false, true) + self:tryAdd(leftRight("Blocks Mined:", message.mined, x), theme.extra, false, false, true) + self:tryAdd(leftRight("Spaces Moved:", message.moved, x), theme.extra, false, false, not self.isPocket) + if message.status then + self:tryAdd(message.status, theme.info, false, false, true) + end + if message.chestFull then + self:tryAdd("Chest Full, Fix It", theme.error, false, true, true) + end + end + if self.size[1] >= 3 then --Large or larger screens + if not self:tryAdd(message.label..alignR(" Turtle #"..str(message.id),x-#message.label), theme.title, true) then + self:tryAdd("Your turtle's name is long...", theme.title, true) + end + self:tryAdd("Fuel: "..alignR(str(message.fuel),x-6), theme.extra, true) + + self:tryAdd("Percentage Done: "..alignR(str(message.percent).."%",x-17), theme.pos, true) + + local var1 = math.max(#str(message.x), #str(message.z), #str(message.layers)) + local var2 = (x-6-var1+3)/3 + self:tryAdd("Pos: "..alignR(" X:"..alignR(str(message.xPos),var1),var2)..alignR(" Z:"..alignR(str(message.zPos),var1),var2)..alignR(" Y:"..alignR(str(message.layersDone),var1),var2), theme.pos, true) + self:tryAdd("Size:"..alignR(" X:"..alignR(str(message.x),var1),var2)..alignR(" Z:"..alignR(str(message.z),var1),var2)..alignR(" Y:"..alignR(str(message.layers),var1),var2), theme.dim, true) + self:tryAdd("Volume: "..str(message.volume), theme.dim, false, true, true) + self:tryAdd("",{}, false, false, true) + self:tryAdd(center("____---- EXTRAS ----____",x), theme.subtitle, false, false, true) + self:tryAdd(center("Time:"..alignR(textutils.formatTime(os.time()),10), x), theme.extra, false, true, true) + self:tryAdd(center("Current Day: "..str(os.day()), x), theme.extra, false, false, true) + self:tryAdd("Used Inventory Slots: "..alignR(str(16-message.openSlots),x-22), theme.extra, false, true, true) + self:tryAdd("Blocks Mined: "..alignR(str(message.mined),x-14), theme.extra, false, true, true) + self:tryAdd("Blocks Moved: "..alignR(str(message.moved),x-14), theme.extra, false, true, true) + self:tryAdd("Distance to Turtle: "..alignR(str(message.distance), x-20), theme.extra, false, false, true) + self:tryAdd("Actual Y Pos (Not Layer): "..alignR(str(message.yPos), x-26), theme.extra, false, false, true) + + if message.chestFull then + self:tryAdd("Dropoff is Full, Please Fix", theme.error, false, true, true) + end + if message.foundBedrock then + self:tryAdd("Found Bedrock! Please Check!!", theme.error, false, true, true) + end + if message.status then + self:tryAdd("Status: "..message.status, theme.info, false, true, true) + end + if message.isAtChest then + self:tryAdd("Turtle is at home chest", theme.info, false, true, true) + end + if message.isGoingToNextLayer then + self:tryAdd("Turtle is going to next layer", theme.info, false, true, true) + end + + + + end + if self.term.isColor() and ((self.size[2] >= 2 and self.size[1] >= 3) or self.isPocket) then + local line = self.acceptsInput and self.dim[2]-1 or self.dim[2] + local part = math.floor(x/4) + if #self.buttons == 0 then + self:addButton(button.new(line, part*0, part*1-1, "drop","Drop")) + self:addButton(button.new(line, part*1, part*2-1, "pause","Pause")) + self:addButton(button.new(line, part*2, part*3-1, "return","Return")) + self:addButton(button.new(line, part*3, part*4-1, "refuel","Refuel")) + end + self:tryAddRaw(line, button.makeLine(self.buttons,"|"):sub(1,self.isPocket and -2 or -1), theme.command, false, true) --Silly code because pocket breaks + end + else --If is done + if self.size[1] == 1 then --Special case for small monitors + self:tryAdd("Done", theme.title, true) + if not self:tryAdd("Dug"..alignR(str(message.mined),4, false), theme.pos, true) then + self:tryAdd("Dug", theme.pos, true) + self:tryAdd(alignR(str(message.mined),x), theme.pos, true) + end + if not self:tryAdd("Fuel"..alignR(str(message.fuel),3, false), theme.pos, true) then + self:tryAdd("Fuel", theme.pos, true) + self:tryAdd(alignR(str(message.fuel),x), theme.pos, true) + end + self:tryAdd("-------", theme.subtitle, false,true,true) + self:tryAdd("Turtle", theme.subtitle, false, true, true) + self:tryAdd(center("is", x), theme.subtitle, false, true, true) + self:tryAdd(center("Done!", x), theme.subtitle, false, true, true) + else + self:tryAdd("Done!", theme.title, true) + self:tryAdd("Curr Fuel: "..str(message.fuel), theme.pos, true) + if message.preciseTotals then + local tab = {} + for a,b in pairs(message.preciseTotals) do --Sorting the table + a = a:match(":(.+)") + if #tab == 0 then --Have to initialize or rest does nothing :) + tab[1] = {a,b} + else + for i=1, #tab do --This is a really simple sort. Probably not very efficient, but I don't care. + if b > tab[i][2] then --Gets the second value from the table, which is the itemCount + table.insert(tab, i, {a,b}) + break + elseif i == #tab then --Insert at the end if not bigger than anything + table.insert(tab,{a,b}) + end + end + end + end + for i=1, #tab do --Print all the blocks in order + local firstPart = "#"..tab[i][1]..": " + self:tryAdd(firstPart..alignR(tab[i][2], x-#firstPart), (i%2 == 0) and theme.inverse or theme.info, true, true, true) --Switches the colors every time + end + else + self:tryAdd("Blocks Dug: "..str(message.mined), theme.inverse, true) + self:tryAdd("Cobble Dug: "..str(message.cobble), theme.pos, false, true, true) + self:tryAdd("Fuel Dug: "..str(message.fuelblocks), theme.pos, false, true, true) + self:tryAdd("Others Dug: "..str(message.other), theme.pos, false, true, true) + end + end + end +end +screenClass.updateHandshake = function(self) + self.toPrint = {} + local half = math.ceil(self.dim[2]/2) + if self.size[1] == 1 then --Not relying on the parameter system because less calls + self:tryAddRaw(half-2, "Waiting", self.theme.error, true) + self:tryAddRaw(half-1, "For Msg", self.theme.error, true) + self:tryAddRaw(half, "On Chnl", self.theme.error, true) + self:tryAddRaw(half+1, tostring(self.receive), self.theme.error, true) + else + local str = "for" + if self.size[1] == 2 then str = "4" end--Just a small grammar change + self:tryAddRaw(half-2, "", self.theme.error, true) --Filler + self:tryAddRaw(half-1, center("Waiting "..str.." Message", self.dim[1]), self.theme.error, true) + self:tryAddRaw(half, center("On Channel "..tostring(self.receive), self.dim[1]), self.theme.error, true) + self:tryAddRaw(half+1, "",self.theme.error, true) + end +end +screenClass.updateBroken = function(self) --If screen needs channel + self.toPrint = {} + if self.size[1] == 1 then + self:tryAddC("No Rec", self.theme.pos, false, true, true) + self:tryAddC("Channel", self.theme.pos, false, true, true) + self:tryAddC("-------", self.theme.title, false, true, true) + self:tryAddC("On Comp", self.theme.info, true) + self:tryAddC("Type:", self.theme.info, true) + self:tryAddC("RECEIVE", self.theme.command, true) + if not self:tryAddC(self.side:upper(), self.theme.command, true) then --If we can't print the full side + self:tryAddC("[side]",self.theme.command, true) + end + self:tryAddC("[Chnl]", self.theme.command, true) + else + self:tryAddC("No receiving", self.theme.pos, false, true, true) + self:tryAddC("channel for", self.theme.pos, false, true, true) + self:tryAddC("this screen", self.theme.pos, false, true, true) + self:tryAddC("-----------------", self.theme.title, false, true, true) + self:tryAddC("On main computer,", self.theme.info, true) + self:tryAddC("Type:", self.theme.info, true) + self:tryAdd("", self.theme.command, false, true, true) + self:tryAddC('"""', self.theme.command, false, true, true) + self:tryAddC("RECEIVE", self.theme.command, true) + if not self:tryAddC(self.side:upper(), self.theme.command, true) then --If we can't print the full side + self:tryAddC("[side]",self.theme.command, true) + end + self:tryAddC("[desired channel]", self.theme.command, true) + self:tryAddC('"""', self.theme.command, false, true, true) + end +end +screenClass.updateStation = function(self) + self.toPrint = {} + sepChar = "| " + local part = math.floor((self.dim[1]-3*#sepChar - 3)/3) + self:tryAdd(alignL("ID",3)..sepChar..alignL("Side",part)..sepChar..alignL("Channel",part)..sepChar..alignL("Theme",part), self.theme.title, true, true, true)--Headings + local line = "" + for i=1, self.dim[1] do line = line.."-" end + self:tryAdd(line, self.theme.title, false, true, true) + for a,b in ipairs(screenClass.screens) do + if b.side ~= "REMOVED" then + self:tryAdd(alignL(b.id,3)..sepChar..alignL(b.side,part)..sepChar..alignL(b.receive, part)..sepChar..alignL(b.theme.name,part), self.theme.info, true, true, true)--Prints info about all screens + end + end +end + +screenClass.updateDisplay = screenClass.updateNormal --Update screen method is normally this one + +--Misc +screenClass.setNormalDisplay = function(self) + self.updateDisplay = self.updateNormal --This defaults to super if doesn't exist +end +screenClass.setHandshakeDisplay = function(self) + self.updateDisplay = self.updateHandshake --Sets update to handshake version, defaults to super if doesn't exist +end +screenClass.setBrokenDisplay = function(self) + self.updateDisplay = self.updateBroken +end +screenClass.setStationDisplay = function(self) + self.updateDisplay = self.updateStation +end + +--Help Function. Goes so low so can see screenClass.theme +local function displayHelp() + local dummy = {term = term} --This will be a dummy "screnClass object" for setting color + setmetatable(dummy, {__index = screenClass}) + local theme = dummy.theme + local tab = {} + local indexOuter = "main" + local indexInner = 1 + for key, value in pairs(helpResources) do + tab[key] = {} + for a in value:gmatch("$$([^$]+)") do + table.insert(tab[key], a) --Just inserting pages + end + end + while true do + dummy:setColor(theme.help) + clearScreen(1,2) + print(tab[indexOuter][indexInner]:match("\n(.+)")) --Print all but first line + dummy:setColor(theme.title) + dummy.term.setCursorPos(1,1) + print(alignL(tab[indexOuter][indexInner]:match("[^\n]+") or "",({dummy.term.getSize()})[1])) --Print first line + dummy:setColor(theme.info) + local text = tostring(indexInner).."/"..tostring(#tab[indexOuter]) + term.setCursorPos(({term.getSize()})[1]-#text,1) + term.write(text) --Print the current page number + local event, key = os.pullEvent("key") + key = keyMap[key] + if tonumber(key) and tab[tonumber(key)] then + indexOuter = tonumber(key) + indexInner = 1 + elseif key == "Q" then + os.pullEvent("char") --Capture extra event (note: this always works because only q triggers this) + return true + elseif key == "0" then --Go back to beginning + indexOuter, indexInner = "main",1 + elseif key == "up" and indexInner > 1 then + indexInner = indexInner-1 + elseif key == "down" and indexInner < #tab[indexOuter] then + indexInner = indexInner + 1 + end + end + +end + + +local function wrapPrompt(prefix, str, dim) --Used to wrap the commandString + return prefix..str:sub(roundNegative(#str+#prefix-computer.dim[1]+2), -1).."_" --it is str + 2 because we add in the "_" +end + +local function updateAllScreens() + for a, b in pairs(screenClass.sides) do + b:updateDisplay() + b:reset() + b:pushScreenUpdates() + end +end +--Rednet +local function newMessageID() + return math.random(1,2000000000) --1 through 2 billion. Good enough solution +end +local function transmit(send, receive, message, legacy, fingerprint) + fingerprint = fingerprint or replyFingerprint + if legacy then + modem.transmit(send, receive, message) + else + modem.transmit(send, receive, {message = message, id = newMessageID(), fingerprint = fingerprint}) + end +end + +--QuadRotor +local function launchQuad(message) + if quadEnabled and message.emergencyLocation then --This means the turtle is out of fuel. Also that it sent its two initial positions + local movement = {} + local function add(what) table.insert(movement,what) end + add(quadDirection) --Get to the fuel chest + add("suck") + add(quadDirection) --So it can properly go down/up first + local function go(dest, orig, firstMove) --Goes to a place. firstMove because I'm lazy. Its for getting away from computer. If false, its the second move so go one above turtle. If nothing then nothing + local distX, distY, distZ = dest[1]-orig[1], dest[2]-orig[2], dest[3]-orig[3] + if firstMove then + distX = distX - 3 * (quadDirection == "east" and 1 or (quadDirection == "west" and -1 or 0)) + distZ = distZ - 3 * (quadDirection == "south" and 1 or (quadDirection == "north" and -1 or 0)) + distY = distY - 1 --Because the quad is a block above the first thing + elseif firstMove == false then + local num = 2 + if message.layersDone <= 1 then + num = 1 + end + distY = distY + num * (distY < 0 and 1 or -1) --This is to be above the turtle and accounts for invert + end + add((distY > 0 and "up" or "down").." "..tostring(math.abs(distY))) + add((distX > 0 and "east" or "west").." "..tostring(math.abs(distX))) + add((distZ > 0 and "south" or "north").." "..tostring(math.abs(distZ))) + if firstMove == false and message.layersDone > 1 then + add(distY < 0 and "down" or "up") --This is so it goes into the turtle's proper layer (invert may or may not work, actually) + end + end + debug("Location Types") + debug(computerLocation) + debug(message.firstPos) + debug(message.secondPos) + debug(message.emergencyLocation) + go(message.firstPos, computerLocation, true) --Get to original position of turtle + go(message.secondPos,message.firstPos) --Get into quarry + go(message.emergencyLocation, message.secondPos, false) + + add("drop") + add("return") + for a,b in pairs(movement) do + debug(a," ",b) + end + quadBase.flyQuad(movement) --Note, if there are no quadrotors, nothing will happen and the turtle will sit forever + + end +end + +--==SET UP== +clearScreen() +print("Welcome to Quarry Receiver!") +sleep(1) + +--==ARGUMENTS== + +--[[ +Parameters: + -help/-?/help/? + -v/verbose --Turn on debugging + -receiveChannel/channel [channel] --For only the main screen + -theme --Sets a default theme + -screen [side] [channel] [theme] + -station + -auto --Prompts for all sides, or you can supply a list of receive channels for random assignment! + -colorEditor + -quad [cardinal direction] --This looks for a quadrotor from the quadrotors mod. The direction is of the fuel chest. + -autoRestart --Will reset any attached screen when done, instead of bricking them +]] + +--tArgs init +local parameters = {} --Each command is stored with arguments + +local function addParam(value) + val = value:lower() + if val:match("^%-") then + parameters[#parameters+1] = {val:sub(2)} --Starts a chain with the command. Can be unpacked later + parameters[val:sub(2)] = {} --Needed for force/before/after parameters + elseif parameterIndex ~= 0 then + table.insert(parameters[#parameters], value) --value because arguments should be case sensitive for filenames + table.insert(parameters[parameters[#parameters][1]], value) --Needed for force/after parameters + end +end + +for a,b in ipairs(tArgs) do + addParam(b) +end + +if parameters.theme then --This goes here so help can display in different theme :) + screenClass:setTheme(parameters.theme[1]) +end + +for a,b in ipairs(tArgs) do + val = b:lower() + if val == "help" or val == "-help" or val == "?" or val == "-?" or val == "usage" or val == "-usage" then + displayHelp() --To make + error("The End of Help",0) + end +end + +--Debug parameters +if parameters.v or parameters.verbose then --Why not + doDebug = true +end + +for i=1,#parameters do + debug("Parameter: ",parameters[i][1]) +end + +--Options before screen loads + +if parameters.modem then + modemSide = parameters.modem[1] +end + +if parameters.quad then + if not parameters.quad[1] then parameters.quad[1] = "direction doesn't exist" end + local dir = parameters.quad[1]:lower():sub(1,1) + if quadDirections[dir] then + quadEnabled = true + quadDirection = quadDirections[dir] + else + clearScreen() + print("Please specify the cardinal direction your quad station is in") + print("Make sure you have a quad station on one side with a chest behind it, forming a line") + print("Like this: [computer] [station] [fuel chest]") + print("The program will now terminate") + error("",0) + end +end + +if parameters.autorestart then + local val = parameters.autorstart[1] + if not val then + autoRestart = true --Assume no value = force true + else + val = val:sub(1,1):lower() + autoRestart = not (val == "n" or val == "f") + end +end + +--Init Modem +while not initModem() do + clearScreen() + print("No modem is connected, please attach one") + if not peripheral.find then + print("What side was that on?") + modemSide = read() + else + os.pullEvent("peripheral") + end +end +debug("Modem successfully connected!") + +local function autoDetect(channels) + if type(channels) ~= "table" then channels = {} end + local tab = peripheral.getNames() + local index = 1 + for i=1, #tab do + if peripheral.getType(tab[i]) == "monitor" and not screenClass.sides[tab[i]] then + screenClass.new(tab[i], channels[index]) --You can specify a list of channels in "auto" parameter + index = index+1 + end + end +end + +--Init QuadRotor Station +if quadEnabled then + local flag + while not flag do + for a,b in ipairs({"front","back","left","right","top"}) do + if peripheral.isPresent(b) and peripheral.getType(b) == "quadbase" then + quadBase = peripheral.wrap(b) + end + end + clearScreen() + if not quadBase then + print("No QuadRotor Base Attached, please attach one") + elseif quadBase.getQuadCount() == 0 then + print("Please install at least one QuadRotor in the base") + sleep(1) --Prevents screen flickering and overcalling gps + else + flag = true + debug("QuadBase successfully connected!") + end + if not computerLocation and not gps.locate(5) then + flag = false + error("No GPS lock. Please make a GPS network to use quadrotors") + else + computerLocation = {gps.locate(5)} + debug("GPS Location Acquired") + end + end +end + +--Init Computer Screen Object (was defined at top) +computer = screenClass.new("computer", (parameters.receivechannel and parameters.receivechannel[1]) or (parameters.channel and parameters.channel[1]))--This sets channel, checking if parameter exists +computer.updateNormal = function(self) + screenClass.updateNormal(self) + computer:displayCommand() +end +computer.updateHandshake = function(self) --Not in setHandshake because that func checks object updateHandshake + screenClass.updateHandshake(self) + computer:displayCommand() +end +computer.updateBroken = function(self) + screenClass.updateBroken(self) + computer:displayCommand() +end +computer.updateStation = function(self)--This gets set in setSize + screenClass.updateStation(self) + self:displayCommand() +end + + +for i=1, #parameters do --Do actions for parameters that can be used multiple times + local command, args = parameters[i][1], parameters[i] --For ease + if command == "screen" then + if not screenClass.sides[args[2]] then --Because this screwed up the computer + local a = screenClass.new(args[2], args[3], args[4]) + debug(type(a)) + else + debug("Overwriting existing screen settings for '",args[2],"'") + local a = screenClass.sides[args[2]] + a:setChannel(tonumber(args[3])) + a:setTheme(args[4]) + end + end + if command == "station" then --This will set the screen update to display stats on all other monitors + if not args[2] or args[2]:lower() == "computer" then --Not below because it exists + computer:setStation() --This handles setting updateNormal, setHandshakeDisplay, etc + else + local a = screenClass.new(args[2], nil, args[3]) --This means syntax is -station [side] [theme] + if a then --If the screen actually exists + a:setStation() + end + end + end +end + +if parameters.auto then --This must go after computer declaration so computer ID is 1 + autoDetect(parameters.auto) + addParam("-station") --Set computer as station + addParam("computer") --Yes, I'm literally just feeding in more tArgs like from IO +end + +computer.displayCommand = function(self) + local sideString = ((defaultSide and " (") or "")..(defaultSide or "")..((defaultSide and ")") or "") + if self.size == 1 then + self:tryAddRaw(self.dim[2], wrapPrompt("Cmd"..sideString:sub(2,-2)..": ", commandString, self.dim[1]), self.theme.command, true) + else + self:tryAddRaw(self.dim[2], wrapPrompt("Command"..sideString..": ",commandString, self.dim[1]), self.theme.command, true) --This displays the last part of a string. + end +end +--Initializing the computer screen +if parameters.coloreditor then + + computer:removeChannel() --So it doesn't receive messages + computer.isStation = true --So we can't assign a channel + + computer.updateNormal = function(self) --This is only for editing colors + self.toPrint = {} + for i=1, #requiredColors do + self:tryAdd(requiredColors[i], self.theme[requiredColors[i]],true) + end + self:displayCommand() + end + computer.updateHandshake = computer.updateNormal + computer.updateBroken = computer.updateNormal + computer.updateStation = computer.updateNormal +end +computer:setSize() --Update changes made to display functions + +for a,b in pairs(screenClass.sides) do debug(a) end + +--==FINAL CHECKS== + +--If only one screen and computer has no channel, make it a station +if #screenClass.screens > 1 and not computer.receive then + debug("Only one screen, no comp channel. Setting station") + computer:setStation() +end + +--Updating all screen for first time and making sure channels are open +for a, b in pairs(screenClass.sides) do + b:setSize() + b:updateDisplay()--Finish initialization process + b:reset() + b:pushScreenUpdates() +end + +--Handshake will be handled in main loop + +--[[Workflow + Wait for events + modem_message + if valid channel and valid message, update appropriate screen + key + if any letter, add to command string if room. + if enter key + if valid self command, execute command. Commands: + command [side] [command] --If only one screen, then don't need channel. Send a command to a turtle + screen [side] [channel] [theme] --Links a new screen to use. + remove [side] --Removes a screen + theme [themeName] --Sets the default theme + theme [side] [themeName] --Changes this screen's theme + savetheme [new name] [themeName] + color [side/theme] [colorName] [textColor] [backgroundColor] + side [side] --Sets a default side, added to prompts + set [string] --Sets a default command, added to display immediately + receive [side] [newChannel] --Changes the channel of the selected screen + send [side] [newChannel] + auto --Automatically adds screens not connected + station --Sets the selected screen as a station (or resets if already a station) + exit/quit/end + peripheral_detach + check what was lost, if modem, set to nil. If screen side, do screen:setSize() + peripheral + check if screen side already added + reset screen size + monitor_resize + resize proper screen + monitor_touch + if screen already added + select screen on main computer + else + add screen + +]] + +--Modes: 1 - Sided, 2 - Not Sided, 3 - Both sided and not +local validCommands = {command = 1, screen = 2, remove = 1, theme = 3, exit = 2, quit = 2, ["end"] = 2, color = 3, side = 2, set = 2, receive = 1, send = 1, savetheme = 2, + auto = 2, verbose = 2, quiet = 2, station = 1} +while continue do + local event, par1, par2, par3, par4, par5 = os.pullEvent() + ----MESSAGE HANDLING---- + if event == "modem_message" and screenClass.channels[par2] then --If we got a message for a screen that exists + local screen = screenClass.channels[par2] --For convenience + if not screen.send then --This is the handshake + debug("\nChecking handshake. Received: ",par4) + local flag = false + if par4 == expectedMessage then --Legacy quarries don't accept receiver dropping in mid-run + screen.legacy = true --Accepts serialized tables + flag = true + elseif type(par4) == "table" and par4.fingerprint == expectedFingerprint then --Don't care about expected message, allows us to start receiver mid-run, fingerprint should be pretty specific + screen.legacy = false + flag = true + end + + if flag and (autoRestart or (not autoRestart and not screen.isDone)) then --We don't accept handshakes when we don't want autorestarts + screen.isDone = false + screen.rec = copyTable(screenClass.rec) --Need to reset this. Existing message from restart doesn't have everything + debug("Screen ",screen.side," received a handshake") + screen.send = par3 + screen:setSize() --Resets update method to proper since channel is set + debug("Sending back on ",screen.send) + transmit(screen.send,screen.receive, replyMessage, screen.legacy) + end + + else --Everything else is for regular messages + + local rec + if screen.legacy then --We expect strings here + if type(par4) == "string" then --Otherwise its not ours + if par4 == "stop" then --This is the stop message. All other messages will be ending ones + screen.isDone = true + elseif par4 == expectedMessage then --We support dropping in mid-run + debug("Screen ",screen.side," received mid-run handshake") + transmit(screen.send,screen.receive, replyMessage, screen.legacy) + elseif textutils.unserialize(par4) then + rec = textutils.unserialize(par4) + rec.distance = par5 + end + end + elseif type(par4) == "table" and par4.fingerprint == expectedFingerprint then --Otherwise, we check if it is valid message + + if type(par4.message) == "table" then + rec = par4.message + if not par4.distance then --This is cool because it can add distances from the repeaters + rec.distance = par5 + else + rec.distance = par4.distance + par5 + end + if rec.isDone then + screen.isDone = true + screen.send = nil --So that we can receive handshakes again. + end + elseif par4.message == expectedMessage then + debug("Screen ",screen.side," received mid-run handshake") + transmit(screen.send,screen.receive, replyMessage, screen.legacy) + else + debug("Message received did not contain table") + end + end + + if rec then + rec.distance = math.floor(rec.distance) + rec.label = rec.label or "Quarry!" + screen.rec = rec --Set the table + --Updating screen occurs outside of the if + local toSend + if screen.queuedMessage then + toSend = screen.queuedMessage + screen.queuedMessage = nil + else + toSend = replyMessage + end + if not screen.isDone then --Because then sendChannel doesn't exist + transmit(screen.send,screen.receive, toSend, screen.legacy) --Send reply message for turtle + end + end + + end + + launchQuad(screen.rec) --Launch the Quad! (This only activates when turtle needs it) + + screen:updateDisplay() --isDone is queried inside this + screen:reset(screen.theme.background) + screen:pushScreenUpdates() --Actually write things to screen + --if screen.isDone and not autoRestart then screen:removeChannel() end --Don't receive any more messages. Allows turtle to think connected. Done after message sending so no error :) + + ----KEY HANDLING---- + elseif event == "key" and keyMap[par1] then + local key = keyMap[par1] + if key ~= "enter" then --If we aren't submitting a command + if key == "backspace" then + if #commandString > 0 then + commandString = commandString:sub(1,-2) + end + elseif key == "up" then + commandString = lastCommand or commandString --Set to last command, or do nothing if it doesn't exist + elseif key == "down" then + commandString = "" --If key down, clear + elseif #key == 1 then + commandString = commandString..key + end + --ALL THE COMMANDS + else --If we are submitting a command + lastCommand = commandString --For using up arrow + local args = {} + for a in commandString:gmatch("%S+") do --This captures all individual words in the command string + args[#args+1] = a:lower() + end + local command = args[1] + if validCommands[command] then --If it is a valid command... + local commandType = validCommands[command] + if commandType == 1 or commandType == 3 then --If the command requires a "side" like transmitting commands, versus setting a default + if defaultSide then table.insert(args, 2, defaultSide) end + local screen + local test = screenClass.screens[tonumber(args[2])] + if test and test.side ~= "REMOVED" then --This way we can specify IDs as well + screen = test + else + screen = screenClass.sides[args[2]] + end + if screen then --If the side exists + if command == "command" and screen.send then --If sending command to the turtle + screen.queuedMessage = table.concat(args," ", 3) --Tells message handler to send appropriate message + --transmit(screen.send, screen.receive, table.concat(args," ", 3), screen.legacy) --This transmits all text in the command with spaces. Duh this is handled when we get message + end + + if command == "color" then + screen.theme:addColor(args[3],colors[args[4]],colors[args[5]] ) + updateAllScreens() --Because we are changing a theme color which others may have + end + if command == "theme" then + screen:setTheme(args[3]) + end + if command == "send" then --This changes a send channel, and can also revert to handshake + local chan = checkChannel(tonumber(args[3]) or -1) + if chan then screen.send = chan else screen.send = nil end + screen:setSize() --If on handshake, resets screen + end + if command == "receive" and not screen.isStation then + local chan = checkChannel(tonumber(args[3]) or -1) + if chan and not screenClass.channels[chan] then + screen:setChannel(chan) + screen:setSize() --Update broken status + end + end + if command == "station" then + if screen.isStation then screen:removeStation() else screen:setStation() end + end + if command == "remove" and screen.side ~= "computer" then --We don't want to remove the main display! + print() + screen:remove() + else --Because if removed it does stupid things + screen:reset() + debug("here") + screen:updateDisplay() + debug("Here") + screen:pushScreenUpdates() + debug("Hereer") + end + end + end + if commandType == 2 or commandType == 3 then--Does not require a screen side + if command == "screen" and peripheral.getType(args[2]) == "monitor" then --Makes sure there is a monitor on the screen side + if not args[3] or not screenClass.channels[tonumber(args[3])] then --Make sure the channel doesn't already exist + local mon = screenClass.new(args[2], args[3], args[4]) + --args[3] is the channel and will set broken display if it doesn't exist + --args[4] is the theme, and will default if doesn't exists. + mon:updateDisplay() + mon:reset() + mon:pushScreenUpdates() + end + end + if command == "theme" then + screenClass:setTheme(args[2], true) --Otherwise this would set base theme to nil, erroring + updateAllScreens() + end + if command == "color" and themes[args[2]] then + themes[args[2]]:addColor(args[3],colors[args[4]],colors[args[5]]) + updateAllScreens() --Because any screen could have this theme + end + if command == "side" then + if screenClass.sides[args[2]] then + defaultSide = args[2] + else + defaultSide = nil + end + end + if command == "set" then + if args[2] then + defaultCommand = table.concat(args," ",2) + defaultCommand = defaultCommand:upper() + else + defaultCommand = nil + end + end + if command == "savetheme" then + if saveTheme(themes[args[2]], args[3]) then + computer:tryAddRaw(computer.dim[2]-1, "Save Theme Succeeded!", computer.theme.inverse, true) + else + computer:tryAddRaw(computer.dim[2]-1, "Save Theme Failed!", computer.theme.inverse, true) + end + computer:reset() + computer:pushScreenUpdates() + sleep(1) + end + if command == "auto" then + local newTab = copyTable(args) --This is so we can pass all additional words as channel numbers + table.remove(newTab, 1) + autoDetect(newTab) + updateAllScreens() + end + if command == "verbose" then doDebug = true end + if command == "quiet" then doDebug = false end + if command == "quit" or command == "exit" or command == "end" then + continue = false + end + end + else + debug("\nInvalid Command") + end + if defaultCommand then commandString = defaultCommand.." " else commandString = "" end --Reset command string because it was sent + end + + + --Update computer display (computer is only one that displays command string + computer:updateDisplay() --Note: Computer's method automatically adds commandString to last line + if not continue then computer:tryAddRaw(computer.dim[2]-1,"Program Exiting", computer.theme.inverse, false, true, true) end + computer:reset() + computer:pushScreenUpdates() + + elseif event == "monitor_resize" then + local screen = screenClass.sides[par1] + if screen then + screen:setSize() + screen:updateDisplay() + screen:reset() + screen:pushScreenUpdates() + end + elseif event == "monitor_touch" then + local screen = screenClass.sides[par1] + debug("Side: ",par1," touched") + if screen then --This part is copied from the "side" command + local test = button.checkPoint(screen.buttons, {par2, par3}) + if test then + screen.queuedMessage = test + else + if not screen.receive then + commandString = "RECEIVE "..par1:upper().." " + end + end + else + debug("Adding Screen") + local mon = screenClass.new(par1) + commandString = "RECEIVE "..mon.side:upper().." " + mon:reset() + mon:updateDisplay() + mon:pushScreenUpdates() + + end + computer:reset() + computer:updateDisplay() + computer:pushScreenUpdates() --Need to update computer for command string + elseif event == "mouse_click" then + screen = computer + local test = button.checkPoint(screen.buttons, {par2, par3}) + if test then + screen.queuedMessage = test + end + + elseif event == "peripheral_detach" then + local screen = screenClass.sides[par1] + if screen then + screen:setSize() + end + --if screen then + -- screen:remove() + --end + + elseif event == "peripheral" then + local screen = screenClass.sides[par1] + if screen then + screen:setSize() + elseif peripheral.getType(par1) == "monitor" then + commandString = "SCREEN "..par1:upper().." " + end + + end + + local flag = false --Saying all screens are done, must disprove + local count = 0 --We want it to wait if no screens have channels + for a,b in pairs(screenClass.channels) do + count = count + 1 + if autoRestart or not b.isDone then + flag = true + end + end + if continue and count > 0 then --If its not already false from something else + continue = flag + end + + if #stationsList > 0 and event ~= "key" and event ~= "char" then --So screen is properly updated + for a, b in ipairs(stationsList) do + b:reset() + b:updateDisplay() + b:pushScreenUpdates() + end + end + + +end + +sleep(1.5) +for a in pairs(screenClass.channels) do + modem.close(a) +end +for a, b in pairs(screenClass.sides) do + if not b.isDone then --Otherwise we want it display the ending stats + b:setTextColor(colors.white) + b:setBackgroundColor(colors.black) + b.term.clear() + b.term.setCursorPos(1,1) + end +end + +local text --Fun :D +if computer.isComputer then text = "SUPER COMPUTER OS 9000" +elseif computer.isTurtle then text = "SUPER DIAMOND-MINING OS XXX" +elseif computer.isPocket then text = "PoCkEt OOS AMAYZE 65" +end +if text and not computer.isDone then + computer:say(text, computer.theme.title,1) +else + computer.term.setCursorPos(1,computer.dim[2]) + computer.term.clearLine() +end +--Down here shut down all the channels, remove the saved file, other cleanup stuff + diff --git a/quarry-repeater.lua b/quarry-repeater.lua new file mode 100644 index 0000000..e958b1a --- /dev/null +++ b/quarry-repeater.lua @@ -0,0 +1,219 @@ +https://pastebin.com/raw/Te359WA2 + +--Version 1.0.3 +--This program will act as a repeater between a turtle and a receiver computer +--important options are doAcceptPing and arbitraryNumber +--expected message format: {message, id, distance, fingerprint} +--added modifications similar to receiver program + +--Config +local doDebug = false --... +local arbitraryNumber = 100 --How many messages to keep between deletions +local saveFile = "QuarryRepeaterSave" +local expectedFingerprints = {quarry = true, quarryReceiver = true} +local acceptLegacy = true --This will auto-format old messages that come along +local doAcceptPing = true --Accept pings. Can be turned off for tight quarters +local pingFingerprint = "ping" +local pingSide = "top" +--Init +local sentMessages = {} --Table of received message ids +local counter = 0 +local tempCounter = 0 --How many messages since last delete +local recentID = 1 --Most recent message ID received, used for restore in delete +local channels = {} --List of channels to open listen on +local modem --The wireless modem +local modemSide --Will not necessarily be set + + +--Function Declarations-- +local function debug(...) if doDebug then return print(...) end end --Debug print + +local function newID() + return math.random(1,2000000000) --1 through 2 billion; close enough +end +local function save() + debug("Saving File") + local file = fs.open(saveFile, "w") + file.writeLine(textutils.serialize(channels):gsub("[\n\r]","")) --All the channels + file.writeLine(counter) --Total number of messages received + file.writeLine(modemSide) --The side the modem is on, helps for old MC + return file.close() +end +local function addID(id) + sentMessages[id] = true + tempCounter = tempCounter + 1 + counter = counter + 1 + recentID = id + save() +end +local function openChannels() + for a,b in pairs(channels) do + debug("Checking channel ",b) + if not modem.isOpen(b) then + debug("Opening channel ",b) + modem.open(b) + end + end +end +local function testPeripheral(periph, periphFunc) + if type(periph) ~= "table" then return false end + if type(periph[periphFunc]) ~= "function" then return false end + if periph[periphFunc]() == nil then --Expects string because the function could access nil + return false + end + return true +end +local function initModem() --Sets up modem, returns true if modem exists + if not testPeripheral(modem, "isWireless") then + if peripheral.getType(modemSide or "") == "modem" then + modem = peripheral.wrap(modemSide) + if not modem.isWireless() then --Apparently this is a thing + modem = nil + return false + end + return true + end + if peripheral.find then + modem = peripheral.find("modem", function(side, obj) return obj.isWireless() end) + end + return modem and true or false + end + return true +end +local function addChannel(num, doPrint) --Tries to add channel number. Checks if channel not already added. Speaks if doPrint set to true. + num = tonumber(num) + for a, b in pairs(channels) do + if b == num then + if doPrint then + print("Channel "..num.." already added.") + end + return false + end + end + if num >= 1 and num <= 65535 then + table.insert(channels, num) + if doPrint then + print("Channel "..num.." added.") + end + end +end + +--Actual Program Part Starts Here-- +if fs.exists(saveFile) then + local file = fs.open(saveFile,"r") + channels = textutils.unserialize(file.readLine()) or (print("Channels could not be read") and {}) + counter = tonumber(file.readLine()) or (print("Counter could not be read") and 0) + modemSide = file.readLine() or (print("Modem Side not read") and "") + print("Done reading save file") + file.close() +end + +while not initModem() do + print("No modem is connected, please attach one") + if not peripheral.find then + print("What side was that on?") + modemSide = read() + else + os.pullEvent("peripheral") + end +end +openChannels() + +sleep(2) --Give users a second to read this stuff. + +local continue = true +while continue do + print("\nHit 'q' to quit, 'r' to remove channels, 'p' to ping or any other key to add channels") + local event, key, receivedFreq, replyFreq, received, dist = os.pullEvent() + term.clear() + term.setCursorPos(1,1) + if event == "modem_message" then + print("Modem Message Received") + debug("Received on channel "..receivedFreq.."\nReply channel is "..replyFreq.."\nDistance is "..dist) + if acceptLegacy and type(received) ~= "table" then + debug("Unformatted message, formatting for quarry") + received = { message = received, id = newID(), distance = 0, fingerprint = "quarry"} + end + + debug("Message Properties") + for a, b in pairs(received) do + debug(a," ",b) + end + + if expectedFingerprints[received.fingerprint] and not sentMessages[received.id] then --A regular expected message + if received.distance then + received.distance = received.distance + dist --Add on to repeater how far message had to go + else + received.distance = dist + end + debug("Adding return channel "..replyFreq.." to channels") + addChannel(replyFreq,false) + debug("Sending Return Message") + modem.transmit(receivedFreq, replyFreq, received) --Send back exactly what we got + addID(received.id) + elseif doAcceptPing and received.fingerprint == pingFingerprint then --We got a ping! + debug("We got a ping!") + redstone.setOutput(pingSide, true) --Just a toggle should be fine + sleep(1) + redstone.setOutput(pingSide, false) + end + + if tempCounter > arbitraryNumber then --Purge messages to save memory + debug("Purging messages") + sleep(0.05) --Wait a tick for no good reason + sentMessages = {} --Completely reset table + sentMessages[recentID] = true --Reset last message (not sure if really needed. Oh well.) + tempCounter = 0 + end + + print("Messages Received: "..counter) + + elseif event == "char" then + if key == "q" then --Quitting + print("Quitting") + continue = false + elseif key == "p" then --Ping other channels + for a,b in pairs(channels) do --Ping all open channels + debug("Pinging channel ",b) + modem.transmit(b,b,{message = "I am ping! Wrar!", fingerprint = "ping"}) + sleep(1) + end + elseif key == "r" then --Removing Channels + print("Enter a comma separated list of channels to remove") + local str = "" --Concatenate all the channels into one, maybe restructure this for sorting? + for i=1,#channels do + str = str..tostring(channels[i])..", " + end + print("Channels: ",str:sub(1,-2)) --Sub for removing comma and space + local input = io.read() + local toRemove = {} --We have to use this table because user list will not be in reverse numerical order. Because modifying while iterating is bad... + for num in input:gmatch("%d+") do + for a, b in pairs(channels) do + if b == tonumber(num) then + debug("Removing ",b) + table.insert(toRemove, a, 1) --This way it will remove indexes from the back of the table + modem.close(b) + break --No use checking the rest of the table for this number + end + end + end + for i=1, #toRemove do + table.remove(channels, toRemove[i]) + end + else --Adding Channels + print("What channels would you like to open. Enter a comma-separated list.\nAdd only sending channels. Receiving ones will be added automatically.\n") + local input = io.read() + for num in input:gmatch("%d+") do + addChannel(num,true) + end + sleep(2) + end + save() + openChannels() --This will only open channels if they aren't open + + end +end + +for i=1, #channels do + modem.close(channels[i]) +end