Causas del Evento

Los eventos son ideales para adjuntar lógica adicional a las acciones de juego, pero tienen la desventaja de proporcionar casi ningún contexto en cuanto a lo que ha causado que el evento ocurra. El objeto :javadoc:`Causa` permite proporcionar y recibir información contextual adicional sobre el evento. Esta información contextual puede entonces ser utilizada para modificar el comportamiento de su detector de eventos.

Por ejemplo, un complemento de protección de mundo necesita información sobre que jugador ha causado un ChangeBlockEvent para que ocurra antes de que puedan decidir si el evento debe ser cancelado o no. En lugar de ir con la ruta tradicional para crear una multitud de subeventos para las diferentes condiciones de fuente esta información en su lugar lo proporciona en la Causa del evento.

Every event provides a Cause object which can be interrogated for the information pertaining to why the event was fired. The Cause object can be retrieved from an event by simply calling Event#cause().

Cause and Context

The Cause object contains two distinct sets of information, the cause stack and the EventContext.

  • The cause stack of the event are the direct causes captured in order of importance. There are no names attached to the objects in the cause stack.

  • The event context contains extra information surrounding the event. Contexts are attached to keys but have no order, they are all equally important.

As an example, if a sheep owned by a player eats some grass, the most direct cause of this is the sheep. The player would be in the EventContext as the EventContextKeys#PLAYER, giving event consumers that additional information about how the event has come about, but would not necessarily be within the direct cause itself.

Another example that you may need to watch out for is if you simulate a player. The simulated player may not be in the direct cause, as the player being simulated may not have been involved in the action, however, the player would be in the EventContext under the EventContextKeys#PLAYER_SIMULATED

Retrieving Objects from the Direct Cause

Estructuralmente, un objeto de Causa contiene un lista secuencial de objetos. Existen varios métodos para recuperar información de un objeto de Causa que discutiremos aquí, para un listado más completo, por favor, vea el Javadocs.

Nota

Lo objetos dentro de una causa se ordenan de tal forma que el primer objeto será la causa más inmediata del evento y los siguientes objetos son de importancia decreciente y/o pueden solo proporcionar información contextual.

Cause#root() devuelve el primer objeto dentro de la causa. Este objeto es la causa más inmediata o directa del evento. Ya que una Causa no puede estar vacía, está garantizado tener una raíz.

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, ...]

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

Both optionals would contain the player object as its 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) es similar a Cause#first(Class) excepto que devuelve el último valor en la cadena de causa que coincide con el tipo.

Continuando el ejemplo anterior, si en su lugar de cambiamos para llamar Cause#last(Class) la primera opción contendría todavía el objeto de jugador, pero la segundo opción contendría ahora la entidad que pasamos en la segunda posición de la causa.

Cause#containsType(Class) devuelve un valor boolean y puede ser utilizado para verificar si una cadena de causa contiene algún objeto que coincida con el tipo proporcionado.

Cause#all() simplemente devuelve todos los objetos dentro de la causa permitiendo un manejo más avanzado.

Event Context

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 EventContext comes in. The event context allows objects to be associated with unique names, in the form of EventContextKeys, allowing them to be easily identified and requested. Some examples of use cases for named causes is the Notifier of a ChangeBlockEvent.All or the Source of a DamageEntityEvent.

Unlike the cause stack, which makes no guarantees as to the objects contained witin it, an object associated with a EventContextKey is guaranteed to be of the type specified by the key.

Retrieving a entry from the context of a cause

@Listener
public void onGrow(ChangeBlockEvent.All event) {
    Optional<UUID> notifier = event.cause().context().get(EventContextKeys.NOTIFIER);
}

This example makes use of EventContext#get(EventContextKey) which can be used to retrieve the expected object associated with a name if it is present within the context. Additionally EventContext#asMap() provides a Map<EventContextKey<?>, Object> which can be used to find all present EventContextKeys and their associated objects.

Nota

Some common identifying names for EventContextKeys are present as static fields in the EventContextKeys class.

Creating Custom Causes

Creating a cause is easy, but depends on whether you are creating your cause on the main server thread or async.

Nota

Los objetos de causa son inmutables por lo tanto no pueden ser modificados una vez creados.

Using the CauseStackManager

Advertencia

The CauseStackManager only works on the main server thread. If you call it from a different thread, an IllegalStateException will be thrown. Ensure you are on the main server thread before calling methods on the CauseStackManager.

If you are creating your event on the main thread, then use the CauseStackManager from the appropriate Engine, which can be found at Engine#causeStackManager(). The CauseStackManager tracks the potential causes of events as the game runs, allowing for easy retrieval of the current Cause without effort. To see the current cause, call CauseStackManager#currentCause(). You may notice that your plugin’s PluginContainer is already in the returned Cause, as plugins are one of the objects tracked by the manager. Using the CauseStackManager for creating causes removes the need for boilerplate-like code where you supply objects like your plugin container, so that you can concentrate on adding your own causes.

Before adding your own causes, you should push a cause stack frame to the manager. Adding a frame acts as a saved state, when you are done with your causes, the removal of the frame returns the manager to its original state.

Truco

Adding a frame to the CauseStackManager does not remove what is already in the manager, so anything that is in the cause stack and contexts before a stack frame is added will be there afterwards. You can verify this by calling Sponge.server().causeStackManager().currentCause() before and after the frame is pushed.

For example, if the cause stack contains a PluginContainer and a ServerPlayer when a frame is pushed, they will remain on the stack and will form part of the Cause if one is obtained from the frame.

For example, if you were to fire an event that was simulating another player in a sudo like command, you may want to add the player you are acting as in the cause and the GameProfile of the player that you are simulating in the context (as the simulated player is not directly responsible for the event being fired.)

Creating a custom Cause with the CauseStackManager

In this example, the variables would be populated, the cause would contain the playerToSimulate as the root cause, the sourceRunningSudo as the second object in the cause and the GameProfile as the EventContextKeys#PLAYER_SIMULATED context, in addition to anything already in the CauseStackManager. Your event code would be at the bottom of the method.

CommandSource sourceRunningSudo = ...;
Player playerToSimulate = ...;
try (CauseStackManager.StackFrame frame = Sponge.server().causeStackManager().pushCauseFrame()) {

  frame.pushCause(sourceRunningSudo);
  frame.pushCause(playerToSimulate);

  frame.addContext(EventContextKeys.PLAYER_SIMULATED, playerToSimulate.getProfile());

  Cause cause = frame.currentCause();
}

Note that the last item you push to the cause stack will be the root of the Cause as stacks are «last in, first out» (LIFO) structures.

Truco

For more information about the stack data type and why the order matters, see the Stack javadocs or this Wikipedia article.

Using the Cause Builder

If you are creating an event that does not fire on the main thread, you cannot use the CauseStackManager. Instead, you will need to create a Cause object manually.

Creating a cause object is easy using the Cause.Builder. You can obtain a builder by calling Cause.builder(). To add a cause to the builder, use the Cause.Builder#append(Object) method, but note that unlike the CauseStackManager, the first element you add will be the root, not the last.

If you wish to add contexts, there is a separate builder for those, the EventContext.Builder, accessed by calling EventContext#builder(). The EventContext can then be added using the Cause.Builder#build(EventContext) when you have finished building the Cause up.

Taking the previous example, this is how we would build it using the cause builder.

Creating a custom Cause with the Cause and EventContext builders

Note that in this example, the variables would be populated, and that the first entry appended to the cause would be the root cause.

CommandSource sourceRunningSudo = ...;
Player playerToSimulate = ...;
PluginContainer plugin = ...;

EventContext context = EventContext.builder()
  .add(EventContextKeys.PLAYER_SIMULATED, playerToSimulate.profile())
  .add(EventContextKeys.PLUGIN, plugin)
  .build();

Cause cause = Cause.builder()
  .append(playerToSimulate)
  .append(sourceRunningSudo)
  .append(plugin)
  .build(context);

Think carefully about what information to include in your cause. If you’re firing an event from your plugin which is usually triggered through other means, it is a good idea to include your PluginContainer in the cause so other plugins know that the event comes from your plugin. 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.