[crossfire] Proposal: Unified event system
Robin Redeker
elmex at ta-sa.org
Fri Sep 8 06:04:42 CDT 2006
Hi!
On Thu, Sep 07, 2006 at 10:57:27PM -0600, Alex Schultz wrote:
> These things that exist or are planned all use some event-type constructs:
[.snip.]
This all sounds similiar to what we implemented in Crossfire+, where
the plugin/extension (Perl) API was completly rewritten (mostly in Perl and C++).
And it works like this (long mail ahead!):
You can "attach" extensions to global events, to type/subtypes,
to specifix objects, to players and to maps.
On top of that a extension can implement new user commands.
If a extension for example wants to attach itself to all jeweler
skills it has to attach itself like this:
cf::attach_to_type cf::SKILL, cf::SK_JEWELER,
on_use_skill => sub {
... handling code here ...
};
This function attaches itself to the type SKILL with the subtype
SK_JEWELER. And everytime someone uses the skill the callback
registered as 'on_use_skill' is called.
Multiple extensions can attach themself to this type, and they
can specify a priority with 'prio => -100' to be executed earlier.
You can also attach a Perl package to the skill like this:
cf::attach_to_type cf::SKILL, cf::SK_JEWELER,
package => 'Crossfire::JewelerSkill';
cf::attach_to_objects will attach handlers for events on _all_ objects
in the game, this is mainly for debugging purposes, as it will produce a
high load.
The map attachments work like this:
If a extension wants to attach itself to the 'trigger' event (this is
the event that is generated when a connection is activated (pushed or
released)), it has to do this:
cf::attach_to_maps
on_trigger => sub {
my ($map, $connection, $state) = @_;
print "CONNECITON TRIGGERED: $connection : $state\n";
for ($map->get_connection ($connection)) {
print "connected obj: " . $_->name . "\n";
}
};
This small attachment dumps all connection activations and the connected
objects. If this attachment now decides to overwrite connection 10, so that
nothing happens if it is triggered, it has to do this:
cf::attach_to_maps
on_trigger => sub {
my ($map, $connection, $state) = @_;
if ($connection == 10) {
cf::override;
return; # the idiom 'return cf::override;' is quite common in our code
# but i want to empasize that cf::override is just a functioncall
}
};
The call of cf::override sets a flag in the event system that will prevent any execution
of further code that handles this event. This way all attachments with 'lower' priority
and the C/C++ code is inhibited/overridden.
Here is a longer code snippet that logs all interesting events to our IRC channel
irc.schmorp.de #cf: http://www.ta-sa.org/files/txt/38c2f5e803590d04866c1b3cc889d315.txt
The attachments are not limited to 'an extension attaches itself to <...>', a map or an
objects itself can attach to a 'registered' attachment. For example our nice cat which runs
around in scorn and heals people at random has following line in it's archetype:
Object nekosan
...
attach [["Nekosan"]]
end
The value of the attach field is a 2 dimensional array in JSON (JavaScript Object Notation)
is a array of arrays which contain following fields:
[ <attachment name/key>, { <key value pairs of arguments for the attachment> }]
The code side of this looks like this:
cf::register_attachment "Nekosan", package => __PACKAGE__;
This registeres an attachment under the name/key 'Nekosan' so that objects like our
cat can attach itself.
Where the package defines for example this function:
sub on_monster_move {
my ($self, $enemy) = @_;
...
}
$self is a mostly empty object, the attachment object, which is initalized
with the arguments that are given in the 'attach' value, if nekosan would have an
attach field like this:
attach [["Nekosan", {"foo":"bar"}]]
The attached function can access the value of the key "foo" like this:
$self->{Nekosan}->{foo}
This way multiple different attachments have a seperate field for storing
their arguments.
If a extension wants to redefine a user command it does it like this:
cf::register_command invite => 10, sub {
my ($who, $args) = @_;
...
}
This registers the 'invite' command with the execution time of 10. The
arguments to the function are ($who, $args), where $who is the player
and $args the argument string to the command.
This is what was implemented in Crossfire+ and works very well, from time to
time we find missing events, which are added very quickly to the (now) C++ code:
Here an example for move_trigger in move_apply ():
Add a line to doc/events.pod with documentation looking like this:
=head3 move_trigger (object victim originator -- )
Invoked whenever a trap-like B<object> has been activated, usually by
moving onto it. This includes not just traps, but also buttons, holes,
signs and similar stuff.
And put some lines like this to move_apply:
if (INVOKE_OBJECT (MOVE_TRIGGER, trap, ARG_OBJECT (victim), ARG_OBJECT (originator)))
goto leave;
This invokes all attachments that are interested in the object event MOVE_TRIGGER, with
the object being 'trap' and the arguments victim and originator. This is all C++ code
that has to be added when one wants to add an event. 'make' will update include/eventinc.h
from the events.pod automatically when building.
Here is the documentation for all this, partly unfinished, but there are many examples:
A very unfinished reference for all this can be found at http://cf.schmorp.de/doc/development/cf.pm.html.
A reference for all possible events can be found at http://cf.schmorp.de/doc/development/events.html.
Examples can be found in http://cvs.schmorp.de/cf.schmorp.de/maps/perl/.
The NPC dialogue system might also be interesting: http://cf.schmorp.de/doc/development/NPC_Dialogue.html
If there are any questions feel free to ask.
I hope this small guide through the plugin/extension system in Crossfire+ helps a bit
(it maybe shows the way you don't want to go) when redesigning the plugin system for Crossfire.
cheers,
Robin (part of the Crossfire+ development team)
More information about the crossfire
mailing list