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
Simple Example
Documentation
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