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:
- EventType: Typ wyliczeniowy (Enum) reprezentujący konkretne zdarzenie w grze (np.
CREATURE_KILL,PLAYER_LOGIN,NPC_TALK). - BaseEvent: Nośnik danych. Każdy typ zdarzenia ma odpowiadającą mu implementację
BaseEvent(np. zdarzenieCreatureKill), która zawiera wszystkie istotne informacje o tym konkretnym wydarzeniu. - ListenersContainer: Dedykowany kontener przechowujący słuchaczy dla różnych zdarzeń. Wiele obiektów w L2J (Gracze, NPC, Zamki, Klany) implementuje lub posiada
ListenersContainer. - 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):
- Instancjonowanie zdarzenia: Główny silnik tworzy nowy obiekt zdarzenia
AttackableAttacki wypełnia go danymi o atakującym, celu, obrażeniach i użytej umiejętności. - Dyspaczowanie (Dispatching): Rdzeń wywołuje
EventDispatcher.getInstance().notifyEvent(event, targetContainer). - Pobieranie listenerów: Dispatcher pobiera wszystkich słuchaczy zarejestrowanych dla tego
EventTypez:- Lokalnego kontenera: Konkretny cel, który jest zaangażowany (np. atakowany potwór).
- Globalnych kontenerów: Zcentralizowane kontenery, takie jak
Containers.Global()lubContainers.Players().
- Sortowanie według priorytetu: Słuchacze są sortowani według wartości
priority(wyższe wartości są wykonywane jako pierwsze). - Wykonanie: Dispatcher iteruje po słuchaczach i wykonuje ich kod jeden po drugim.
- Powrót/Zatrzymanie: Słuchacze mogą zwrócić
TerminateReturnlub użyć flagiabort(), 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:
| Typ | Opis |
|---|---|
GLOBAL | Słucha zdarzeń dziejących się gdziekolwiek na świecie. |
NPC | Podpięty do szablonu NPC. Wyzwalany dla wszystkich NPC o danym ID. |
ITEM | Podpięty do szablonu przedmiotu. |
PLAYER | Podpięty do konkretnej instancji gracza. |
GLOBAL_NPCS | Globalnie słucha zdarzeń dla wszystkich NPC. |
GLOBAL_MONSTERS | Globalnie słucha zdarzeń dla wszystkich potworów. |
GLOBAL_PLAYERS | Globalnie słucha zdarzeń dla wszystkich graczy. |
CASTLE / FORT | Podpięty do konkretnej rezydencji. |
ZONE | Podpię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ą:
- Abort: Przestań powiadamiać kolejnych słuchaczy.
- Override: Nadpisz wszystkie poprzednie wartości zwracane.
- 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
- Używaj adnotacji: Są czytelniejsze i automatycznie zarządzane przez
AbstractScript(np. automatyczne wyrejestrowanie przy przeładowaniu skryptu). - 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.
- Sprawdzaj nulle (Null-checks): Zawsze upewnij się, że
event.attacker()lubevent.target()nie są nullami, ponieważ śmierć środowiskowa lub złożone umiejętności mogą powodować nieoczekiwane stany zdarzeń. - 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żyjThreadPoolManager.