The gory details of Event filtering

Overview

An Event is something that has happened.  If Joe says "hi" an event will be created for this, and it will propagated around the event hierarchy, ultimately reaching all objects that have ears and are within hearing range of Joe (in the simplest case).   The event will travel along the branches of the container tree, and will multiply itself at some nodes and spread along several branches.  Each node may affect ("filter") this event in different ways, depending on what type of event it is and which direction it is travelling.

Here is an example of propagation through a container tree:

The node with the blue circle (n3) has generated an event.  The red nodes are interested in receiving events ("have ears" so to speak).  The white nodes are not interested, although they have to help ensure that the red nodes receive the event.

receiveEvent vs processEvent

Each Physical has two methods: receiveEvent and processEvent.  The difference is important.  ReceiveEvent is used for event propagation - even if an item is totally uninterested in events (like the white nodes above), it may still receive events that are to be propagated along to other nodes.  ProcessEvent is used when an event has been received that this item is personally interested in processing.

In the above example n3 has produced an event, this will result in a n3.receiveEvent call.  Here the event willl be propagated in all directions, ie both children (n1 and n2) and the parent (n4).  N3, however, is interested in that event type himself, which means he should "hear himself".  So n3.processEvent will also be called - that is where n3 gets to respond to his own event.

The same thing will happen to n1 and n2.  ReceiveEvent will be called first, and from there processEvent will be called since they are registered as listeners for that type of event.

N5 will receive the event, but is not in itself interested in processing the event.  So it will propagate it on up to n6 and down to n4 without calling it's own processEvent.  And so on.

ReceiveEvent is called from "the outside" and will take care of event propagation and event filtering.  It will also call processEvent on the same object if desired.  ProcessEvent, on the contrary, is a "dead end" in event propagation.  This is where someone really receives the event and gets to respond to it.

Filtering

From the point of view of the ReceiveEvent method, an event can travel to and arrive from three different "directions".  It can come from above (the parent), below (a child), and from the node itself (self).  I'll use the abbreviations P, C, and S.  When n3 produces the event it will do four progations: to n6 (S->P), to n1 (S->C), to n2 (S->C), and finally to itself (S->S).  N5 will propagate to n4 (C->C) and n6 (C->P).  In total there are seven types of propagation possible: The only missing combination is Parent->Parent, since that would cause a recursive behaviour.  Note that ->Child and ->Parent means calling receiveEvent, while ->Self means calling processEvent.  The exact usage of each filtering type is illustrated later in this document.

Each Physical has a set of EventFilters.  An event filter is a "black box" type object that, given an event, will produce a filtered event (which may be the same event unmodified).  You stuff in an event, indicate the source type (P, S, or C) and destination type (P, S, C) and a new filtered event will pop out on the other side.

You can connect lots of these together to produce an EventFilterCollection which acts just like a single EventFilter.  Before receiveEvent propagates an event, the event will be run through this EventFilterCollection.

Filter example: Dome of silence

Here's an example of a filter.  The DomeOfSilence spell is cast on an area or a square and will add a silencing filter that stops all sounds from entering, although sounds produced from within will be heard from the outside.  People inside the dome won't hear each other either.  Now let's say this is cast on a house.  The filter will do the following for each type of propagation:
 
Self->Self Nothing If the house itself was alive and had ears, it would be able to hear it's own words...
Self->Child Nothing If the house itself could talk, the inhabitants of the house would be able to hear what the house said
Self->Parent Nothing If the house itself could talk, people outside the house would be able to hear it. 
Child->Self Nothing If the house had ears it would be able to hear what the inhabitants inside are saying
Child->Child Kill all sound The inhabitants inside cannot hear each other talking
Child->Parent Nothing People outside the house can hear what the people inside are saying
Parent->Self Nothing If the house had ears, it would be able to hear what people outside are saying
Parent->Child Kill all sound The people inside cannot hear what the people outside are saying.
 

Filter diagrams

The following diagrams illustrate how the event propagates through the different methods and filters on it's way around the container hierarchy.  I've drawn one diagram for when an event arrives from the parent, one for when an event arrives from a child, and finally one where the event is generated by the item itself.  The blue circle is the source of the event.  The diamonds represent the different types of filtering.

Events from parent:

Events from child:

Events from self:

The ItemListener interface

Anyone can register itself as an ItemListener to an Item.  All you have to implement is the ItemListener interface and call the addListener method on the Item you want to listen to.  The ItemListener interface provides a receiveEvent method, which the Item uses to pass events to the listener.

There are two types of ItemListeners, and it is important to understand the distinction.  I'll use the term host to refer to the Item that we are listening to:

  1. ItemListeners that want to receive all events that the host receives.  These will be the same events that the children of the Item receive, ie events that have been X->Child filtered.
  2. ItemListeners that want to receive all events as heard by the host.  This means it will receive exactly the same events the are passed to the processEvent method of the host, ie events that have been X->Self filtered.
If A adds itself as a child of B, then A will automatically get registered as a listener to B (if A wants events).  By default A will be want X->Child filtered events.  That means if someone casts a spell that makes B deaf, it won't affect A at all.

Soul is a good example of the second type of ItemListener.  These want to hear events as heard by the host.  This means if the host becomes deaf suddently, the soul won't hear anything either.  But the cat that the host is holding (and is also a child of the host) will be unaffected - events will pass right through to the cat.

The ItemListener provides the following method to indicate the type:

   public boolean wantsPrivateEvents();

I use the term "private event" for events that are sent to the processEvent method of the host, ie stuff that the host really hears.  In most cases this will return false, but listeners that really want to hear what the host hears should return true.

Indicating which event types are desired

For now you cannot indicate which type of events you like to listen to.  So if you are a registered ItemListener you will receive all kinds of events.  This is a matter of performance.  I'm not sure what will give best performance - to keep track of which types of events are desired and minimize unnecessary event propagation, or to keep things simple and just send all event types.  Time will tell, I might change this in the future so that the nodes keep track of which types of events are desired by who.

Automatic registration of ItemListeners

Physical has the following method:

 public boolean wantsParentEvents();

If wantsParentEvents() returns true then that Physical will always be registered as an ItemListener to it's parent.  When it moves around in the container hierarchy, it will automatically be deregistered from it's old parent and registered to it's new parent.  The default behaviour is that my wantsParentEvents() method returns true only if I have others listeners registered to me (ie there are items that want to receive events from me, so I therefore want to receive events from my parent).

If an item always wants to receive events regardless of whether or not it has listeners it should override this method and simply return true.  Soul is an example of this - it always wants to receive events.  So when the Soul is added to the body using body.addEffect(soul), the soul will become a child of the body and, since wantsParentEvents() = true, it will automatically get registered as an ItemListener to the body.  Vóila.

Summary of propagation and filtering methods


Henrik Kniberg

Last updated: