Causes des Events

Les événements sont utiles pour attacher une logique supplémentaire aux actions du jeu, mais ils ont l’inconvénient de ne fournir quasiment aucun contexte quant à ce qui a provoqué cet événement. L’objet Cause permet de fournir et de recevoir des informations contextuelles à propos de l’événement. Ces informations peuvent être utilisées pour modifier le comportement de votre event listener.

Par exemple, un plugin de protection de monde a besoin d’informations sur quel joueur a causé l’appel du ChangeBlockEvent avant de pouvoir décider si l’événement doit être annulé ou pas. Plutôt que de passer par la voie traditionnelle avec la création d’une multitude de sous-événements pour les différentes conditions de source, cette information est fournie dans la Cause de l’événement.

Chaque événement fournit un objet Cause qui peut être examiné pour obtenir des informations relatives à pourquoi l’événement a été déclenché. L’objet Cause peut être récupéré à partir d’un événement en appelant simplement Event#getCause().

Cause et Contexte

L’objet Cause contient deux ensembles distincts d’informations, la pile de causes et l” EventContext.

  • La pile de causes de l’event est la cause directe capturée par importance. Il n’y a pas de noms attachés aux objets constituant cette pile.

  • Le contexte de l’event contient des informations supplémentaires sur cet event. Les contextes sont attachés à des clés, et n’ont pas d’ordre, ils ont tous la même importance.

Par exemple, si un mouton appartenant à un joueur mange de l’herbe, la cause directe est ce mouton. Le joueur sera dans l” EventContext en tant qu” EventContextKeys#OWNER, donnant aux listeners de l’event d’autres informations sur comment l’event est arrivé, mais il n’est pas obligatoirement la cause directe.

Un autre exemple serait de vouloir savoir si vous simulez un joueur.Le joueur simulé ne sera peut être pas la cause directe puisqu’il ne sera pas impliqué dans l’action, mais il sera tout de même dans l” EventContext sous EventContextKeys#PLAYER_SIMULATED

Récupérer des objets de la cause directe

Structurellement, un objet Cause contient une liste séquentielle d’objets. Il y a de nombreuses méthodes de récupération des informations d’un objet Cause dont nous discuterons ici, pour obtenir une liste plus complète veuillez consulter les Javadocs.

Note

Les objets à l’intérieur d’une cause sont classés tels que le premier objet est la cause la plus immédiate de l’événement, et les objets suivants ont une importance décroissante et/ou peuvent seulement fournir des informations contextuelles.

Cause#root() retourne le premier objet à l’intérieur d’une cause. Cet objet est la cause la plus immédiate ou directe de l’événement. Puisqu’une Cause ne peut pas être vide, elle possède forcément une racine (root).

Cause#first(Class) retourne le premier objet dans la chaîne de la cause dont le type est soit identique, soit un sous-type de la classe donnée. Par exemple, pour une cause qui contient un joueur suivi par une entité : [Player, Entity, ...]

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

Les deux Optionals contiendraient l’objet joueur puisque son type correspond directement à la demande pour un objet de type Player, et il correspond aussi à la demande pour un objet de type Entity puisque Player est un sous-type d’Entity.

Cause#last(Class) est semblable à Cause#first(Class), sauf qu’il retourne la dernière valeur dans la chaîne de cause correspondant au type.

Pour continuer dans l’exemple ci-dessus, si au lieu de ça nous l’avions changé pour appeler Cause#last(Class), le premier Optional contiendrait encore l’objet Player, mais le second Optional contiendrait désormais l’entité que nous avons passé en seconde position de la cause.

Cause#containsType(Class) retourne un booléen et peut être utilisé pour vérifier si une chaîne de cause contient un objet correspondant au type fournit.

Cause#all() retourne simplement tous les objets à l’intérieur de la cause pour permettre une gestion plus avancée.

Contexte de l’event

Parfois, l’ordre des objets dans la cause n’est pas suffisant pour avoir une idée de ce que l’objet représente dans l’event. C’est là où l” EventContext entre en jeu. Ce contexte permet d’avoir des objets avec des noms uniques, sous forme d” EventContextKeys, leur permettant d’être facilement identifié et demandé. Quelques exemples de causes nommées sont le Notifier d’un ChangeBlockEvent.Grow ou la Source d’un DamageEntityEvent.

Contrairement à la pile de causes, qui ne garantit pas le type de l’objet contenu, la valeur d’un EventContextKey sera toujours du type spécifié par la clé.

Récupérer une entrée du contexte de la cause

@Listener
public void onGrow(ChangeBlockEvent.Grow event) {
    Optional<User> notifier = event.getCause().getContext().get(EventContextKeys.NOTIFIER);
}

Cet exemple utilise EventContext#get(EventContextKey) qui peut être utilisé pour récupérer l’objet associé à un nom s’il est présent dans le contexte. De plus, EventContext#asMap() fournit une Map<EventContextKey<?>, Object> qui peut être utilisée pour obtenir tous les EventContextKeys et leurs objets.

Note

Les EventContextKey communs sont présents dans des fields statiques dans la classe EventContextKeys.

Créer une cause personnalisée

Créer une cause est simple, mais dépend de si sa création se fait sur le thread principal ou en asynchrone.

Note

Les objets Cause sont immuables donc il ne peuvent pas être modifiés une fois créés.

Utiliser le CauseStackManager

Avertissement

Le CauseStackManager ne fonctionne que sur le thread principal du serveur, si ce n’est pas le cas, un IllegalStateException sera lancé. Assurez-vous d’être sur le thread principal avant d’appeler les méthodes du CauseStackManager.

Si vous créez votre event sur le thread principal, alors utilisez les CauseStackManager, qui peut être retrouvé sur Sponge#getCauseStackManager(). Le CauseStackManager traque les causes potentielles des events quand le jeu tourne, permettant de facilement récupérer la Cause actuelle sans effort. Pour voir la cause actuelle, appelez CauseStackManager#getCurrentCause(). Vous noterez que le PluginContainer de votre plugin est déjà dans la cause, puisque les plugins sont traqués par le manager. Utiliser le CauseStackManager pour créer des causes supprime le besoin de code répété où vous fournissez des objets comme le container de votre plugin, que vous pouvez donc concentrer en ajoutant vos propres causes.

Avant d’ajouter vos propres causes, vous devriez pousser une cause stack frame dans le manager. Ajouter une frame agot comme un état sauvegardé, quand vous en avez fini avec vos causes, la suppression de la frame rend au manager sont contenu original.

Astuce

Ajouter une frame au CauseStackManager ne supprime pas ce qui est déjà présent dans le manager, donc ce qui est déjà dans la pile de la cause et le contexte déjà présent avant que la frame ne soit générée sera là après la fermeture de cette frame. Vous pouvez vérifier cela en appelant Sponge.getCauseStackManager().getCurrentCause() avant et après que la frame ne soit poussée.

Par exemple, si la pile de la cause contient un PluginContainer et une CommandSource quand la frame est poussée, ils resteront dans la pile et formeront la `Cause si vous les obtenez depuis la frame.

Par exemple, si vous lancez un event qui simule un joueur dans une commande comme sudo, vous voudriez vouloir ajouter le joueur agissant comme cause et le GameProfile de ce joueur qua vous simulez dans le contexte (comme le joueur simulé n’est pas la cause responsable de l’event).

Créer une Cause personnalisée avec le CauseStackManager

Dans cet exemple, les variables seront remplies, la cause contiendrait le playerToSimulate comme racine de la cause, sourceRunningSudo sera le second objet dans la cause et le GameProfile dans EventContextKeys#PLAYER_SIMULATED à l’intérieur du contexte, en plus de tout ce que le CauseStackManager contient déjà. Votre code d’événement sera en bas de la méthode.

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

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

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

  Cause cause = frame.getCurrentCause();
}

Notez que le dernier objet poussé dans la pile de causes sera la racine de la Cause comme les stacks suivent la structure « last in, first out » (LIFO).

Astuce

Pour plus d’informations à propos des types de données des piles et du pourquoi l’ordre est important, voir les javadocs de Stack ou cet article Wikipedia.

Utiliser le builder de Cause

Si vous créez un event qui n’est pas lancé depuis le thread principal, vous ne pouvez pas utiliser le CauseStackManager, à la place, vous devrez créer une Cause manuellement.

Créer une cause est simple en utilisant le Cause.Builder, vous pouvez obtenir un builder en appelant Cause.builder(). Pour ajouter une cause au builder, utilisez Cause.Builder#append(Object), mais notez que contrairement au CauseStackManager, le premier élément que vous ajoutez sera la racine, par le dernier.

Si vous voulez ajouter des éléments au contexte, il y a un builder séparé, le EventContext.Builder, disponible via EventContext#builder(). L” EventContext peut êyte ajouté en utilisant Cause.Builder#build(EventContext) qyand vous avez finit de configurer votre Cause.

Pour remprendre l’exemple précédent, voilà comment nous construirons la cause en utilisant son builder.

Créer une Cause personnalisé avec les builders de Cause et l’EventContext

Notez que dans cet exemple, les variables sont assignées, et que la première entrée mise dans la cause sera la racine.

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

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

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

Pensez bien à quelles informations inclure dans la cause, si vous lancez un event depuis votre plugin, qui est généralement lancé depuis d’autres moyens, c’est une bonne idée de mettre votre PluginContainer dans la cause, pour que les autres plugins sachent que l’event vient de votre plugin. Si vous lancez l’event après l’action d’un joueur, c’est généralement une bonne idée d’inclure ce joueur dans la cause.