Przejdź do treści

System Listenerów (Słuchaczy)

System Eventów/Listenerów to architektura sterowana zdarzeniami oparta na priorytetach, zaprojektowana w celu oddzielenia logiki opartej na skryptach od jądra gry. Pozwala to programistom „podpiąć się” (hook) pod konkretne zdarzenia w grze (takie jak walka, logowanie postaci, tworzenie przedmiotów itp.) i uruchomić własny kod bez bezpośredniej modyfikacji kodu źródłowego głównego silnika.

Podstawowe pojęcia

System opiera się na czterech głównych filarach:

  1. EventType: Typ wyliczeniowy (Enum) reprezentujący konkretne zdarzenie w grze (np. CREATURE_KILL, PLAYER_LOGIN, NPC_TALK).
  2. BaseEvent: Nośnik danych. Każdy typ zdarzenia ma odpowiadającą mu implementację BaseEvent (np. zdarzenie CreatureKill), która zawiera wszystkie istotne informacje o tym konkretnym wydarzeniu.
  3. ListenersContainer: Dedykowany kontener przechowujący słuchaczy dla różnych zdarzeń. Wiele obiektów w L2J (Gracze, NPC, Zamki, Klany) implementuje lub posiada ListenersContainer.
  4. EventDispatcher: Silnik zarządzający procesem powiadamiania, zapewniający wywoływanie słuchaczy we właściwej kolejności priorytetów.

Jak to działa: Cykl wykonania

Gdy w świecie gry następuje akcja (np. gracz atakuje potwora):

  1. Instancjonowanie zdarzenia: Główny silnik tworzy nowy obiekt zdarzenia AttackableAttack i wypełnia go danymi o atakującym, celu, obrażeniach i użytej umiejętności.
  2. Dyspaczowanie (Dispatching): Rdzeń wywołuje EventDispatcher.getInstance().notifyEvent(event, targetContainer).
  3. Pobieranie listenerów: Dispatcher pobiera wszystkich słuchaczy zarejestrowanych dla tego EventType z:
    • Lokalnego kontenera: Konkretny cel, który jest zaangażowany (np. atakowany potwór).
    • Globalnych kontenerów: Zcentralizowane kontenery, takie jak Containers.Global() lub Containers.Players().
  4. Sortowanie według priorytetu: Słuchacze są sortowani według wartości priority (wyższe wartości są wykonywane jako pierwsze).
  5. Wykonanie: Dispatcher iteruje po słuchaczach i wykonuje ich kod jeden po drugim.
  6. Powrót/Zatrzymanie: Słuchacze mogą zwrócić TerminateReturn lub użyć flagi abort(), aby powstrzymać dyspaczera przed szukaniem kolejnych słuchaczy lub powiadomić rdzeń o anulowaniu zdarzenia.

Rejestracja listenera

Słuchaczy można rejestrować na trzy sposoby, od najczęstszego (adnotacje) po ręczną kontrolę.

1. Przez adnotacje (Zalecane dla skryptów)

W klasach dziedziczących po AbstractScript (takich jak skrypty AI NPC lub questy), możesz użyć adnotacji do rejestracji metod. Jest to najczystszy i najczęściej używany sposób.

@RegisterEvent(EventType.CREATURE_KILL)
@RegisterType(ListenerRegisterType.NPC)
@Id(20432) // Elpy
@Priority(100)
public void onElpyKill(CreatureKill event) {
LOG.info("Elpy został zabity przez {}", event.attacker().getName());
}

Dostępne adnotacje filtrujące:

  • @Id(int...): Filtruj według konkretnych identyfikatorów NPC, przedmiotów lub zamków.
  • @Ids({@Id(1), @Id(2)}): Pozwól na wiele identyfikatorów.
  • @Range(from=X, to=Y): Filtruj według zakresu identyfikatorów.
  • @NpcLevelRange(from=X, to=Y): Filtruj według zakresu poziomów NPC.
  • @Priority(int): Kontroluj kolejność wykonywania (domyślnie 0).

2. Rejestracja funkcyjna (Lambda)

Przydatna do szybkiej rejestracji wewnątrz konstruktora.

public MyScript() {
registerConsumer((PlayerLogin event) -> {
event.player().sendMessage("Witaj ponownie!");
}, EventType.PLAYER_LOGIN, ListenerRegisterType.GLOBAL_PLAYERS);
}

3. Rejestracja ręczna

Używana rzadko, ale potężna do dynamicznego podpinania słuchaczy do konkretnych instancji.

L2PcInstance player = ...;
player.addListener(new ConsumerEventListener(player, EventType.ATTACK, (Attack event) -> {
// Twoja unikalna logika specyficzna dla tego gracza
}, this));

Typy rejestracji (Zasięg)

ListenerRegisterType definiuje gdzie słuchacz zostanie podpięty:

TypOpis
GLOBALSłucha zdarzeń dziejących się gdziekolwiek na świecie.
NPCPodpięty do szablonu NPC. Wyzwalany dla wszystkich NPC o danym ID.
ITEMPodpięty do szablonu przedmiotu.
PLAYERPodpięty do konkretnej instancji gracza.
GLOBAL_NPCSGlobalnie słucha zdarzeń dla wszystkich NPC.
GLOBAL_MONSTERSGlobalnie słucha zdarzeń dla wszystkich potworów.
GLOBAL_PLAYERSGlobalnie słucha zdarzeń dla wszystkich graczy.
CASTLE / FORTPodpięty do konkretnej rezydencji.
ZONEPodpięty do konkretnej strefy.

Kontrola wykonania: TerminateReturn

System pozwala słuchaczom zwracać wartości do rdzenia. Możesz użyć TerminateReturn, aby całkowicie anulować akcję lub zatrzymać łańcuch słuchaczy.

Przykład: Zapobieganie śmierci

Ustawiając wysoki priorytet, możesz przechwycić zdarzenie zanim do niego dojdzie.

@RegisterEvent(EventType.CREATURE_KILL)
@RegisterType(ListenerRegisterType.GLOBAL_PLAYERS)
@Priority(Integer.MAX_VALUE) // Upewnij się, że uruchomi się jako "pierwszy"
public TerminateReturn onPlayerDeath(CreatureKill event) {
if (event.target().isGM()) {
event.target().setCurrentHp(event.target().getMaxHp());
return new TerminateReturn(true, true, true); // Anuluj śmierć!
}
return null;
}

Trzy wartości logiczne w TerminateReturn(boolean abort, boolean override, boolean value) oznaczają:

  1. Abort: Przestań powiadamiać kolejnych słuchaczy.
  2. Override: Nadpisz wszystkie poprzednie wartości zwracane.
  3. Value: Rzeczywisty wynik logiczny zwracany do rdzenia (np. „czy ta akcja się powiodła?”).

Zaawansowany przykład: Śledzenie respawnu

W L2J „respawn” (odrodzenie) nie jest pojedynczym zdarzeniem PLAYER_RESPAWN, ponieważ jest to złożony proces obejmujący śmierć, wybór punktu i teleportację. Jednak korzystając z mocy systemu listenerów, możemy efektywnie śledzić ten cykl życia.

Poniższy skrypt RespawnListener.java demonstruje solidne podejście. Zamiast skomplikowanych dynamicznych słuchaczy, używamy wspólnego stanu (Set), aby śledzić martwych graczy i procesować ich przy następnej teleportacji (co wskazuje na odrodzenie poprzez kliknięcie przycisku „Do miasta” itp.).

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())) {
// Zespawnuj NPC na nowych współrzędnych gracza.
// Parametry: 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());
}
}

Ten wzorzec jest wysoce wydajny, ponieważ używa trwałych słuchaczy i utrzymuje zarządzanie stanem w sposób samoczyszczący. Jest idealny do implementacji „buffów po odrodzeniu”, „ochrony po spawnie” lub złożonych wyzwalaczy zadań po śmierci.


Najlepsze praktyki i wskazówki

  1. Używaj adnotacji: Są czytelniejsze i automatycznie zarządzane przez AbstractScript (np. automatyczne wyrejestrowanie przy przeładowaniu skryptu).
  2. Mądrze dobieraj priorytety:
    • Integer.MAX_VALUE: Dla usług, które muszą działać jako „pierwsze” (jak logowanie lub bezpieczeństwo).
    • Wartości dodatnie: Dla logiki, która ma nastąpić przed standardową logiką gry.
    • Wartości ujemne: Dla logiki, która powinna nastąpić tylko wtedy, gdy nic innego nie anulowało zdarzenia.
  3. Sprawdzaj nulle (Null-checks): Zawsze upewnij się, że event.attacker() lub event.target() nie są nullami, ponieważ śmierć środowiskowa lub złożone umiejętności mogą powodować nieoczekiwane stany zdarzeń.
  4. Unikaj ciężkiej logiki: Logika wewnątrz słuchaczy powinna być szybka, ponieważ wiele zdarzeń (np. ATTACK) dzieje się bardzo często. Jeśli potrzebujesz ciężkich obliczeń lub operacji na bazie danych, użyj ThreadPoolManager.