Système de Listeners (Écouteurs)
Le système d'événements/listeners est une architecture pilotée par les événements et basée sur les priorités, conçue pour séparer la logique basée sur les scripts du noyau du jeu. Il permet aux développeurs de se "brancher" (hook) sur des événements de jeu spécifiques (tels que le combat, la connexion d'un personnage, la création d'objets, etc.) et d'exécuter leur propre code sans modification directe du code source du moteur principal.
Concepts de base
Le système repose sur quatre piliers principaux :
- EventType : Une énumération (Enum) représentant un événement de jeu spécifique (par exemple,
CREATURE_KILL,PLAYER_LOGIN,NPC_TALK). - BaseEvent : Le transporteur de données. Chaque type d'événement possède sa propre implémentation
BaseEventcorrespondante (par exemple, l'événementCreatureKill), contenant toutes les informations pertinentes sur cet événement particulier. - ListenersContainer : Un conteneur dédié qui contient les auditeurs pour différents événements. De nombreux objets dans L2J (Joueurs, PNJ, Châteaux, Clans) implémentent ou possèdent un
ListenersContainer. - EventDispatcher : Le moteur qui gère le processus de notification, s'assurant que les écouteurs sont appelés dans le bon ordre de priorité.
Fonctionnement : Le cycle d'exécution
Lorsqu'une action se produit dans le monde du jeu (par exemple, un joueur attaque un monstre) :
- Instanciation de l'événement : Le moteur principal crée un nouvel objet d'événement
AttackableAttacket le remplit avec les données de l'attaquant, de la cible, des dégâts et de la compétence utilisée. - Dispatching (Distribution) : Le noyau appelle
EventDispatcher.getInstance().notifyEvent(event, targetContainer). - Récupération des listeners : Le répartiteur récupère tous les auditeurs enregistrés pour ce
EventTypeà partir de :- Conteneur local : La cible spécifique impliquée (le monstre attaqué).
- Conteneurs mondiaux : Conteneurs centralisés comme
Containers.Global()ouContainers.Players().
- Tri par priorité : Les écouteurs sont triés par valeur de
priority(les valeurs les plus élevées sont exécutées en premier). - Exécution : Le répartiteur parcourt les écouteurs et exécute leur code un par un.
- Retour/Arrêt : Les écouteurs peuvent renvoyer un
TerminateReturnou utiliser le flagabort()pour empêcher le répartiteur de chercher d'autres écouteurs ou notifier le noyau de l'annulation de l'événement.
Enregistrement d'un Listener
Les écouteurs peuvent être enregistrés de trois manières, allant de la plus courante (annotations) au contrôle manuel.
1. Par Annotations (Recommandé pour les scripts)
Dans les classes héritant de AbstractScript (telles que les scripts d'IA des PNJ ou les quêtes), vous pouvez utiliser des annotations pour enregistrer des méthodes. C'est la manière la plus propre et la plus utilisée.
@RegisterEvent(EventType.CREATURE_KILL)
@RegisterType(ListenerRegisterType.NPC)
@Id(20432) // ID de l'Elpy
@Priority(100)
public void onElpyKill(CreatureKill event) {
LOG.info("Un Elpy a été tué par {}", event.attacker().getName());
}
Annotations de filtrage disponibles :
@Id(int...): Filtrer par identifiants spécifiques de PNJ, d'objets ou de châteaux.@Ids({@Id(1), @Id(2)}): Autoriser plusieurs identifiants.@Range(from=X, to=Y): Filtrer par une plage d'identifiants.@NpcLevelRange(from=X, to=Y): Filtrer par une plage de niveaux de PNJ.@Priority(int): Contrôler l'ordre d'exécution (par défaut 0).
2. Enregistrement fonctionnel (Lambda)
Pratique pour un enregistrement rapide à l'intérieur d'un constructeur.
public MyScript() {
registerConsumer((PlayerLogin event) -> {
event.player().sendMessage("Bienvenue de retour !");
}, EventType.PLAYER_LOGIN, ListenerRegisterType.GLOBAL_PLAYERS);
}
3. Enregistrement manuel
Utilisé rarement, mais puissant pour l'attachement dynamique d'auditeurs à des instances spécifiques.
L2PcInstance player = ...;
player.addListener(new ConsumerEventListener(player, EventType.ATTACK, (Attack event) -> {
// Votre logique unique spécifique à ce joueur
}, this));
Types d'enregistrement (Portée)
Le ListenerRegisterType définit où l'écouteur sera attaché :
| Type | Description |
|---|---|
GLOBAL | Écoute les événements se produisant n'importe où dans le monde. |
NPC | Attaché au template d'un PNJ. Se déclenche pour tous les PNJ ayant cet ID. |
ITEM | Attaché au template d'un objet. |
PLAYER | Attaché à une instance spécifique de joueur. |
GLOBAL_NPCS | Écoute globalement les événements pour tous les PNJ. |
GLOBAL_MONSTERS | Écoute globalement les événements pour tous les monstres. |
GLOBAL_PLAYERS | Écoute globalement les événements pour tous les joueurs. |
CASTLE / FORT | Attaché à une résidence spécifique. |
ZONE | Attaché à une zone spécifique. |
Contrôle de l'exécution : TerminateReturn
Le système permet aux écouteurs de renvoyer des valeurs au noyau. Vous pouvez utiliser TerminateReturn pour annuler complètement une action ou arrêter la chaîne d'écouteurs.
Exemple : Empêcher la mort
En définissant une priorité élevée, vous pouvez intercepter l'événement avant qu'il ne se produise.
@RegisterEvent(EventType.CREATURE_KILL)
@RegisterType(ListenerRegisterType.GLOBAL_PLAYERS)
@Priority(Integer.MAX_VALUE) // S'assurer qu'il s'exécute en "premier"
public TerminateReturn onPlayerDeath(CreatureKill event) {
if (event.target().isGM()) {
event.target().setCurrentHp(event.target().getMaxHp());
return new TerminateReturn(true, true, true); // Annuler la mort !
}
return null;
}
Les trois booléens dans TerminateReturn(boolean abort, boolean override, boolean value) signifient :
- Abort : Arrêter de notifier les écouteurs suivants.
- Override : Remplacer toutes les valeurs de retour précédentes.
- Value : Le résultat booléen réel renvoyé au noyau (par exemple, "cette action a-t-elle réussi ?").
Exemple avancé : Suivi du respawn
Dans L2J, le "respawn" (renaissance) n'est pas un événement PLAYER_RESPAWN unique, car il s'agit d'un processus complexe impliquant la mort, le choix du point et la téléportation. Cependant, en exploitant la puissance du système de listeners, nous pouvons suivre ce cycle de vie efficacement.
Le script RespawnListener.java suivant présente une approche robuste. Au lieu d'écouteurs dynamiques complexes, nous utilisons un état partagé (Set) pour suivre les joueurs morts et les traiter lors de leur prochaine téléportation (ce qui indique la renaissance via le clic sur le bouton "To Town", etc.).
public class RespawnListener extends AbstractNpcAI {
private static final int ELPY_ID = 20432;
private final Set<Integer> _pendingRespawn = ConcurrentHashMap.newKeySet();
@RegisterEvent(CREATURE_KILL)
@RegisterType(GLOBAL_PLAYERS)
public void onPlayerDeath(CreatureKill event) {
final var target = event.target();
if (target.isPlayer()) {
_pendingRespawn.add(target.getObjectId());
}
}
@RegisterEvent(CREATURE_TELEPORTED)
@RegisterType(GLOBAL_PLAYERS)
public void onPlayerTeleport(CreatureTeleported event) {
final var creature = event.creature();
if (creature.isPlayer() && _pendingRespawn.remove(creature.getObjectId())) {
// Faire apparaître le PNJ aux nouvelles coordonnées du joueur.
// Paramètres : npcId, x, y, z, heading, randomOffset, despawnTime
addSpawn(ELPY_ID, creature.getX(), creature.getY(), creature.getZ(), creature.getHeading(), false, 60000);
}
}
@RegisterEvent(PLAYER_LOGOUT)
@RegisterType(GLOBAL_PLAYERS)
public void onPlayerLogout(PlayerLogout event) {
_pendingRespawn.remove(event.player().getObjectId());
}
}
Ce modèle est extrêmement efficace car il utilise des écouteurs persistants et maintient la gestion de l'état de manière auto-nettoyante. Il est idéal pour implémenter des "buffs après le respawn", une "protection au spawn" ou des déclencheurs de quêtes complexes après la mort.
Bonnes pratiques et conseils
- Utilisez les annotations : Elles sont plus lisibles et gérées automatiquement par
AbstractScript(par exemple, désenregistrement automatique lors du rechargement du script). - Choisissez vos priorités avec soin :
Integer.MAX_VALUE: Pour les services qui doivent s'exécuter en "premier" (comme le login ou la sécurité).- Valeurs positives : Pour la logique qui doit se produire avant la logique de jeu normale.
- Valeurs négatives : Pour la logique qui ne doit se produire que si rien d'autre n'a annulé l'événement.
- Vérifiez les valeurs nulles : Assurez-vous toujours que
event.attacker()ouevent.target()ne sont pas nuls, car la mort environnementale ou des compétences complexes peuvent causer des états d'événement inattendus. - Évitez la logique lourde : La logique à l'intérieur des écouteurs doit être rapide, car de nombreux événements (comme
ATTACK) se produisent très fréquemment. Si vous avez besoin de calculs lourds ou d'opérations sur la base de données, utilisez leThreadPoolManager.