Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documentation for event filters #379

Merged
11 commits merged into from
Jan 10, 2016
122 changes: 122 additions & 0 deletions source/plugin/event/causes.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
============
Event Causes
============

Events are great for attaching additional logic to game actions, but
they have the drawback of providing next to no context as to what has **caused** that event to
occur. The ``Cause`` object allows providing and receiving additional contextual information about
the event. This contextual information can then used to modify the behavior of your event listener.

For example, a world protection plugin needs information on what player has caused a
ChangeBlockEvent to occur before they can decide if the event should be cancelled or not.
Rather than go with the traditional route of creating a multitude of subevents for the different source
conditions this information is instead provided in the ``Cause`` of the event.

Any event which extends ``CauseTracked`` provides a ``Cause`` object which can be interogated
for the information pertaining to why the event was fired. The Cause object can be retrived from
an event by simply calling ``event.getCause()``.

Retrieving objects from a Cause
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Structurally, a ``Cause`` object contains a sequential list of objects. There are several methods of
retrieving information from a Cause object which we will discuss here, for a more complete
listing please see the javadocs **link**.

.. note::

The objects within a cause are ordered such that the first object is the most immediate
cause of the event, and subsequent objects are of decreasing importance and/or may only
provide contextual information.

``Cause#root()`` returns the first object within the cause, if it exists. This object is the most
immediate or direct cause of the event.

``Cause#first(Class)`` returns the first object in the cause chain whose type is either the
same as or is a subtype of the given class. For example given a cause which contained a player
followed by an entity ``[Player, Entity, ...]``

.. code-block:: java

@Listener
public void onEvent(ExampleCauseEvent event) {
Cause cause = event.getCause(); // [Player, Entity]
Optional<Player> firstPlayer = cause.first(Player.class); // 1
Optionml<Entity> firstEntity = cause.first(Entity.class); // 2
}

Both optionals would contain the player object as it's type directly matched request for a
Player type and it matched the request for an Entity type as Player is a subtype of Entity.

``Cause#last(Class)`` is similar to *Cause#first(Class)* except it returns the last value in
the cause chain matching the type.

Continuing from the example above, if we instead changed it to call ``Cause.last(Class)`` the first
optional would contain the player object still, but the second optional would now contain
the entity that we passed in the second position of the cause.

``Cause.has(Class)`` returns a boolean value and can be used to check if a cause chain
contains any object matching the provided type.

``Cause.all()`` simply returns all objects within the cause allowing more advanced handling.

Named Causes
~~~~~~~~~~~~

Sometimes the ordering of objects within the cause isn't enough to get the proper idea of what
an object represents in relation to the event. This is where ``NamedCause`` comes in. Named
causes provide a method for tagging objects within a cause with a **unique** name allowing them
to be easily identified and requested. Some examples of use cases for named causes is the
`Notifier` of a GrowBlockEvent or the `Source` of a DamageEntityEvent.

**Retrieving a named entry from a cause**

.. code-block:: java

@Listener
public void onGrow(GrowBlockEvent event) {
Optional<Player> notifier = event.getCause().get(NamedCause.NOTIFIER);
}

This example makes use of ``Cause.get(String name)`` which can be used to retrive the object
associated with a name if it is present within the cause chain. Additionally
``Cause.getNamedCauses()`` provides a ``Map<String, Object>`` which can be used to find all
present names and their associated objects.

.. note::

Some common identifying names for ``NamedCause``\ s are present as static fields in the
``NamedCause`` class. Identifiers which are specific to certain events can often be found
as static fields on the event class, for example ``DamageEntityEvent.SOURCE``.

Creating custom Causes
~~~~~~~~~~~~~~~~~~~~~~

Creating a cause to use when firing an event is extremely easy. The hardest part is deciding
what information to include in the cause. If you're firing an event from your plugin which is
usually triggered through other means perhaps you want to include your plugin container so
other plugins know that the event comes from your plugin. Or if you are firing the event on
behalf of a player due to some action it's usually a good idea to include that player in
the cause.

.. note::

Cause objects are immutable therefore cannot be modified once created.

In the simplest case where you don't want to provide any information you can simply use
``Cause.of()`` to get a cause which contains no objects.

Using ``Cause.of(Object...)`` or ``Cause.ofNullable(Object...)`` you can construct a cause
from a series of objects. The objects will be added to the cause chain in the order that they
are passed to the method, so the first object parameter will become the root cause.

If you already have a cause object and would like to append some more objects to the
chain you can use ``Cause.with(Object...)``. This constructs a new Cause object containing
first the objects already present in the original cause, then followed by the additional
objects that you provided.

Finally if you wish to add an object to a cause with a defined named first call
``NamedCause.of(String name, Object object)`` and then pass the returned ``NamedCause`` instance
to the cause chain as you would a normal object. Simply calling ``Cause.of(name, object)``
will result in a cause chain containing two objects (the String name, and then the object),
rather than one object with an attached name.
84 changes: 84 additions & 0 deletions source/plugin/event/custom.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
======================
Creating Custom Events
======================

You can write your own event classes and dispatch those events using the method described above.

An event class must either implement the ``Event`` interface or extend the ``AbstractEvent`` class.

If you want your event to be cancellable, the class must also implement ``Cancellable``.

Example: Custom Event Class
~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code-block:: java

import org.spongepowered.api.entity.player.Player;
import org.spongepowered.api.event.impl.AbstractEvent;
import org.spongepowered.api.event.Cancellable;

public class PrivateMessageEvent extends AbstractEvent implements Cancellable {

private boolean cancelled = false;

private Player sender;
private Player recipient;

private String message;

public Player getSender() {
return sender;
}

public Player getRecipient() {
return recipient;
}

public String getMessage() {
return message;
}

@Override
public boolean isCancelled() {
return cancelled;
}

@Override
public void setCancelled(boolean cancel) {
cancelled = cancel;
}

public PrivateMessageEvent(Player sender, Player recipient, String message) {
this.sender = sender;
this.recipient = recipient;
this.message = message;
}
}


Example: Fire Custom Event
~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code-block:: java

game.getEventManager().post(new PrivateMessageEvent(playerA, playerB, "Hello World!"));


Example: Listen for Custom Event
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. code-block:: java

import org.spongepowered.api.text.Texts;
import org.spongepowered.api.text.chat.ChatTypes;

@Listener
public void onPrivateMessage(PrivateMessageEvent event) {
if(event.getMessage().equals("hi i am from planetminecraft")) {
event.setCancelled(true);
return;
}

String senderName = event.getSender().getName();
event.getRecipient().sendMessage(ChatTypes.CHAT, Texts.of("PM from " + senderName + ": " + event.getMessage()));
}
155 changes: 155 additions & 0 deletions source/plugin/event/filters.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
=============
Event Filters
=============

Now that you've spent a bit of time working with events you've probably noticed that there are several preconditions that you
very commonly check while writing an event listener. Event filters are a group of annotations that assist you by allowing you
to automatically validate aspects of the event, and if the validation fails then your listener will not be called. This allows
your listener to be dedicated to the logic of your handler, rather than the preconditions, resulting in cleaner code.

Event filters come in two varieties, event type filters and parameter filters.

Event type filters are method annotations that are applied to your listener method along with the ``@Listener`` annotation and
provide several filters based on the type or state of the event.

Parameter filters validate objects contained within the event such as the cause. They come in a further two varieties
parameter sources and parameter filters. Each additional parameter must have one source annotation, and optionally may include
any number of filter annotations.

Event Type Filters
==================

**@Include/@Exclude**
These two parameters are used in conjunction with listening for supertype events such as ``EntityEvent`` while narrowing the
events that you receive to just a subset of the events extending the event you're listening for.

For example:

.. code-block:: java

@Listener
@Exclude(InteractBlockEvent.Primary.class)
public void onInteract(InteractBlockEvent event) {
// do something
}

This listener would normally be called for all events extending InteractBlockEvent. However, the ``Exclude`` annotation
will prevent your listener from being called for the ``InteractBlockEvent.Primary`` event (leaving just the
``InteractBlockEvent.Secondary`` event).

An example with ``@Include`` could be:

.. code-block:: java

@Listener
@Include({DamageEntityEvent.class, DestructEntityEvent.class})
public void onEvent(EntityEvent event) {
// do something
}

This listener would normally be called for all EntityEvents, however the ``Include`` annotation narrows it to
only recieve ``DamageEntityEvent`` and ``DestructEntityEvent``\ s.

**@IsCancelled**
This annotation allows filtering events by their cancellation state at the time that your event listener would normally be
called. By default your event listener will not be called if the event has been cancelled by a previous event listener.
However you can change this behavior to one of three states depending on the ``Tristate`` value in the ``@IsCancelled``
annotation.

- ``Tristate.FALSE`` is the default behavior if the ``IsCancelled`` annotation is not present, and will not call your
listener if the event has been cancelled.
- ``Tristate.UNDEFINED`` will cause your listener to be called regardless of the cancellation state of the event.
- ``Tristate.TRUE`` will cause your listener to be called only if the event has been cancelled by a previous event listener.

Parameter Filters
=================

Parameter filters allow you to filter based on objects within the event. These annotations come in two types, sources and
filters. Each additional parameter for your listener method, beyond the normal event parameter, requires exactly one source
annotation and may optionally have any number of filter annotations.

Parameter Source Annotations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Parameter source annotations tell the event system where it should look for this parameter's value. This may be in the events
cause or in a member of the event object itself.

**@First** This parameter source annotation tells the event system to find the first object in the event's cause which matches
the type of your parameter (This is equivalent to ``Cause#first(Class<?>)``). If no object is found matching this parameter
then your listener is not called.

**In this example your listener will only be called if there is a player in the event's cause, and the ``player`` parameter
will be set to the first player present the cause.**

.. code-block:: java

@Listener
public void onInteract(InteractBlockEvent.Secondary event, @First Player player) {
// do something
}

**@Last** This is similar to ``@First`` however it instead makes a call to ``Cause#last(Class<?>)``.

.. code-block:: java

@Listener
public void onInteract(InteractBlockEvent.Secondary event, @Last Player player) {
// do something
}

**@All** This parameter source annotation requires that the annotated parameter be an array
type. The returned array will be equivalent to the contents of calling ``Cause#all(Class<?>)``. By default if the
returned array would be empty then the validation fails however this can be disabled by setting ``ignoreEmpty=false``.

**In this example your listener will always be called, although the players array may be empty if the event's cause contained
no players.**

.. code-block:: java

@Listener
public void onInteract(InteractBlockEvent.Secondary event, @All(ignoreEmpty=false) Player[] players) {
// do something
}

**@Root** This parameter source annotation will fetch the root object of the cause, equivalent to ``Cause#root()``.
It also performs an additional check that the type of the root object matches the type of your parameter.

Parameter Filter Annotations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Parameter filter annotations add additional validation to objects returned from parameter source annotations. As with all
event filters if any of these validations fail then your listener will not be called.

**@Supports**
This parameter filter may be applied to any parameter type which is a ``DataHolder``. It takes a class extending
``DataManipulator`` as its parameter and validates that the annotated DataHolder supports the given DataManipulator
ype. This validation is equivalent to ``DataHolder#supports(Class<? extends DataManipulator>)``.

**In this example the listener will be called only if there is an entity in the event's cause, and if that entity supports
the data manipulator ``FlyingData``.**

.. code-block:: java

@Listener
public void onInteract(InteractBlockEvent.Secondary event, @First @Supports(FlyingData.class) Entity entity) {
// do something
}

**@Has**
This parameter filter is similar to the ``@Supports`` parameter filter except that it additionally validates that the
``DataHolder`` contains an instance of the given ``DataManipulator``. This validation is equivalent to
``DataHolder#hasData(Class<? extends DataManipulator>)``.

**In this example the listener will be called only if there is an entity in the event's cause, and if that entity has an
instance of ``FlyingData`` available.**

.. code-block:: java

@Listener
public void onInteract(InteractBlockEvent.Secondary event, @First @Has(FlyingData.class) Entity entity) {
// do something
}

.. note::
Both ``@Has`` and ``@Supports`` have an optional parameter ``inverse()`` which can be set to cause validation
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe remove the brackets?

to fail if the does have, or does support, the target DataManipulator.
Loading