Direkt zum Inhalt

Listener-System

Das Event/Listener-System ist eine prioritätsbasierte, ereignisgesteuerte Architektur, die darauf ausgelegt ist, den Spielkern von der skriptbasierten Logik zu entkoppeln. Es ermöglicht Entwicklern, sich in bestimmte Spielereignisse (wie Kampf, Charakter-Login oder Item-Erstellung) einzuklinken ("Hooking") und eigenen Code auszuführen, ohne den Quellcode der Haupt-Engine direkt ändern zu müssen.

Kernkonzepte

Das System basiert auf vier Hauptpfeilern:

  1. EventType: Ein Enum (Konstante), das ein bestimmtes Ereignis im Spiel darstellt (z. B. CREATURE_KILL, PLAYER_LOGIN, NPC_TALK).
  2. BaseEvent: Der Datenträger. Jeder Ereignistyp hat eine entsprechende BaseEvent-Implementierung (z. B. das CreatureKill-Ereignis), das alle relevanten Informationen über das spezifische Vorkommnis enthält.
  3. ListenersContainer: Ein spezialisierter Speichercontainer, der Listener für verschiedene Ereignisse hält. Viele Objekte in L2J (Spieler, NPCs, Burgen, Clans) implementieren oder besitzen einen ListenersContainer.
  4. EventDispatcher: Die Engine, die den Benachrichtigungsprozess verwaltet und sicherstellt, dass Listener in der richtigen Prioritätsreihenfolge aufgerufen werden.

Funktionsweise: Der Ausführungszyklus

Wenn eine Aktion in der Welt stattfindet (z. B. ein Spieler greift ein Monster an):

  1. Ereignis-Instanziierung: Die Core-Engine erstellt ein neues AttackableAttack-Ereignisobjekt und füllt es mit Daten über Angreifer, Ziel, Schaden und die verwendete Fertigkeit.
  2. Dispatching: Der Kern ruft EventDispatcher.getInstance().notifyEvent(event, targetContainer) auf.
  3. Listener-Abruf: der Dispatcher ruft alle registrierten Listener für diesen EventType ab von:
    • Lokaler Container: Das spezifisch beteiligte Ziel (z. B. das angegriffene Monster).
    • Globale Container: Zentralisierte Container wie Containers.Global() oder Containers.Players().
  4. Prioritätssortierung: Listener werden nach ihrem priority-Wert sortiert (höhere Werte werden zuerst ausgeführt).
  5. Ausführung: Der Dispatcher iteriert durch die Listener und führt sie nacheinander aus.
  6. Return/Terminierung: Wenn ein Listener ein TerminateReturn zurückgibt oder das abort()-Flag verwendet, kann der Dispatcher die Suche nach weiteren Listenern stoppen oder dem Kern signalisieren, das Ereignis abzubrechen.

Registrierung von Listenern

Es gibt drei Möglichkeiten, einen Listener zu registrieren, von der gebräuchlichsten (Annotationen) bis zur manuellen Steuerung.

1. Annotationsbasiert (Empfohlen für Skripte)

In jeder Klasse, die AbstractScript erweitert (wie NPC AI Skripte oder Quests), können Sie Annotationen zur Registrierung von Methoden verwenden. Dies ist die sauberste und am häufigsten verwendete Methode.

@RegisterEvent(EventType.CREATURE_KILL)
@RegisterType(ListenerRegisterType.NPC)
@Id(20432) // Elpy
@Priority(100)
public void onElpyKill(CreatureKill event) {
LOG.info("Ein Elpy wurde von {} getötet", event.attacker().getName());
}

Verfügbare Filter-Annotationen:

  • @Id(int...): Filtern nach spezifischer NPC-, Item- oder Burgen-ID.
  • @Ids({@Id(1), @Id(2)}): Erlaubt mehrere IDs.
  • @Range(from=X, to=Y): Filtern nach ID-Bereich.
  • @NpcLevelRange(from=X, to=Y): Filtern nach NPC-Level-Bereich.
  • @Priority(int): Steuert die Ausführungsreihenfolge (Standard ist 0).

2. Funktionale (Lambda) Registrierung

Nützlich für die schnelle Registrierung innerhalb eines Konstruktors.

public MyScript() {
registerConsumer((PlayerLogin event) -> {
event.player().sendMessage("Willkommen zurück!");
}, EventType.PLAYER_LOGIN, ListenerRegisterType.GLOBAL_PLAYERS);
}

3. Manuelle Registrierung

Wird selten verwendet, ist aber mächtig, um Listener dynamisch an spezifische Instanzen anzuhängen.

L2PcInstance player = ...;
player.addListener(new ConsumerEventListener(player, EventType.ATTACK, (Attack event) -> {
// Eigene Logik für diesen spezifischen Spieler
}, this));

Registrierungstypen (Scope)

ListenerRegisterType definiert, wo der Listener angehängt wird:

TypBeschreibung
GLOBALHört auf das Ereignis überall in der Welt.
NPCWird an NPC-Templates angehängt. Löst für alle NPCs dieser ID aus.
ITEMWird an Item-Templates angehängt.
PLAYERWird an eine spezifische Spielerinstanz angehängt.
GLOBAL_NPCSHört global auf Ereignisse für ALLE NPCs.
GLOBAL_MONSTERSHört global auf Ereignisse für ALLE Monster.
GLOBAL_PLAYERSHört global auf Ereignisse für ALLE Spieler.
CASTLE / FORTWird an eine spezifische Residenz angehängt.
ZONEWird an eine spezifische Zone angehängt.

Ausführungskontrolle: TerminateReturn

Das System ermöglicht es Listenern, Werte an den Kern zurückzugeben. Ein TerminateReturn kann verwendet werden, um eine Aktion vollständig abzubrechen oder die Listener-Kette zu unterbrechen.

Beispiel: Tod verhindern

Durch Setzen einer hohen Priorität können Sie ein Ereignis abfangen, bevor es eintritt.

@RegisterEvent(EventType.CREATURE_KILL)
@RegisterType(ListenerRegisterType.GLOBAL_PLAYERS)
@Priority(Integer.MAX_VALUE) // Stellt sicher, dass dies ZUERST ausgeführt wird
public TerminateReturn onPlayerDeath(CreatureKill event) {
if (event.target().isGM()) {
event.target().setCurrentHp(event.target().getMaxHp());
return new TerminateReturn(true, true, true); // Tod abbrechen!
}
return null;
}

Die drei booleschen Werte in TerminateReturn(boolean abort, boolean override, boolean value) stehen für:

  1. Abort: Benachrichtigung weiterer Listener stoppen.
  2. Override: Alle vorherigen Rückgabewerte ersetzen.
  3. Value: Das tatsächliche boolesche Ergebnis, das an den Kern zurückgegeben wird (z. B. „War diese Aktion erfolgreich?“).

Fortgeschrittenes Beispiel: Respawn-Tracking

In L2J gibt es kein einzelnes PLAYER_RESPAWN-Ereignis, da das Wiederbeleben ein komplexer Prozess ist, der mehrere Schritte umfasst (Tod, Punktauswahl und Teleportation). Sie können jedoch die Leistung des Listener-Systems nutzen, um diesen Lebenszyklus effizient zu verfolgen.

Das folgende Skript RespawnListener.java zeigt einen robusten Ansatz. Anstatt komplexer dynamischer Listener verwendet es einen gemeinsamen Status (Set), um tote Spieler zu verfolgen, und verarbeitet sie bei ihrem nächsten Teleport (der ein Wiederbeleben durch Klicken auf „To Village“ anzeigt).

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())) {
// NPC am neuen Standort des Spielers spawnen.
// Parameter: 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());
}
}

Dieses Muster ist sehr effizient, da es permanente Listener verwendet und das Statusmanagement selbstreinigend hält. Es ist ideal für die Implementierung von Systemen wie „Wiederbelebungs-Buffs“, „Spawn-Schutz“ oder komplexen Quest-Triggern nach dem Tod.


Best Practices und Tipps

  1. Verwenden Sie Annotationen: Sie sind einfacher zu lesen und werden automatisch von AbstractScript verwaltet (z. B. werden sie beim Neuladen eines Skripts automatisch deregistriert).
  2. Prioritäten mit Bedacht setzen:
    • Integer.MAX_VALUE: Für Dienste, die „zuerst ausgeführt werden müssen“, wie Logging oder Sicherheit.
    • Positive Werte: Für Logik, die vor den Standard-Spielmechaniken stattfinden soll.
    • Negative Werte: Für Logik, die nur eintreten soll, wenn nichts anderes das Ereignis abgebrochen hat.
  3. Null-Prüfung: Überprüfen Sie immer, ob event.attacker() oder event.target() nicht null sind, da Umgebungs-Tode oder komplexe Fertigkeiten zu unerwarteten Ereigniszuständen führen können.
  4. Schwere Logik vermeiden: Da viele Ereignisse (wie ATTACK) häufig ausgelöst werden, halten Sie die Logik innerhalb des Listeners schnell. Verwenden Sie den ThreadPoolManager, wenn Sie schwere Berechnungen oder Datenbankoperationen durchführen müssen.