Μετάβαση στο κύριο περιεχόμενο

Σύστημα Ακροατών

Το σύστημα Συμβάντων/Ακροατών είναι μια ιεραρχημένη αρχιτεκτονική βασισμένη σε συμβάντα, σχεδιασμένη για να αποσυνδέει τον πυρήνα του παιχνιδιού από τη λογική που βασίζεται σε σενάρια (scripts). Επιτρέπει στους προγραμματιστές να "αγκιστρώνονται" σε συγκεκριμένα συμβάντα του παιχνιδιού (όπως μάχη, σύνδεση χαρακτήρα ή δημιουργία αντικειμένου) και να εκτελούν προσαρμοσμένο κώδικα χωρίς να τροποποιούν απευθείας τον πηγαίο κώδικα της κύριας μηχανής.

Βασικές Έννοιες

Το σύστημα βασίζεται σε τέσσερις κύριους πυλώνες:

  1. EventType: Μια απαρίθμηση (σταθερά) που αντιπροσωπεύει ένα συγκεκριμένο περιστατικό στο παιχνίδι (π.χ. CREATURE_KILL, PLAYER_LOGIN, NPC_TALK).
  2. BaseEvent: Ο φορέας δεδομένων. Κάθε τύπος συμβάντος έχει μια αντίστοιχη υλοποίηση του BaseEvent (π.χ. το συμβάν CreatureKill) που περιέχει όλες τις σχετικές πληροφορίες για το συγκεκριμένο περιστατικό.
  3. ListenersContainer: Ένα εξειδικευμένο δοχείο αποθήκευσης που περιέχει ακροατές για διάφορα συμβάντα. Πολλά αντικείμενα στο L2J (Παίκτες, NPC, Κάστρα, Φατρίες) υλοποιούν ή κατέχουν ένα ListenersContainer.
  4. EventDispatcher: Η μηχανή που διαχειρίζεται τη διαδικασία ειδοποίησης, διασφαλίζοντας ότι οι ακροατές καλούνται με τη σωστή σειρά προτεραιότητας.

Πώς Λειτουργεί: Ο Κύκλος Ζωής Εκτέλεσης

Όταν συμβαίνει μια ενέργεια στον κόσμο (για παράδειγμα, ένας Παίκτης επιτίθεται σε ένα Τέρας):

  1. Δημιουργία Συμβάντος: Η κύρια μηχανή δημιουργεί ένα νέο αντικείμενο συμβάντος AttackableAttack, συμπληρώνοντάς το με τον επιτιθέμενο, τον στόχο, τη ζημιά και την ικανότητα που χρησιμοποιήθηκε.
  2. Αποστολή: Ο πυρήνας καλεί το EventDispatcher.getInstance().notifyEvent(event, targetContainer).
  3. Ανάκτηση Ακροατών: Ο αποστολέας ανακτά όλους τους εγγεγραμμένους ακροατές για αυτό το EventType από:
    • Τοπικό Δοχείο: Τον συγκεκριμένο στόχο που εμπλέκεται (π.χ. το Τέρας που δέχτηκε την επίθεση).
    • Παγκόσμια Δοχεία: Κεντρικά δοχεία όπως το Containers.Global() ή το Containers.Players().
  4. Ταξινόμηση Προτεραιότητας: Οι ακροατές ταξινομούνται βάσει της τιμής priority (οι υψηλότερες τιμές εκτελούνται πρώτες).
  5. Εκτέλεση: Ο αποστολέας διατρέχει τους ακροατές και τους εκτελεί διαδοχικά.
  6. Επιστροφή/Διακοπή: Εάν ένας ακροατής επιστρέψει ένα TerminateReturn ή χρησιμοποιήσει τη σημαία abort(), ο αποστολέας μπορεί να σταματήσει να αναζητά περαιτέρω ακροατές ή να δώσει σήμα στον πυρήνα να ακυρώσει το συμβάν.

Εγγραφή Ακροατών

Υπάρχουν τρεις τρόποι εγγραφής ενός ακροατή, από τον πιο κοινό (σχολιασμοί - annotations) έως τον χειροκίνητο έλεγχο.

1. Βασισμένη σε Σχολιασμούς (Συνιστάται για Scripts)

Σε οποιαδήποτε κλάση επεκτείνει το AbstractScript (όπως scripts NPC AI ή Quests), μπορείτε να χρησιμοποιήσετε σχολιασμούς για την εγγραφή μεθόδων. Αυτή είναι η πιο καθαρή και συνηθισμένη μέθοδος.

@RegisterEvent(EventType.CREATURE_KILL)
@RegisterType(ListenerRegisterType.NPC)
@Id(20432) // Elpy
@Priority(100)
public void onElpyKill(CreatureKill event) {
LOG.info("Ένα Elpy σκοτώθηκε από τον {}", event.attacker().getName());
}

Διαθέσιμοι Σχολιασμοί Φιλτραρίσματος:

  • @Id(int...): Φιλτράρισμα βάσει συγκεκριμένου ID NPC, Αντικειμένου ή Κάστρου.
  • @Ids({@Id(1), @Id(2)}): Επιτρέπει πολλαπλά ID.
  • @Range(from=X, to=Y): Φιλτράρισμα βάσει εύρους ID.
  • @NpcLevelRange(from=X, to=Y): Φιλτράρισμα βάσει επιπέδου του NPC.
  • @Priority(int): Ελέγχει τη σειρά εκτέλεσης (η προεπιλογή είναι 0).

2. Λειτουργική Εγγραφή (Lambda)

Χρήσιμη για γρήγορη εγγραφή κατά τη διάρκεια ενός κατασκευαστή (constructor).

public MyScript() {
registerConsumer((PlayerLogin event) -> {
event.player().sendMessage("Καλώς ορίσατε και πάλι!");
}, EventType.PLAYER_LOGIN, ListenerRegisterType.GLOBAL_PLAYERS);
}

3. Χειροκίνητη Εγγραφή

Σπάνια χρησιμοποιείται, αλλά είναι ισχυρή για τη δυναμική προσκόλληση ακροατών σε συγκεκριμένα στιγμιότυπα.

L2PcInstance player = ...;
player.addListener(new ConsumerEventListener(player, EventType.ATTACK, (Attack event) -> {
// Προσαρμοσμένη λογική για αυτόν τον συγκεκριμένο παίκτη
}, this));

Τύποι Εγγραφής (Εμβέλεια)

Το ListenerRegisterType ορίζει πού προσκολλάται ο ακροατής:

ΤύποςΠεριγραφή
GLOBALΑκούει το συμβάν παντού στον κόσμο.
NPCΠροσκολλάται σε Πρότυπα NPC. Ενεργοποιείται για όλα τα NPC αυτού του ID.
ITEMΠροσκολλάται σε Πρότυπα Αντικειμένων.
PLAYERΠροσκολλάται σε ένα συγκεκριμένο στιγμιότυπο παίκτη.
GLOBAL_NPCSΑκούει συμβάντα για ΟΛΑ τα NPC παγκοσμίως.
GLOBAL_MONSTERSΑκούει συμβάντα για ΟΛΑ τα τέρατα παγκοσμίως.
GLOBAL_PLAYERSΑκούει συμβάντα για ΟΛΟΥΣ τους παίκτες παγκοσμίως.
CASTLE / FORTΠροσκολλάται σε μια συγκεκριμένη κατοικία.
ZONEΠροσκολλάται σε μια συγκεκριμένη ζώνη.

Έλεγχος Εκτέλεσης: TerminateReturn

Το σύστημα επιτρέπει στους ακροατές να επιστρέφουν τιμές στον πυρήνα. Ένα TerminateReturn μπορεί να χρησιμοποιηθεί για να ακυρώσει μια ενέργεια εξ ολοκλήρου ή να διακόψει την αλυσίδα των ακροατών.

Παράδειγμα: Πρόληψη Θανάτου

Ορίζοντας μια υψηλή προτεραιότητα, μπορείτε να αναχαιτίσετε ένα συμβάν πριν αυτό συμβεί.

@RegisterEvent(EventType.CREATURE_KILL)
@RegisterType(ListenerRegisterType.GLOBAL_PLAYERS)
@Priority(Integer.MAX_VALUE) // Διασφάλιση ότι αυτό εκτελείται ΠΡΩΤΟ
public TerminateReturn onPlayerDeath(CreatureKill event) {
if (event.target().isGM()) {
event.target().setCurrentHp(event.target().getMaxHp());
return new TerminateReturn(true, true, true); // Ακύρωση του θανάτου!
}
return null;
}

Οι τρεις τιμές boolean στο TerminateReturn(boolean abort, boolean override, boolean value) αντιπροσωπεύουν:

  1. Abort: Διακοπή ειδοποίησης άλλων ακροατών.
  2. Override: Αντικατάσταση οποιασδήποτε προηγούμενης τιμής επιστροφής.
  3. Value: Το πραγματικό αποτέλεσμα boolean που θα επιστραφεί στον πυρήνα (π.χ. "Ήταν επιτυχής αυτή η ενέργεια;").

Προχωρημένο Παράδειγμα: Παρακολούθηση Respawn

Στο L2J δεν υπάρχει ένα μεμονωμένο γεγονός PLAYER_RESPAWN, καθώς η αναγέννηση είναι μια σύνθετη διαδικασία που περιλαμβάνει πολλαπλά βήματα (θάνατος, επιλογή σημείου και τηλεμεταφορά). Ωστόσο, μπορείτε να χρησιμοποιήσετε τη δύναμη του Συστήματος Ακροατών για να παρακολουθήσετε αυτόν τον κύκλο ζωής αποτελεσματικά.

Το παρακάτω σενάριο, RespawnListener.java, παρουσιάζει μια στιβαρή προσέγγιση. Αντί για πολύπλοκους δυναμικούς ακροατές, χρησιμοποιεί μια κοινή κατάσταση (Set) για την παρακολούθηση των νεκρών παικτών και τους επεξεργάζεται όταν τηλεμεταφερθούν την επόμενη φορά (κάτι που σηματοδοτεί μια αναγέννηση κάνοντας κλικ στο "Στο Χωριό").

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 στη νέα τοποθεσία του παίκτη.
// Παράμετροι: 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());
}
}

Αυτό το μοτίβο είναι εξαιρετικά αποδοτικό επειδή χρησιμοποιεί μόνιμους ακροατές και διατηρεί τη διαχείριση κατάστασης αυτοκαθαριζόμενη. Είναι ιδανικό για την υλοποίηση συστημάτων όπως "Buffs Αναγέννησης", "Προστασία Εμφάνισης" ή σύνθετα triggers αποστολών (quests) μετά τον θάνατο.


Βέλτιστες Πρακτικές και Συμβουλές

  1. Χρησιμοποιήστε Σχολιασμούς: Είναι πιο εύκολοι στην ανάγνωση και διαχειρίζονται αυτόματα από το AbstractScript (π.χ. διαγράφονται αυτόματα όταν ένα σενάριο επαναφορτώνεται).
  2. Ορίστε Προτεραιότητες με Σύνεση:
    • Integer.MAX_VALUE: Για υπηρεσίες που "πρέπει να εκτελούνται πρώτες", όπως καταγραφή ή ασφάλεια.
    • Θετικές τιμές: Για λογική που πρέπει να συμβεί πριν από τους τυπικούς μηχανισμούς του παιχνιδιού.
    • Αρνητικές τιμές: Για λογική που πρέπει να συμβεί μόνο εάν τίποτα άλλο δεν ακύρωσε το συμβάν.
  3. Ελέγξτε για Null: Πάντα να επαληθεύετε ότι το event.attacker() ή το event.target() δεν είναι null, καθώς οι θάνατοι από το περιβάλλον ή οι σύνθετες ικανότητες μπορούν να παράγουν απρόσμενες καταστάσεις συμβάντων.
  4. Αποφύγετε τη Βαριά Λογική: Δεδομένου ότι πολλά συμβάντα (όπως η ATTACK) πυροδοτούνται συχνά, διατηρήστε τη λογική μέσα στον ακροατή γρήγορη. Χρησιμοποιήστε το ThreadPoolManager εάν χρειάζεται να εκτελέσετε βαριούς υπολογισμούς ή λειτουργίες βάσης δεδομένων.