Advertisement
1lann

go chat

Jul 25th, 2015
405
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 16.86 KB | None | 0 0
  1. -- START OF GOROUTINES
  2.  
  3. -- Goroutines for ComputerCraft!
  4. -- Made by 1lann (Jason Chu)
  5. -- Last updated:  26th July 2015
  6.  
  7. --[[
  8. Licensed under the MIT License:
  9. The MIT License (MIT)
  10.  
  11. Copyright (c) 2015 1lann (Jason Chu)
  12.  
  13. Permission is hereby granted, free of charge, to any person obtaining a copy
  14. of this software and associated documentation files (the "Software"), to deal
  15. in the Software without restriction, including without limitation the rights
  16. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  17. copies of the Software, and to permit persons to whom the Software is
  18. furnished to do so, subject to the following conditions:
  19.  
  20. The above copyright notice and this permission notice shall be included in
  21. all copies or substantial portions of the Software.
  22.  
  23. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  24. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  25. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  26. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  27. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  28. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  29. THE SOFTWARE.
  30. ]]--
  31.  
  32.  
  33. -- Goroutine manager variables
  34.  
  35. local activeGoroutines = {}
  36. local channels = {}
  37. local termCompatibilityMode = false
  38. local quitDispatcherEvent = "quit_goroutine_dispatcher"
  39. local channelEventHeader = "goroutine_channel_event_"
  40. local waitGroupEvent = "goroutine_wait_group_event"
  41. local goEnv = {}
  42. local nativeTerm = term.current()
  43. local dispatcherRunning = false
  44. local dispatcherQuitFunc = function() end
  45.  
  46. local currentGoroutine = 0
  47.  
  48. -- Goroutine utility functions
  49.  
  50. local function keyOfGoroutineId(id)
  51.     for k, v in pairs(activeGoroutines) do
  52.         if v.id == id then
  53.             return k
  54.         end
  55.     end
  56.  
  57.     return nil
  58. end
  59.  
  60. local function currentKey()
  61.     local key = keyOfGoroutineId(currentGoroutine)
  62.     if key == nil then
  63.         return goEnv.error("Cannot store term context outside of goroutine.", 1)
  64.     end
  65.  
  66.     return key
  67. end
  68.  
  69. -- Stacktracer
  70. -- A very small portion was taken from CoolisTheName007
  71. -- Origin: http://pastebin.com/YWwLUUpk
  72.  
  73. local function stacktrace(depth, isInvoke)
  74.     local trace = {}
  75.     local i = depth + 2
  76.     local first = true
  77.  
  78.     while true do
  79.         i = i + 1
  80.         _, err = pcall(error, "" , i)
  81.         if err:match("^[^:]+") == "bios" or
  82.             #err == 0 then
  83.             break
  84.         end
  85.  
  86.         if first then
  87.             first = false
  88.             if isInvoke then
  89.                 table.insert(trace, "created by " .. err:sub(1, -3))
  90.             else
  91.                 table.insert(trace, "at " .. err:sub(1, -3))
  92.             end
  93.         else
  94.             table.insert(trace, "from " .. err:sub(1, -3))
  95.         end
  96.     end
  97.  
  98.     if currentGoroutine == -1 then
  99.         table.insert(trace, "created by ? (trace unavailable)")
  100.     else
  101.         local goTrace
  102.         for _, v in pairs(activeGoroutines) do
  103.             if v.id == currentGoroutine then
  104.                 goTrace = v.stacktrace
  105.             end
  106.         end
  107.  
  108.         if not goTrace then
  109.             table.insert(trace, "created by ? (trace unavailable)")
  110.         else
  111.             for _, v in pairs(goTrace) do
  112.                 table.insert(trace, v)
  113.             end
  114.         end
  115.     end
  116.  
  117.     return trace
  118. end
  119.  
  120. function goEnv.error(err, depth)
  121.     if not depth then
  122.         depth = 1
  123.     end
  124.  
  125.     if currentGoroutine == -1 then
  126.         return error(err, depth)
  127.     end
  128.  
  129.     local trace = stacktrace(depth)
  130.     table.insert(activeGoroutines[currentKey()].errors, {
  131.         err = err,
  132.         trace = trace,
  133.     })
  134.  
  135.     if #(activeGoroutines[currentKey()].errors) > 10 then
  136.         table.remove(activeGoroutines[currentKey()].errors, 1)
  137.     end
  138.  
  139.     return error(err, depth)
  140. end
  141.  
  142. local function traceError(err)
  143.     local location, msg = err:match("([^:]+:%d+): (.+)")
  144.  
  145.     local recentErrors = activeGoroutines[currentKey()].errors
  146.  
  147.     local tracedTrace = nil
  148.     for _, v in pairs(recentErrors) do
  149.         if v.err == msg then
  150.             tracedTrace = v.trace
  151.             break
  152.         end
  153.     end
  154.  
  155.     local _, y = nativeTerm.getCursorPos()
  156.     nativeTerm.setCursorPos(1, y)
  157.     nativeTerm.setTextColor(colors.red)
  158.  
  159.     if tracedTrace then
  160.         print("goroutine runtime error:")
  161.         print(msg)
  162.         for _, v in pairs(tracedTrace) do
  163.             print("  " .. v)
  164.         end
  165.  
  166.         return
  167.     end
  168.  
  169.     if not msg then
  170.         msg = err
  171.     end
  172.  
  173.     print("goroutine runtime error:")
  174.     print(msg)
  175.  
  176.     if location then
  177.         print("  at " .. location)
  178.     else
  179.         print("  at ? (location unavailable)")
  180.     end
  181.  
  182.     print("  from ? (trace unavailable)")
  183.  
  184.     if currentGoroutine == -1 then
  185.         print("  created by ? (trace unavailable)")
  186.         print("  from goroutine start")
  187.     else
  188.         local goTrace
  189.         for _, v in pairs(activeGoroutines) do
  190.             if v.id == currentGoroutine then
  191.                 goTrace = v.stacktrace
  192.             end
  193.         end
  194.  
  195.         if not goTrace then
  196.             print("  created by ? (trace unavailable)")
  197.             print("  from goroutine start")
  198.         else
  199.             for _, v in pairs(goTrace) do
  200.                 print("  " .. v)
  201.             end
  202.         end
  203.     end
  204.  
  205. end
  206.  
  207. -- Term wrapper to allow for saving terminal states
  208.  
  209. local emulatedTerm = {}
  210.  
  211. for k, v in pairs(nativeTerm) do
  212.     emulatedTerm[k] = v
  213. end
  214.  
  215. emulatedTerm.setCursorBlink = function(blink)
  216.     activeGoroutines[currentKey()].termState.blink = blink
  217.     return nativeTerm.setCursorBlink(blink)
  218. end
  219.  
  220. emulatedTerm.setBackgroundColor = function(color)
  221.     if type(color) ~= "number" then
  222.         return goEnv.error("Argument to term.setBackgroundColor must be a number")
  223.     end
  224.  
  225.     activeGoroutines[currentKey()].termState.bg_color = color
  226.     return nativeTerm.setBackgroundColor(color)
  227. end
  228.  
  229. emulatedTerm.write = function(text)
  230.     if type(text) ~= "string" then
  231.         return goEnv.error("Argument to term.write must be a string")
  232.     end
  233.     goEnv.emitChannel("term_events", "write")
  234.     return nativeTerm.write(text)
  235. end
  236.  
  237. emulatedTerm.setTextColor = function(color)
  238.     if type(color) ~= "number" then
  239.         return goEnv.error("Argument to term.setTextColor must be a number")
  240.     end
  241.  
  242.     activeGoroutines[currentKey()].termState.txt_color = color
  243.     return nativeTerm.setTextColor(color)
  244. end
  245.  
  246. emulatedTerm.scroll = function(...)
  247.     goEnv.emitChannel("term_events", "scroll")
  248.     return nativeTerm.scroll(...)
  249. end
  250.  
  251. emulatedTerm.clearLine = function(...)
  252.     goEnv.emitChannel("term_events", "clearLine")
  253.     return nativeTerm.clearLine(...)
  254. end
  255.  
  256. emulatedTerm.clear = function(...)
  257.     goEnv.emitChannel("term_events", "clear")
  258.     return nativeTerm.clear(...)
  259. end
  260.  
  261. local function restoreTermState(termState)
  262.     nativeTerm.setTextColor(termState.txt_color)
  263.     nativeTerm.setBackgroundColor(termState.bg_color)
  264.     nativeTerm.setCursorBlink(termState.blink)
  265.     nativeTerm.setCursorPos(unpack(termState.cursor_pos))
  266. end
  267.  
  268. function goEnv.goroutineId()
  269.     return currentGoroutine
  270. end
  271.  
  272. -- Goroutines and channel functions
  273.  
  274. function goEnv.go(...)
  275.     local args = {...}
  276.  
  277.     if type(args[1]) ~= "function" then
  278.         return goEnv.error("First argument to go must be a function.")
  279.     end
  280.  
  281.     local funcArgs = {}
  282.  
  283.     if #args > 1 then
  284.         for i = 2, #args do
  285.             table.append(funcArgs, args[i])
  286.         end
  287.     end
  288.  
  289.     local idsInUse = {}
  290.     for k,v in pairs(activeGoroutines) do
  291.         idsInUse[tostring(v.id)] = true
  292.     end
  293.  
  294.     local newId = -1
  295.     for i = 1, 1000000 do
  296.         if not idsInUse[tostring(i)] then
  297.             newId = i
  298.             break
  299.         end
  300.     end
  301.  
  302.     if newId < 0 then
  303.         return error("Reached goroutine limit, cannot spawn new goroutine")
  304.     end
  305.  
  306.     local parent = activeGoroutines[currentKey()]
  307.     local copyTermState = {}
  308.  
  309.     for k, v in pairs(parent.termState) do
  310.         copyTermState[k] = v
  311.     end
  312.  
  313.     table.insert(activeGoroutines, {
  314.         id = newId,
  315.         func = coroutine.create(args[1]),
  316.         arguments = funcArgs,
  317.         stacktrace = stacktrace(1, true),
  318.         termState = copyTermState,
  319.         errors = {},
  320.         filter = nil,
  321.         firstRun = true,
  322.     })
  323. end
  324.  
  325. function goEnv.emitChannel(channel, data, wait)
  326.     if type(channel) ~= "string" then
  327.         return goEnv.error("First argument to emitChannel must be a string.")
  328.     end
  329.  
  330.     if data == nil then
  331.         return goEnv.error("Second argument (data) to emitChannel, cannot be nil.")
  332.     end
  333.  
  334.     if channel == "term_events" and termCompatibilityMode and data then
  335.         return
  336.     end
  337.  
  338.     channels[channel] = {data, goEnv.goroutineId()}
  339.  
  340.     os.queueEvent(channelEventHeader .. channel)
  341.  
  342.     if wait then
  343.         while true do
  344.             if channels[channel] == nil then
  345.                 return
  346.             end
  347.  
  348.             coroutine.yield(channelEventHeader .. channel)
  349.         end
  350.     end
  351. end
  352.  
  353. function goEnv.waitChannel(channel, allowPrev, timeout)
  354.     if type(channel) ~= "string" then
  355.         return goEnv.error("First argument to waitChannel must be a string.")
  356.     end
  357.  
  358.     if timeout and type(timeout) ~= "number" then
  359.         return goEnv.error("Third argument to waitChannel must be a number or nil.")
  360.     end
  361.  
  362.     local stillAlive = true
  363.  
  364.     if timeout then
  365.         goEnv.go(function()
  366.             sleep(timeout)
  367.             if stillAlive then
  368.                 goEnv.emitChannel(channel, false)
  369.             end
  370.         end)
  371.     end
  372.  
  373.     if not allowPrev then
  374.         channels[channel] = nil
  375.     end
  376.  
  377.     os.queueEvent(channelEventHeader .. channel)
  378.  
  379.     while true do
  380.         if channels[channel] ~= nil then
  381.             stillAlive = false
  382.             local value = channels[channel]
  383.             channels[channel] = nil
  384.             return unpack(value)
  385.         end
  386.  
  387.         coroutine.yield(channelEventHeader .. channel)
  388.     end
  389. end
  390.  
  391. -- Wait groups
  392.  
  393. goEnv.WaitGroup = {}
  394. goEnv.WaitGroup.__index = goEnv.WaitGroup
  395.  
  396. function goEnv.WaitGroup.new()
  397.     local self = setmetatable({}, goEnv.WaitGroup)
  398.     self:setZero()
  399.     return self
  400. end
  401.  
  402. function goEnv.WaitGroup:setZero()
  403.     self.incrementer = 0
  404.     os.queueEvent(waitGroupEvent)
  405. end
  406.  
  407. function goEnv.WaitGroup:done()
  408.     if self.incrementer > 0 then
  409.         self.incrementer = self.incrementer - 1
  410.         os.queueEvent(waitGroupEvent)
  411.     end
  412. end
  413.  
  414. function goEnv.WaitGroup:wait()
  415.     while true do
  416.         if self.incrementer == 0 then
  417.             return
  418.         end
  419.  
  420.         coroutine.yield(waitGroupEvent)
  421.     end
  422. end
  423.  
  424. function goEnv.WaitGroup:add(amount)
  425.     self.incrementer = self.incrementer + amount
  426.     if self.incrementer < 0 then
  427.         self.incrementer = 0
  428.     end
  429.     os.queueEvent(waitGroupEvent)
  430. end
  431.  
  432. function goEnv.WaitGroup:value()
  433.     return self.incrementer
  434. end
  435.  
  436. -- Runner
  437.  
  438. local function cleanUp()
  439.     channels = {}
  440.     activeGoroutines = {}
  441.     dispatcherRunning = false
  442.     termCompatibilityMode = false
  443.     term.redirect(nativeTerm)
  444.  
  445.     local ret, err = pcall(dispatcherQuitFunc)
  446.     if not ret then
  447.         local _, y = term.getCursorPos()
  448.         term.setCursorPos(1, y)
  449.         term.setTextColor(colors.red)
  450.  
  451.         print("user dispatcher quit error:")
  452.         print(err)
  453.  
  454.         term.setTextColor(colors.white)
  455.     end
  456. end
  457.  
  458. function runDispatcher(programFunction)
  459.     if dispatcherRunning then
  460.         error("Dispatcher already running.")
  461.     end
  462.     dispatcherRunning = true
  463.  
  464.     term.redirect(emulatedTerm)
  465.  
  466.     local env = {}
  467.     local global = getfenv(0)
  468.  
  469.     for k, v in pairs(global) do
  470.         env[k] = v
  471.     end
  472.  
  473.     for k, v in pairs(goEnv) do
  474.         env[k] = v
  475.     end
  476.  
  477.     env["shell"] = shell
  478.  
  479.     local main = setfenv(programFunction, env)
  480.  
  481.     table.insert(activeGoroutines, {
  482.         func = coroutine.create(main),
  483.         arguments = {},
  484.         id = 0,
  485.         termState = {
  486.             txt_color = colors.white,
  487.             bg_color = colors.black,
  488.             blink = false,
  489.             cursor_pos = {nativeTerm.getCursorPos()},
  490.         },
  491.         errors = {},
  492.         stacktrace = {"from dispatcher start"},
  493.         filter = nil,
  494.         firstRun = true,
  495.     })
  496.  
  497.     local events = {}
  498.  
  499.     while true do
  500.         for k, v in pairs(activeGoroutines) do
  501.             if coroutine.status(v.func) ~= "dead" then
  502.                 if (v.filter and events and #events > 0 and
  503.                     events[1] == v.filter) or not v.filter then
  504.                     activeGoroutines[k].filter = nil
  505.                     local resp, err
  506.  
  507.                     currentGoroutine = v.id
  508.  
  509.                     restoreTermState(v.termState)
  510.  
  511.                     if v.firstRun then
  512.                         resp, err = coroutine.resume(v.func, unpack(v.arguments))
  513.                         activeGoroutines[k].firstRun = false
  514.                     else
  515.                         resp, err = coroutine.resume(v.func, unpack(events))
  516.                     end
  517.  
  518.                     activeGoroutines[k].termState.cursor_pos =
  519.                         {nativeTerm.getCursorPos()}
  520.  
  521.                     if resp then
  522.                         if err == quitDispatcherEvent then
  523.                             cleanUp()
  524.                             return
  525.                         end
  526.  
  527.                         if type(err) == "string" then
  528.                             activeGoroutines[k].filter = err
  529.                         end
  530.                     else
  531.                         traceError(err)
  532.                         return
  533.                     end
  534.  
  535.                     currentGoroutine = -1
  536.                 end
  537.             end
  538.         end
  539.  
  540.         local sweeper = {}
  541.  
  542.         for k,v in pairs(activeGoroutines) do
  543.             if coroutine.status(v.func) ~= "dead" then
  544.                 table.insert(sweeper, v)
  545.             end
  546.         end
  547.  
  548.         activeGoroutines = {}
  549.  
  550.         for k,v in pairs(sweeper) do
  551.             if v.termState.blink then
  552.                 nativeTerm.setCursorBlink(true)
  553.                 nativeTerm.setTextColor(v.termState.txt_color)
  554.                 nativeTerm.setCursorPos(unpack(v.termState.cursor_pos))
  555.             end
  556.             table.insert(activeGoroutines, v)
  557.         end
  558.  
  559.         if #activeGoroutines == 0 then
  560.             cleanUp()
  561.             return
  562.         end
  563.  
  564.         events = {os.pullEventRaw()}
  565.  
  566.         if events and #events > 0 and events[1] == quitDispatcherEvent then
  567.             cleanUp()
  568.             return
  569.         elseif events and #events > 0 and events[1] == "terminate" then
  570.             printError("Terminated")
  571.             cleanUp()
  572.             return
  573.         end
  574.     end
  575. end
  576.  
  577. function goEnv.quitDispatcher()
  578.     coroutine.yield(quitDispatcherEvent)
  579. end
  580.  
  581. function termCompatibility()
  582.     term.redirect(nativeTerm)
  583.     termCompatibilityMode = true
  584. end
  585.  
  586. -- You should not manipulate the terminal with the
  587. -- dispatch quit function, as it will be also be
  588. -- called on dirty quits, such as errors.
  589. function onDispatcherQuit(func)
  590.     dispatcherQuitFunc = func
  591. end
  592.  
  593. function quitDispatcher()
  594.     os.queueEvent(quitDispatcherEvent)
  595. end
  596.  
  597. goEnv.onDispatcherQuit = onDispatcherQuit
  598. goEnv.termCompatibility = termCompatibility
  599.  
  600. -- local global = getfenv(1)
  601. -- global._G.quitDispatcher = quitDispatcher
  602. -- global._G.runDispatcher = runDispatcher
  603. -- global._G.termCompatibility = termCompatibility
  604. -- setfenv(1, global)
  605.  
  606.  
  607. -- START OF ACTUAL PROGRAM
  608.  
  609. local function main()
  610.     local protocol = "go-chat"
  611.     local w, h = term.getSize()
  612.     local messages = {}
  613.     local username
  614.  
  615.     local function receiver()
  616.         while true do
  617.             _, message = rednet.receive(protocol)
  618.             emitChannel("message", message)
  619.         end
  620.     end
  621.  
  622.     local function messageRenderer()
  623.         term.setTextColor(colors.lightBlue)
  624.         while true do
  625.             local message = waitChannel("message", true)
  626.  
  627.             if type(message) == "table" then
  628.                 local newMessages = {}
  629.                 local newMessageGroup = {}
  630.  
  631.                 local buffer = ""
  632.                 for word in message.message:gmatch("%S+") do
  633.                     if #(buffer .. word .. " ") > (w + 1) then
  634.                         table.insert(newMessageGroup, {
  635.                             message = buffer,
  636.                             color = message.color,
  637.                         })
  638.                         buffer = ""
  639.                     end
  640.                     buffer = buffer .. word .. " "
  641.                 end
  642.  
  643.                 table.insert(newMessageGroup, {
  644.                     message = buffer,
  645.                     color = message.color,
  646.                 })
  647.  
  648.                 for i = #newMessageGroup, 1, -1 do
  649.                     table.insert(newMessages, newMessageGroup[i])
  650.                 end
  651.  
  652.                 for i = 1, math.min(#messages, h) do
  653.                     table.insert(newMessages, messages[i])
  654.                 end
  655.  
  656.                 messages = newMessages
  657.             end
  658.  
  659.             for i = 1, h-2 do
  660.                 term.setCursorPos(1, h - i)
  661.                 term.clearLine()
  662.  
  663.                 if messages[i] then
  664.                     term.setTextColor(messages[i].color)
  665.                     term.write(messages[i].message)
  666.                 end
  667.             end
  668.         end
  669.     end
  670.  
  671.     local function sendingRenderer()
  672.         while true do
  673.             term.setTextColor(colors.green)
  674.             term.setCursorPos(1, h)
  675.             term.write("> ")
  676.             term.setTextColor(colors.gray)
  677.  
  678.             emitChannel("reading", true)
  679.  
  680.             term.setCursorBlink(true)
  681.             local message = read()
  682.  
  683.             if message == "exit" or message == "/exit" then
  684.                 for _, side in pairs(rs.getSides()) do
  685.                     if peripheral.getType(side) == "modem" then
  686.                         rednet.close(side)
  687.                     end
  688.                 end
  689.  
  690.                 term.setBackgroundColor(colors.black)
  691.                 term.setTextColor(colors.yellow)
  692.                 term.clear()
  693.                 term.setCursorPos(1, 1)
  694.                 print("Thanks for trying out Go Chat!")
  695.                 quitDispatcher()
  696.             end
  697.  
  698.             if message == "error" then
  699.                 error("Intentional error")
  700.             end
  701.  
  702.             if #(message:match("^%s*(.-)%s*$")) > 0 then
  703.                 local sendMessage = {
  704.                     message = "<" .. username .. "> " .. message,
  705.                     color = colors.blue,
  706.                 }
  707.                 emitChannel("message", sendMessage)
  708.                 rednet.broadcast(sendMessage, protocol)
  709.             else
  710.                 emitChannel("message", true)
  711.             end
  712.         end
  713.     end
  714.  
  715.     local function displayBar()
  716.         term.setBackgroundColor(colors.lightGray)
  717.         while true do
  718.             term.setCursorPos(1, 1)
  719.             term.clearLine()
  720.             term.setTextColor(colors.blue)
  721.             if w < 30 then
  722.                 term.write(" Chat by 1lann")
  723.             else
  724.                 term.write(" Go Chat Demo by 1lann")
  725.             end
  726.             term.setCursorPos(w - 8, 1)
  727.             term.setTextColor(colors.white)
  728.             term.write(textutils.formatTime(os.time()))
  729.             waitChannel("reading", false, 0.2)
  730.         end
  731.     end
  732.  
  733.     local modemOpen = false
  734.  
  735.     for _, side in pairs(rs.getSides()) do
  736.         if peripheral.getType(side) == "modem" then
  737.             rednet.open(side)
  738.             modemOpen = true
  739.         end
  740.     end
  741.  
  742.     if not modemOpen then
  743.         print("Please attach a modem first")
  744.         return
  745.     end
  746.  
  747.     write("Enter a username: ")
  748.     term.setCursorBlink(true)
  749.     username = read()
  750.     term.setCursorBlink(false)
  751.  
  752.     term.setBackgroundColor(colors.white)
  753.     term.clear()
  754.  
  755.     local sendMessage = {
  756.         message = username .. " has joined the room.",
  757.         color = colors.green,
  758.     }
  759.     emitChannel("message", sendMessage)
  760.     rednet.broadcast(sendMessage, protocol)
  761.  
  762.     go(messageRenderer)
  763.     go(receiver)
  764.     go(sendingRenderer)
  765.     go(displayBar)
  766. end
  767.  
  768. runDispatcher(main)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement