Aller au contenu principal

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 :

  1. EventType : Une énumération (Enum) représentant un événement de jeu spécifique (par exemple, CREATURE_KILL, PLAYER_LOGIN, NPC_TALK).
  2. BaseEvent : Le transporteur de données. Chaque type d'événement possède sa propre implémentation BaseEvent correspondante (par exemple, l'événement CreatureKill), contenant toutes les informations pertinentes sur cet événement particulier.
  3. 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.
  4. 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) :

  1. Instanciation de l'événement : Le moteur principal crée un nouvel objet d'événement AttackableAttack et le remplit avec les données de l'attaquant, de la cible, des dégâts et de la compétence utilisée.
  2. Dispatching (Distribution) : Le noyau appelle EventDispatcher.getInstance().notifyEvent(event, targetContainer).
  3. 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() ou Containers.Players().
  4. Tri par priorité : Les écouteurs sont triés par valeur de priority (les valeurs les plus élevées sont exécutées en premier).
  5. Exécution : Le répartiteur parcourt les écouteurs et exécute leur code un par un.
  6. Retour/Arrêt : Les écouteurs peuvent renvoyer un TerminateReturn ou utiliser le flag abort() 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 l'écouteur sera attaché :

TypeDescription
GLOBALÉcoute les événements se produisant n'importe où dans le monde.
NPCAttaché au template d'un PNJ. Se déclenche pour tous les PNJ ayant cet ID.
ITEMAttaché au template d'un objet.
PLAYERAttaché à 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 / FORTAttaché à une résidence spécifique.
ZONEAttaché à 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 :

  1. Abort : Arrêter de notifier les écouteurs suivants.
  2. Override : Remplacer toutes les valeurs de retour précédentes.
  3. 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

  1. Utilisez les annotations : Elles sont plus lisibles et gérées automatiquement par AbstractScript (par exemple, désenregistrement automatique lors du rechargement du script).
  2. 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.
  3. Vérifiez les valeurs nulles : Assurez-vous toujours que event.attacker() ou event.target() ne sont pas nuls, car la mort environnementale ou des compétences complexes peuvent causer des états d'événement inattendus.
  4. É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 le ThreadPoolManager.