[Home] [Downloads] [Search] [Help/forum]


Register forum user name Search FAQ

Gammon Forum

[Folder]  Entire forum
-> [Folder]  MUSHclient
. -> [Folder]  Plugins
. . -> [Subject]  EventManager: dispatching custom script events from anywhere to anywhere

EventManager: dispatching custom script events from anywhere to anywhere

It is now over 60 days since the last post. This thread is closed.     [Refresh] Refresh page


Posted by Ked   Russia  (524 posts)  [Biography] bio
Date Thu 11 Mar 2004 02:47 PM (UTC)

Amended on Thu 11 Mar 2004 02:53 PM (UTC) by Ked

Message
This plugin was inspired by Shadowfyr's reply in:

http://www.gammon.com.au/forum/bbshowpost.php?bbsubject_id=3925&page=999999

I've always wanted to make some system of propagating custom events throughout the entire script network of my world: from one plugin to several others and/or main script file, main script to several plugins. World.CallPlugin only works one way, therefore it's not very useful for that purpose, besides it requires always keeping in mind what plugins need to be notified, and constantly putting in more CallPlugin calls in a multitude of places in a multitude of scripts. Ack! World.Execute, on the other hand, is suited perfectly for this. All that was missing was a simple realization that you don't need a separate alias for each call, they can all go through basically the same alias, duplicated in each "event-aware" plugin and the world file. Here's what I am talking about:


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE muclient >

<muclient>
   <plugin name="EventManager" author="Keldar" language="vbscript" purpose="Managing custom script events"
     save_state="y" date_written="2004-03-11" requires="3.32" version="1.0" id="fc8161da9a2a5d7f56a03600">
 </plugin>

<aliases>
<alias
	name="register_listener"
	match="fc8161da9a2a5d7f56a03600 reg_listener */*"
	enabled="y"
	sequence="100"
	send_to="12"
><send>call RegisterListener(&quot;%1&quot;, &quot;%2&quot;)</send></alias>
<alias
	name="register_event"
	match="fc8161da9a2a5d7f56a03600 reg_event *"
	enabled="y"
	sequence="100"
	send_to="12"
><send>call RegisterEvent(&quot;%1&quot;)</send></alias>
<alias
	name="dispatch_event"
	match="fc8161da9a2a5d7f56a03600 dispatch_event */*"
	enabled="y"
	sequence="100"
	send_to="12"
><send>call DispatchEvent(&quot;%1&quot;, &quot;%2&quot;)</send></alias>
</aliases>

<script> 
<![CDATA[
option explicit
dim evt_list()
ReDim evt_list(0, 0)

'Register a new event
RegisterEvent "OnTest"

'Register a plugin to listen for the event, supplying the plugin's ID and the event's name.
'The listening plugin must have a sub with the same name as an event it is listening to and
'this sub must accept exactly 1 argument, otherwise the event will cause an error
RegisterListener "8019349d696b61bfba64b08a", "OnTest"

sub RegisterListener (id, evt)
dim i, j, listenerExists
listenerExists = 0
	for i = 0 to ubound(evt_list, 1)
		if evt_list(i, 0) = evt then
			for j = 0 to ubound(evt_list, 2)
				if evt_list(i, j) = id then
					listenerExists = 1
					exit for
				end if
			next
			if listenerExists then
				exit for
			end if
			Redim Preserve evt_list(ubound(evt_list, 1), ubound(evt_list, 2) + 1)
			evt_list(i, ubound(evt_list, 2)) = id
			exit for
		end if
	next
end sub

sub RegisterEvent(evt_name)
dim i, eventExists, j
dim tempEvents_list(), tempListeners_list()
eventExists = 0
Redim tempEvents_list(ubound(evt_list, 1))
Redim tempListeners_list(ubound(evt_list, 2))

	for i = 0 to ubound(evt_list, 1)
		if evt_list(i, 0) = evt_name then
			eventExists = 1
			exit for
		end if
		tempEvents_list(i) = evt_list(i, 0)
		for j = 0 to ubound(evt_list, 2)
			tempListeners_list(j) = evt_list(i, j)
		next
	next
	if eventExists then
		exit sub
	end if
	
	Redim evt_list((ubound(evt_list, 1) + 1), 0)
	
	for i = 0 to ubound(tempEvents_list)
		evt_list(i, 0) = tempEvents_list(i)
		for j = 0 to ubound(tempListeners_list)
			evt_list(i, j) = tempListeners_list(j)
		next
	next

	evt_list(ubound(evt_list, 1), 0) = evt_name
end sub

sub DispatchEvent(evt, args)
dim i, j
	for i = 0 to ubound(evt_list, 1)
		if evt_list(i, 0) = evt then
			if ubound(evt_list, 2) = 0 then
				exit for
			end if
			for j = 1 to ubound(evt_list, 2)
				world.Execute evt_list(i, j) & ":" & evt_list(i, 0) & " " & args
			next
			exit for
		end if
	next
end sub

]]>        
</script>
</muclient>


From here all that is needed to dispatch a registered event from any outside script is to do:

world.Execute "fc8161da9a2a5d7f56a03600 dispatch_event OnTest//none"

All that is needed to accept this event is an alias like this:


  <alias
   name="Sur_Send"
   match="8019349d696b61bfba64b08a\:(\w+) (.*)"
   enabled="y"
   regexp="y"
   keep_evaluating="y"
   send_to="12"
  >
  <send>call %1 (%2)</send>
  <//alias>


where 8019349d696b61bfba64b08a is the listener's id: either a plugin id (if it's a plugin) or the world's id (if it's the main script). Another requirement from a listener is a sub with the same name as the event it is listening to, which sub must accept exactly 1 argument.

Note, that although EventManager provides aliases for registering events and listeners, it is better to register both (event first, listeners after that) directly in EventManager's script section in global scope, the way it is done with the "OnTest" event. This is because there's no easy way to define an exact sequence in which the plugins are loaded, therefore if a dispatching plugin is loaded before EventManager, and tries to register new events upon startup, its' World.Execute calls will go straight to the world as text commands, which is no big deal but results in missing events and errors when these events are being dispatched later.

The following post has 2 test plugins that I used for testing this system - Test1 dispatched the "OnTest" event and Test2 catches it.
[Go to top] top

Posted by Ked   Russia  (524 posts)  [Biography] bio
Date Reply #1 on Thu 11 Mar 2004 03:08 PM (UTC)
Message
Test1 plugin - the dispatcher

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE muclient >

<muclient>
   <plugin name="Test1" author="Keldar" language="vbscript" purpose="Testing EventManager"
     save_state="y" date_written="2004-03-11" requires="3.32" version="1.0" 
     id="f2093b9c1e5fd33d682221f2">
</plugin>
<aliases>
<alias
 name="test"
 match="test"
 enabled="y"
 send_to="12"
><send>call TestEvent()</send></alias>
</aliases>

<script> 
<![CDATA[
option explicit

sub TestEvent
'                     this is EventManager's id                          event   argument
world.Execute "fc8161da9a2a5d7f56a03600 dispatch_event OnTest/none"
end sub
]]>        
</script>

</muclient>


Test2 plugin - the listener

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE muclient >

<muclient>
   <plugin name="Test2" author="Keldar" language="vbscript" purpose="Testing EvtManager"
     save_state="y" date_written="2004-03-11" requires="3.32" version="1.0" 
     id="8019349d696b61bfba64b08a">
</plugin>

<aliases>
  <alias
   name="Sur_Send"
   match="8019349d696b61bfba64b08a\:(\w+) (.*)"
   enabled="y"
   regexp="y"
   keep_evaluating="y"
   send_to="12"
  >
  <send>call %1 (%2)</send>
  </alias>

</aliases>

<script> 
<![CDATA[
option explicit

sub OnTest(arg)
	world.Note "I am the Test2 plugin - OnTest event caught"
end sub
]]>        
</script>
</muclient>


After installing EventManager, Test1, and Test2, all you have to do to test it is type 'test'.

This can be used for many purposes, one recent example (from which this thing was born) is OnPluginPartialLine event: you can set up a proxy plugin which simply accepts OnPluginPartialLine and dispatches it as a custom event to any interested parties. That way you don't need to duplicate OnPluginPartialLine in every plugin that needs it, the output of that builtin event can be easily sent to any other plugin(s) and/or the main script with a total of 2 World.Execute calls (one in the dummy plugin, one in EventManager). If you have a bunch of plugins that need OnPluginPartialLine's output, then "wrapping" it in a custom event will save you a whole bunch of regexp COM calls.

In a more general sense, this technique can make the approach to scripting complex, highly integrated systems (like Achaean/Aetolian/Imperian combat systems for example) a much more modular one. Being able to define and propagate events saves you from having to remember exactly which subs in which plugins need to be called from which subs in which plugins, and allows you to spread the code across plugins, isolate the tasks more precisely, and make it all look very much like Python! :) I just had to mention Python, especially after messing with those dreaded vbScript arrays.
[Go to top] top

Posted by Tarbor   (5 posts)  [Biography] bio
Date Reply #2 on Thu 11 Mar 2004 11:13 PM (UTC)
Message
I'm not sure I fully understood how to use it by now but it sounds promising...

Thank you Ked, I'll give it a try!
[Go to top] top

Posted by Shadowfyr   USA  (1,786 posts)  [Biography] bio
Date Reply #3 on Fri 12 Mar 2004 01:25 AM (UTC)

Amended on Fri 12 Mar 2004 01:27 AM (UTC) by Shadowfyr

Message
Hmm. A good idea for events. The method I suggested before is best for calling subs though, since you can send pretty much anything except arrays through it. It may be useful in such cases to keep a list of registered "properties" as well, since the plugin maker can join an array into a mushclient variable and access it in what would seem to be a normal manner. Something like:

Alias: RegisterProperty (.*) (.*) (.*)

Exa: RegisterProperty "8019349d696b61bfba64b08a", "MyVar", "...type..."

Type would be Number, String or Array. the following SetProperty call would not be allowed on an array.

Then the calls would work like this:

ID = GetPluginID
GetProperty ID, "GetVar", "MyPlugin.MyVar"
SetProperty ID, "SetVar", "MyPlugin.MyVar", "Hello"
EnumerateProperty ID, "EnumVar", "MyPlugin.MyVar"

Basically, ID is the plugin making the request, GetVar, SetVar and EnumVar are where the value returned gets placed. The reason for the Enumerate version is that I am not sure you can send entire arrays. Enumrate type functions keep returning the next item in the array, until it hits the end, then it returns a value of False.

All three would be useful to have, but in general events don't return any data, you use other calls to do that, in which case the bare minimum would have to be two parameters, one being the ID of the plugin that caused the call and the second being the variable name(s) it stored the data in. Or maybe not. Your design will probably work in 90% of all cases and a plugin designer can always specify that they variable passed back to the sub needs to contain something like:

<ID of calling plugin>, <variable list>

Then the person whose plugin wants to talk to someone elses would simply have to follow what ever requirement the plugin maker required. This would in fact make my Property system completely unneeded.
[Go to top] top

Posted by Larkin   (278 posts)  [Biography] bio
Date Reply #4 on Thu 17 Feb 2005 02:06 PM (UTC)
Message
I don't understand why you use the plugin ID in the alias. It seems to me (at first glance) that this doesn't make things much easier or more readable. Don't you still have to put in all the plugin IDs for all the listeners when signalling your event?

This looks like something extremely useful, but I'm still trying to wrap my head around how it works to know if it will make my life easier. I want to make plugins for my scripts instead of having one big module, eventually.
[Go to top] top

Posted by Shadowfyr   USA  (1,786 posts)  [Biography] bio
Date Reply #5 on Thu 17 Feb 2005 04:47 PM (UTC)
Message
This was an incomplete system anyway Larkin and not much better than callplugin. There is absolutely no reason to 'register' listeners really. A different idea is to simple place an event handler into each plugin like:

<alias
   name="Handle_Event"
   match="Event\:(\w+) (.*)"
   enabled="y"
   regexp="y"
   keep_evaluating="y"
   send_to="12"
  >
  <send>temp_Events = split(Event_List,",")
        for each Event in temp_Events
          if "%1" = Event then
            call %1 (%2)
          end if
        next</send>
</alias>

Basically, this would cause 'any' event to fire that was listed in the plugins event_list string, by adding it to a string that loads at startup. If you executed a call to something that didn't exist, then it would likely cause an error. This means instead that you can avoid that, but still only add the above alias and do the following in your plugin and have it work:

Event_List = "Event1,Event2,Event3"

sub Event1(a, b, c)
...
end sub

sub Event2(a, b)
...
end sub

sub Event3(a, b, c, d)
...
end sub

The whole listeners thing was a way to have a single plugin manage all the connections and only send events to the plugins that requested it. It would mean that for a single event that 20 plugins wanted, it would execute not '1' command, but 20, one for each plugin that wanted that event. Not exactly efficient.
[Go to top] top

Posted by Ked   Russia  (524 posts)  [Biography] bio
Date Reply #6 on Fri 18 Feb 2005 05:42 AM (UTC)
Message
Yeah, needing to register the listeners is an overkill. I added it because I supposed that too many interplugin dependancies could turn into a debugging nightmare, so registering was supposed to provide a way of figuring out what is listening to what and where. But it didn't prove very helpfull. Plus you are already using a much better system anyway Larkin - the one in PyAchaea, it's not as specifically geared towards events, but lets you do the same thing and more, and is faster by orders of magnitude (even if it works only in Python). Alias events like this can be very slow, especially if used often. Setting up an event in PyAchaea should be easy (although messy) enough with variables, like this:


from PyAchaea.Avatar import Avatar
my_avatar = Avatar(world)

def OnPromptHandler(health, mana):
    world.Note("Health is: %s" % health)
    world.Note("Mana is: %s" % mana)

evt = my_avatar.getVar("Events_OnPrompt")
if type(evt) != type(list()):
    evt = []
if not OnPromptHandler in evt:
    evt.append(OnPromptHandler)    
my_avatar.setVar("Events_OnPrompt",evt)


Then the event can be dispatched like this:


from PyAchaea.Avatar import Avatar
my_avatar = Avatar(world)

for i in my_avatar.getVar("Events_OnPrompt"):
    i(3500, 3400)


I still can't sit down and decide what exactly should be the _official_ way of doing this, so any suggestions are certainly welcome.
[Go to top] top

The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).

To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.


22,568 views.

It is now over 60 days since the last post. This thread is closed.     [Refresh] Refresh page

Go to topic:           Search the forum


[Go to top] top

Quick links: MUSHclient. MUSHclient help. Forum shortcuts. Posting templates. Lua modules. Lua documentation.

Information and images on this site are licensed under the Creative Commons Attribution 3.0 Australia License unless stated otherwise.

[Home]


Written by Nick Gammon - 5K   profile for Nick Gammon on Stack Exchange, a network of free, community-driven Q&A sites   Marriage equality

Comments to: Gammon Software support
[RH click to get RSS URL] Forum RSS feed ( https://gammon.com.au/rss/forum.xml )

[Best viewed with any browser - 2K]    [Hosted at HostDash]