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

[0.7.5] Events API

Started by Pinkishu, 22 June 2012 - 09:04 AM
Pinkishu #1
Posted 22 June 2012 - 11:04 AM
Hi there,

so I had that Idea after reading that help topic where someone asked about receiving a rednet message while using read()

No clue if something like this has been posted before either :P/>/> so tell me if it has been :)/>/>

Also let me know if you have suggestions or find bugs

Version: 0.7.5
Known Issues:
  • None I've encountered, be sure to report them if you find any.

Future Plans:
  • Maybe provide better events (e.g. a redstone event telling the callback function which colors changed and such)
  • Add an "all" event
  • Add a flag that allows programs to keep their events registered after they close
= Installation =


I might change how to install that at a later version. For now:

Make a file called events with the code:
http://pastebin.com/MWLPr157


Put into startup (at end):

term.clear() term.setCursorPos(1,1)
parallel.waitForAny(function() shell.run("shell") end, function() shell.run("events") end)
os.queueEvent("terminate")

= Usage =


Functions:

events.registerEvent(eventName,eventCallback)
  • eventName: string / the name of the event to catch, see wiki for a CC event list
  • eventCallback: function to call (arguments: event,p1,p2,p3,p4,p5,p6)
- Returns the registration ID
Registers the events and calls the callback function if an event is caught



events.unregisterEvent(registrationID)
  • registrationID: the ID returned by events.registerEvent
Unregisters the event so the callback function is no longer called

= Example =


Example of how to use it:


function redstoneCallback()
  local cX,cY = term.getCursorPos()
  term.setCursorPos(1,cY+1)
  term.clearLine()
  term.write("Redstone at " .. os.clock())
  term.setCursorPos(cX,cY)
end

local regID = events.registerEvent("redstone",redstoneCallback) -- Registers the event
read() -- You can type now and it will print the redstone thingy if it receives a signal change
events.unregisterEvent(regID) -- Unregisters the event
read() -- Now if it receives a redstone signal it will not trigger redstoneCallback() anymore



= ChangeLog =



v0.7.5
- changed from parallel API to own co-routine handling to avoid locking up and missing events

v0.7
- changed arguments of events.unregisterEvent to require regID
- some internal changes
- events of progams that end without unregistering them should now be unregistered
- changeg the code for startup a little

v0.5
- Release
kazagistar #2
Posted 24 June 2012 - 02:31 PM
I can't believe you posted something this awesome and it got no attention. I was just making one of these myself, but yours is very well done, good job.
Pinkishu #3
Posted 24 June 2012 - 04:35 PM
updated to 0.7 :P/>/>
kazagistar #4
Posted 25 June 2012 - 12:54 AM
There are some neat features in there such as the automated unlink when a process ends. Giving people an ID when you request a callback is brilliant: it is simple and matches the structure set up by the built in timer infrastructure (I used a given text based string, which puts the burden of handling multiple callbacks on the user, which is terrible on my part.) Your code is also very safe… I have a tenancy to just be like "if you use it wrong it will break everywhere, so just be smart about it", which is probably bad for sharing. Finally, linking in with the os.run, and holding copies of the pull events functions in local space is wise… if everything else gets burned down and overwritten, it will still continue to function, and you get a small speed boost out of it as well. If you are interested, here is my version of this that I was developing in parallel, but it is kinda… unpolished, and I really want to change it up. It comes in two parts, the event system, and a basic emulator thing that runs on top of it, and lets you run a process (basically, a custom reimplementation of parallel to some degree).

Eos v0.3 Reactor
Spoiler

--[[
kazagistar's event reactor
Do whatever you want with it, no rights reserved, as per WTFPL
(http://sam.zoy.org/wtfpl/COPYING)
The point of this program is to provide a centralized observer pattern. Events
can be generated by hardware and software, and any "observer" function which
is registered will be called.
reactor:register(event, name, func): Registers a function as an event listener,
given the event string and a unique name string to ID this function. For the
parameters passed to the function, see the documentation of the event sender,
but they are always passed the event and name. If you want to listen for any
event, listen for "reactor.any", but dont overuse this, as it can lead to poor
performance.
reactor:unregister(event, name): Unregisters a function from an event, given
the event and name.
reactor:event(event, ...): Pushes an event to resolve.
reactor:run() Starts the reactor running. Set reactor.running = false to stop
--]]
if not reactor then
reactor = {}
reactor.observer = {}
reactor.register = function(r, event, name, func)
if not event then
  event = "reactor.any"
end
	if not r.observer[event] then
		r.observer[event] = {}
	end
	r.observer[event][name]=func
end
reactor.unregister = function(r, event, name)
if event == nil then
  event = "reactor.any"
end
r:event("reactor.delete", event, name)
end
reactor.event = function(r, ...)
os.queueEvent(...)
end
reactor.resolve = function(r, event, ...)
local args = {...}
if event == "reactor.delete" then
  local event, name = unpack(args)
  r.observer[event][name] = nil
  if not next(r.observer[event]) then
   r.observer[event] = nil
  end
end
if r.observer[event] then
  for name, func in pairs(r.observer[event]) do
   func(event, name, unpack(args))
  end
end
if r.observer["reactor.any"] then
  for name, func in pairs(r.observer["reactor.any"]) do
   func(event, name, unpack(args))
  end
end
end

-- This utility function is stupidly useful, so I put it here
reactor.capture = function(str, ...) return str, arg end
-- Until the reactor.running = false, it will pull events and share em around
reactor.run = function(r)
r.running = true
while r.running do
  r:resolve(coroutine.yield())
end
end
end

Eos v0.3 Emulator
Spoiler

--[[
kazagistars emulator
Do whatever you want with it, no rights reserved, as per WTFPL
(http://sam.zoy.org/wtfpl/COPYING)
Sometimes you just want to run something as if you didn't have a reactor
churning away below doing cool things. So, you can use the emulator!
Just call emulator:run(program) with a file name or function to run, and it
will be run in parallel with everything else running in the reactor,
including other emulated programs.
--]]
if not emulator then
if not reactor then error 'emulator: missing dependancy "reactor"' end
emulator = {}
emulator.threads = {} -- List of threads
emulator.waiting = {} -- List of events they are waiting for
emulator.nextid = 1
emulator.count = 0
emulator.run = function(e, program)
local func
if type(program) == "string" then
  func = function() shell.run(program) end
elseif type(program) == "function" then
  func = program
else
  error("Emulator param must be a program")
end
local new = e.nextid
e.nextid = new + 1
e.count = e.count + 1
e.threads[new] = coroutine.create(func)
e.waiting[new] = "emulator.init"
reactor:register(nil, "emulator", emulator)
os.queueEvent("emulator.init")
end
emulator.mt = {__call = function(e, event, name, ...)
local dead = {}
	for id, thread in pairs(e.threads) do
  if (not e.waiting[id]) or (e.waiting[id] == event) then
   success, filter = coroutine.resume(thread, event, ...)
   if not success then
	print("Emulated thread error: "..filter)
   end
   if coroutine.status(thread) == "dead" then
	dead[#dead+1] = id
   end
   e.waiting[id] = filter
  end
end
for _,id in ipairs(dead) do
  e.threads[id] = nil
  e.waiting[id] = nil
  e.count = e.count - 1
end
if e.count == 0 then reactor:unregister(nil, "emulator") end
end}
setmetatable(emulator, emulator.mt)
end
If you want, I can take these out of your thread, btw, but they are much less friendly to use atm, so I don't think it should be much competition. :P/>/>

EDIT: Also, I have a question: why did you decide to delete event handlers on coroutine exit? I could see a legitimate use case for a library registering event handlers and exiting, and the event handlers taking care of the rest. I am just curious if I misunderstood your code. Also, I am not sure what the purpose of hold-deletion is. Your deletion system is confusing… :)/>/>
Pinkishu #5
Posted 25 June 2012 - 02:23 AM
There are some neat features in there such as the automated unlink when a process ends. Giving people an ID when you request a callback is brilliant: it is simple and matches the structure set up by the built in timer infrastructure (I used a given text based string, which puts the burden of handling multiple callbacks on the user, which is terrible on my part.) Your code is also very safe… I have a tenancy to just be like "if you use it wrong it will break everywhere, so just be smart about it", which is probably bad for sharing. Finally, linking in with the os.run, and holding copies of the pull events functions in local space is wise… if everything else gets burned down and overwritten, it will still continue to function, and you get a small speed boost out of it as well. If you are interested, here is my version of this that I was developing in parallel, but it is kinda… unpolished, and I really want to change it up. It comes in two parts, the event system, and a basic emulator thing that runs on top of it, and lets you run a process (basically, a custom reimplementation of parallel to some degree).

Eos v0.3 Reactor
Spoiler

--[[
kazagistar's event reactor
Do whatever you want with it, no rights reserved, as per WTFPL
(http://sam.zoy.org/wtfpl/COPYING)
The point of this program is to provide a centralized observer pattern. Events
can be generated by hardware and software, and any "observer" function which
is registered will be called.
reactor:register(event, name, func): Registers a function as an event listener,
given the event string and a unique name string to ID this function. For the
parameters passed to the function, see the documentation of the event sender,
but they are always passed the event and name. If you want to listen for any
event, listen for "reactor.any", but dont overuse this, as it can lead to poor
performance.
reactor:unregister(event, name): Unregisters a function from an event, given
the event and name.
reactor:event(event, ...): Pushes an event to resolve.
reactor:run() Starts the reactor running. Set reactor.running = false to stop
--]]
if not reactor then
reactor = {}
reactor.observer = {}
reactor.register = function(r, event, name, func)
if not event then
  event = "reactor.any"
end
	if not r.observer[event] then
		r.observer[event] = {}
	end
	r.observer[event][name]=func
end
reactor.unregister = function(r, event, name)
if event == nil then
  event = "reactor.any"
end
r:event("reactor.delete", event, name)
end
reactor.event = function(r, ...)
os.queueEvent(...)
end
reactor.resolve = function(r, event, ...)
local args = {...}
if event == "reactor.delete" then
  local event, name = unpack(args)
  r.observer[event][name] = nil
  if not next(r.observer[event]) then
   r.observer[event] = nil
  end
end
if r.observer[event] then
  for name, func in pairs(r.observer[event]) do
   func(event, name, unpack(args))
  end
end
if r.observer["reactor.any"] then
  for name, func in pairs(r.observer["reactor.any"]) do
   func(event, name, unpack(args))
  end
end
end

-- This utility function is stupidly useful, so I put it here
reactor.capture = function(str, ...) return str, arg end
-- Until the reactor.running = false, it will pull events and share em around
reactor.run = function(r)
r.running = true
while r.running do
  r:resolve(coroutine.yield())
end
end
end

Eos v0.3 Emulator
Spoiler

--[[
kazagistars emulator
Do whatever you want with it, no rights reserved, as per WTFPL
(http://sam.zoy.org/wtfpl/COPYING)
Sometimes you just want to run something as if you didn't have a reactor
churning away below doing cool things. So, you can use the emulator!
Just call emulator:run(program) with a file name or function to run, and it
will be run in parallel with everything else running in the reactor,
including other emulated programs.
--]]
if not emulator then
if not reactor then error 'emulator: missing dependancy "reactor"' end
emulator = {}
emulator.threads = {} -- List of threads
emulator.waiting = {} -- List of events they are waiting for
emulator.nextid = 1
emulator.count = 0
emulator.run = function(e, program)
local func
if type(program) == "string" then
  func = function() shell.run(program) end
elseif type(program) == "function" then
  func = program
else
  error("Emulator param must be a program")
end
local new = e.nextid
e.nextid = new + 1
e.count = e.count + 1
e.threads[new] = coroutine.create(func)
e.waiting[new] = "emulator.init"
reactor:register(nil, "emulator", emulator)
os.queueEvent("emulator.init")
end
emulator.mt = {__call = function(e, event, name, ...)
local dead = {}
	for id, thread in pairs(e.threads) do
  if (not e.waiting[id]) or (e.waiting[id] == event) then
   success, filter = coroutine.resume(thread, event, ...)
   if not success then
	print("Emulated thread error: "..filter)
   end
   if coroutine.status(thread) == "dead" then
	dead[#dead+1] = id
   end
   e.waiting[id] = filter
  end
end
for _,id in ipairs(dead) do
  e.threads[id] = nil
  e.waiting[id] = nil
  e.count = e.count - 1
end
if e.count == 0 then reactor:unregister(nil, "emulator") end
end}
setmetatable(emulator, emulator.mt)
end
If you want, I can take these out of your thread, btw, but they are much less friendly to use atm, so I don't think it should be much competition. :P/>/>

EDIT: Also, I have a question: why did you decide to delete event handlers on coroutine exit? I could see a legitimate use case for a library registering event handlers and exiting, and the event handlers taking care of the rest. I am just curious if I misunderstood your code. Also, I am not sure what the purpose of hold-deletion is. Your deletion system is confusing… :)/>/>

haha will have to look through that tomorrow

hm well I unregister events of a program once it ends - just so if some program registers events and crashes or such it won't keep calling its events although the program isn't anymore around but the events weren't unregistered, maybe i can add a persistance flag or something so they don't get deleted

holdDeletion is just a little optimization cause it would run executeDeletion() everytime it unregisters an event so it would be like
if a program had 10 events registered and exited without unregistering them, it would unregister them one by one
running executeDeletion() all the time, so if it sets holdDeletion the unregister function won't call executeDeletion()
but then once all of them are unregistered the function that unregisters them calls it so all 10 are deleted in 1 go xD
If that makes sense- i suck at explaining B)/>/>

I might still change how that works anyway B)/>/>

And I use a weird deletion cause i was told its bad to delete while looping through a table <.< So i add them to a queue and delete them then
Pinkishu #6
Posted 04 July 2012 - 03:28 PM
And v0.7.5 is out :3
toxicwolf #7
Posted 12 July 2012 - 09:32 PM
Dunno if you noticed, or if it's an error on my part, but with this, startup files from the root of a computer or on a disk aren't run at all. If the "add this to the startup file code" is added to a new startup file in the root of a computer instead, disk startups work. Not sure what's up with that.
Pinkishu #8
Posted 13 July 2012 - 01:38 PM
Hm not sure what you mean? Will test around with disks later
Usually you'd just stick it at the end of your existing startup file (be that on the disk or the PC)
toxicwolf #9
Posted 13 July 2012 - 03:50 PM
Hm not sure what you mean? Will test around with disks later
Usually you'd just stick it at the end of your existing startup file (be that on the disk or the PC)
Might be the problem then. I added the code to the startup file in the rom directory, and that's what prevented the startup files on a computer or disk to run.
The problem was actually discovered by minizbot2012 over at the OCL thread, but I checked it out and then posted here.
Pinkishu #10
Posted 13 July 2012 - 04:22 PM
Oh ok, i'll have a look at putting it in the rom startup then~
MysticT #11
Posted 13 July 2012 - 05:08 PM
If you put it in the rom startup it won't load any other startup, that's because the new shell (the one that is called from the startup on the parallel call) won't load startup files cause it has a parent shell. I'm not sure about this, but if you call shell.exit() (or use the exit program), it might run the startup file from computer or disk, since it will exit the current shell and return to the old one, wich is still loading the startup files, but this api won't work, since it's not running in the background anymore.
So, it's better to just put this in the computer or disk startup.
Pinkishu #12
Posted 13 July 2012 - 05:09 PM
Well yeah i'll look into how to solve this :)/>/> might change the way it starts up anyway