This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
Kepler's profile picture

Coroutine and event manager

Started by Kepler, 04 August 2017 - 06:34 AM
Kepler #1
Posted 04 August 2017 - 08:34 AM
A combined coroutine manager and event callback handler.

An alternative to placing all logic in a single os.pullEvent while loop.
Event handlers are run in separate coroutines and are not reentrant.
Easily add new coroutines at any time (before or after event loop has started).
Advanced control of coroutines using the returned handler object.

Installation
pastebin get Yek469px event.lua

Code
Spoiler

local Event = {
  uid	   = 1,	   -- unique id for handlers
  routines  = { },	 -- coroutines
  types	 = { },	 -- event handlers
  timers    = { },	 -- named timers
  terminate = false,
}
local Routine = { }
function Routine:isDead()
  if not self.co then
    return true
  end
  return coroutine.status(self.co) == 'dead'
end
function Routine:terminate()
  if self.co then
    self:resume('terminate')
  end
end
function Routine:resume(event, ...)
  if not self.co then
    error('Cannot resume a dead routine')
  end
  if not self.filter or self.filter == event or event == "terminate" then
    local s, m = coroutine.resume(self.co, event, ...)
    if coroutine.status(self.co) == 'dead' then
	  self.co = nil
	  self.filter = nil
	  Event.routines[self.uid] = nil
    else
	  self.filter = m
    end
    if not s and event ~= 'terminate' then
	  error('\n' .. (m or 'Error processing event'))
    end
    return s, m
  end
  return true, self.filter
end
local function nextUID()
  Event.uid = Event.uid + 1
  return Event.uid - 1
end
function Event.on(event, fn)
  local handlers = Event.types[event]
  if not handlers then
    handlers = { }
    Event.types[event] = handlers
  end
  local handler = {
    uid	 = nextUID(),
    event   = event,
    fn	  = fn,
  }
  handlers[handler.uid] = handler
  setmetatable(handler, { __index = Routine })
  return handler
end
function Event.off(h)
  if h and h.event then
    Event.types[h.event][h.uid] = nil
  end
end
local function addTimer(interval, recurring, fn)
  local timerId = os.startTimer(interval)
  local handler
  handler = Event.on('timer', function(t, id)
    if timerId == id then
	  fn(t, id)
	  if recurring then
	    timerId = os.startTimer(interval)
	  else
	    Event.off(handler)
	  end
    end
  end)
  return handler
end
function Event.onInterval(interval, fn)
  return addTimer(interval, true, fn)
end
function Event.onTimeout(timeout, fn)
  return addTimer(timeout, false, fn)
end
function Event.addNamedTimer(name, interval, recurring, fn)
  Event.cancelNamedTimer(name)
  Event.timers[name] = addTimer(interval, recurring, fn)
end
function Event.cancelNamedTimer(name)
  local timer = Event.timers[name]
  if timer then
    Event.off(timer)
  end
end
function Event.waitForEvent(event, timeout)
  local timerId = os.startTimer(timeout)
  repeat
    local e = { os.pullEvent() }
    if e[1] == event then
	  return table.unpack(e)
    end
  until e[1] == 'timer' and e[2] == timerId
end
function Event.addRoutine(fn)
  local r = {
    co  = coroutine.create(fn),
    uid = nextUID()
  }
  setmetatable(r, { __index = Routine })
  Event.routines[r.uid] = r
  r:resume()
  return r
end
function Event.pullEvents(...)
  for _, fn in ipairs({ ... }) do
    Event.addRoutine(fn)
  end
  repeat
    local e = Event.pullEvent()
  until e[1] == 'terminate'
end
function Event.exitPullEvents()
  Event.terminate = true
  os.sleep(0)
end
local function processHandlers(event)
  local handlers = Event.types[event]
  if handlers then
    for _,h in pairs(handlers) do
	  if not h.co then
	    -- callbacks are single threaded (only 1 co per handler)
	    h.co = coroutine.create(h.fn)
	    Event.routines[h.uid] = h
	  end
    end
  end
end
local function tokeys(t)
  local keys = { }
  for k in pairs(t) do
    keys[#keys+1] = k
  end
  return keys
end
local function processRoutines(...)
  local keys = tokeys(Event.routines)
  for _,key in ipairs(keys) do
    local r = Event.routines[key]
    if r then
	  r:resume(...)
    end
  end
end
function Event.pullEvent(eventType)
  while true do
    local e = { os.pullEventRaw() }
    processHandlers(e[1])
    processRoutines(table.unpack(e))
    if Event.terminate or e[1] == 'terminate' then
	  Event.terminate = false
	  return { 'terminate' }
    end
    if not eventType or e[1] == eventType then
	  return e
    end
  end
end
return Event

Simple Example
Spoiler

local Event = dofile('event.lua')

Event.onInterval(1, function()
  print('interval ' .. os.clock())
end)

local cancelTimer = Event.onInterval(2, function()
  print('2 second timer')
end)

Event.onTimeout(5, function()
  print('5 second timer')
  print('canceling 2 second timer')
  Event.off(cancelTimer)
end)

Event.on('char', function(event, ch)
  print('pressed: ' .. ch)
  print('hit enter')
  read()
  print('enter hit')
  error('forced error')
end)

Event.on('mouse_click', function(event, button, x, y)
  print('button: ' .. button)
  print('exiting')
  Event.exitPullEvents()
end)

Event.addRoutine(function()
  while true do
	os.sleep(1.5)
	print('routine ' .. os.clock())
  end
end)

Event.pullEvents()

Documentation
Spoiler

Event.on(string event, function fn)

  Invokes a function when an event has been fired

  parameters
	event: type of event to pull. example: char, mouse_click
	fn:	callback function

  returns
	Handler object

  example
	Event.on('mouse_click', function(event, button, x, y)
	  print('button: ' .. button)
	end)

Event.off(Handler h)

  Removes an event handler

  parameters
	h:	 Handler object

  example
	local timer1 = Event.onInterval(2, function()
	  ...
	end)

	Event.onTimeout(5, function()
	  Event.off(timer1)
	end)

Event.onInterval(number interval, function fn)

  Repeatedly invokes a function. The interval is reset after the function
  has completed.

  parameters
	interval: any number accepted by os.sleep
	fn:	   callback function

  returns
	Handler object

  example
	Event.onInterval(1, function()
	  print('interval ' .. os.clock())
	end)

Event.onTimeout(number timeout, function fn)

  Invokes a function once after the specified time

  parameters
	interval: any number accepted by os.sleep
	fn:	   callback function

  returns
	Handler object

  example
	Event.onTimeout(5, function()
	  print('one time timer called after 5 seconds')
	end)

Event.addNamedTimer(string name, number interval, boolean recurring, function fn)

  Invokes a function after the specified time

  parameters
	name:	  unique name for this timer
	interval:  any number accepted by os.sleep
	recurring: single or repeating
	fn:		callback function

  example
	Event.addNamedTimer('example', 3, false, function()
	  ...
	end)

Event.cancelNamedTimer(string name)

  Cancels the named timer

  parameters
	name:	  unique name for this timer

  example
	Event.addNamedTimer('example', 3, false, function()
	  ...
	end)
	...
	Event.cancelNamedTimer('example')

Event.addRoutine(function fn)

  Adds a function to be run in a coroutine

  parameters
	fn:		function that will run in a coroutine

  example
	Event.addRoutine(function()
	  while true do
		os.sleep(1.5)
		print('routine ' .. os.clock())
	  end
	end)
	Event.pullEvents()

Event.pullEvents(functions ...)

  Process events. Runs coroutines and calls event handler functions.

  parameters
	...:	   an optional list of functions that will be run as coroutines

  example
	function fn1()
	  while true do os.sleep(1) print('1') end
	end
	function fn2()
	  while true do os.sleep(2) print('2') end
	end
	Event.on('char', function(event, ch)
	  print('pressed: ' .. ch)
	end)
	Event.pullEvents(fn1, fn2)

Event.exitPullEvents()

  Exits the event loop.

  example
	Event.on('mouse_click', function(event, button, x, y)
	  Event.exitPullEvents()
	end)
	Event.pullEvents()

Event.pullEvent(eventType)

  Process a single event. An alternative to Event.pullEvents.

  parameters
	eventType: an optional event type to wait for

  returns
	a table containing the same arguments as returned from os.pullEvent

  example
	while true do
	  local e = Event.pullEvent()
	  if e[1] == 'terminate' then
		break
	  end
	  ...
	end

Handler Objects
---------------

Routine:isDead()

  returns whether this coroutine is dead

  returns
	boolean

Routine:terminate()

  terminates a coroutine

Routine:resume(event, ...)

  parameters
	same arguments as passed to os.queueEvent
  returns
	the same arguments coroutine.resume
Edited on 07 August 2017 - 06:49 PM
Dave-ee Jones #2
Posted 07 August 2017 - 04:42 AM
Interesting idea, though in some of those functions (particularly your 'char' function) I don't like the way you do some things. E.g. Testing for key 'Enter' pressed by calling the read() function. That's just lazy :P/>
AddRoutine's sleep(1.5) isn't ideal either. It's quite crude.

If you want it to look and act more accurately you should use timer events and key press events. Otherwise, good job.
Kepler #3
Posted 07 August 2017 - 08:06 PM
Interesting idea, …

Hmm, maybe I did not write this post clearly. What you are looking at is an example of using the API. The example code is very simple to just show how to use the API. Take a look at the linked pastebin code. I'll update this post when I get a chance.

Thanks for the feedback.