Sistema de Escuchadores
El sistema de Eventos/Escuchadores es una arquitectura basada en eventos y priorizada diseñada para desacoplar el núcleo del juego de la lógica basada en scripts. Permite a los desarrolladores "engancharse" a eventos específicos del juego (como el combate, el inicio de sesión de los personajes o la creación de objetos) y ejecutar código personalizado sin modificar directamente el código fuente del motor principal.
Conceptos Clave
El sistema se basa en cuatro pilares principales:
- EventType: Una enumeración (constante) que representa un suceso específico en el juego (p. ej.,
CREATURE_KILL,PLAYER_LOGIN,NPC_TALK). - BaseEvent: El portador de datos. Cada tipo de evento tiene una implementación correspondiente de
BaseEvent(p. ej., el eventoCreatureKill) que contiene toda la información relevante sobre ese suceso específico. - ListenersContainer: Un contenedor de almacenamiento especializado que guarda los escuchadores para varios eventos. Muchos objetos en L2J (Jugadores, NPCs, Castlos, Clanes) implementan o poseen un
ListenersContainer. - EventDispatcher: El motor que gestiona el proceso de notificación, asegurando que los escuchadores sean llamados en el orden correcto de prioridad.
Cómo funciona: El ciclo de vida de ejecución
Cuando ocurre una acción en el mundo (por ejemplo, un Jugador ataca a un Monstruo):
- Instanciación del Evento: El motor principal crea un nuevo objeto de evento
AttackableAttack, poblándolo con el atacante, el objetivo, el daño y la habilidad utilizada. - Despacho: El núcleo llama a
EventDispatcher.getInstance().notifyEvent(event, targetContainer). - Recuperación de Escuchadores: El despachador obtiene todos los escuchadores registrados para ese
EventTypede:- Contenedor Local: El objetivo específico involucrado (p. ej., el Monstruo golpeado).
- Contenedores Globales: Contenedores centralizados como
Containers.Global()oContainers.Players().
- Clasificación por Prioridad: Los escuchadores se ordenan por su valor de
prioridad(los valores más altos se ejecutan primero). - Ejecución: El despachador recorre los escuchadores y los ejecuta secuencialmente.
- Retorno/Abortar: Si un escuchador devuelve un
TerminateReturno utiliza el indicadorabort(), el despachador puede dejar de buscar más escuchadores o indicar al núcleo que cancele el evento.
Registro de Escuchadores
Hay tres formas de registrar un escuchador, desde la más común (anotaciones) hasta el control manual.
1. Basado en anotaciones (Recomendado para Scripts)
En cualquier clase que extienda AbstractScript (como scripts de IA o Quests), puedes usar anotaciones para registrar métodos. Este es el método más limpio y común.
@RegisterEvent(EventType.CREATURE_KILL)
@RegisterType(ListenerRegisterType.NPC)
@Id(20432) // Elpy
@Priority(100)
public void onElpyKill(CreatureKill event) {
LOG.info("Un Elpy fue eliminado por {}", event.attacker().getName());
}
Anotaciones de filtrado disponibles:
@Id(int...): Filtra por un ID específico de NPC, Objeto o Castillo.@Ids({@Id(1), @Id(2)}): Permite múltiples IDs.@Range(from=X, to=Y): Filtra por un rango de IDs.@NpcLevelRange(from=X, to=Y): Filtra por el nivel del NPC.@Priority(int): Controla el orden de ejecución (el valor predeterminado es 0).
2. Registro funcional (Lambda)
Útil para un registro rápido durante un constructor.
public MyScript() {
registerConsumer((PlayerLogin event) -> {
event.player().sendMessage("¡Bienvenido de nuevo!");
}, EventType.PLAYER_LOGIN, ListenerRegisterType.GLOBAL_PLAYERS);
}
3. Registro manual
Rara vez se utiliza, pero es potente para adjuntar escuchadores a instancias específicas de forma dinámica.
L2PcInstance player = ...;
player.addListener(new ConsumerEventListener(player, EventType.ATTACK, (Attack event) -> {
// Lógica personalizada para este jugador específico
}, this));
Tipos de Registro (Alcance)
El ListenerRegisterType define dónde se adjunta el escuchador:
| Tipo | Descripción |
|---|---|
GLOBAL | Escucha el evento en todo el mundo. |
NPC | Adjunto a las Plantillas de NPC. Se activa para todos los NPCs de ese ID. |
ITEM | Adjunto a las Plantillas de Objetos. |
PLAYER | Adjunto a una instancia de jugador específica. |
GLOBAL_NPCS | Escucha eventos para TODOS los NPCs globalmente. |
GLOBAL_MONSTERS | Escucha eventos para TODOS los monstruos globalmente. |
GLOBAL_PLAYERS | Escucha eventos para TODOS los jugadores globalmente. |
CASTLE / FORT | Adjunto a una residencia específica. |
ZONE | Adjunto a una zona específica. |
Control de la ejecución: TerminateReturn
El sistema permite que los escuchadores devuelvan valores al núcleo. Se puede utilizar un TerminateReturn para cancelar una acción por completo o interrumpir la cadena de escuchadores.
Ejemplo: Prevenir la muerte
Al establecer una prioridad alta, puedes interceptar un evento antes de que ocurra.
@RegisterEvent(EventType.CREATURE_KILL)
@RegisterType(ListenerRegisterType.GLOBAL_PLAYERS)
@Priority(Integer.MAX_VALUE) // Asegura que esto se ejecute PRIMERO
public TerminateReturn onPlayerDeath(CreatureKill event) {
if (event.target().isGM()) {
event.target().setCurrentHp(event.target().getMaxHp());
return new TerminateReturn(true, true, true); // ¡Abortar la muerte!
}
return null;
}
Los tres booleanos en TerminateReturn(boolean abort, boolean override, boolean value) representan:
- Abort: Detener la notificación a otros escuchadores.
- Override: Reemplazar cualquier valor de retorno anterior.
- Value: El resultado booleano real que se devolverá al núcleo (p. ej., "¿Tuvo éxito esta acción?").
Ejemplo avanzado: Rastreo de Respawn
L2J no tiene un evento único PLAYER_RESPAWN porque el renacer es un proceso complejo que involucra varios pasos (morir, elegir un punto y teletransportarse). Sin embargo, puedes usar el poder del Sistema de Escuchadores para rastrear este ciclo de vida de manera eficiente.
El siguiente script, RespawnListener.java, demuestra un enfoque robusto. En lugar de complejos escuchadores dinámicos, utiliza un estado compartido (Set) para rastrear a los jugadores muertos y los procesa cuando se teletransportan por próxima vez (lo que significa un renacimiento al hacer clic en "Al Pueblo").
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())) {
// Generar el NPC en la nueva ubicación del jugador.
// Parámetros: 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());
}
}
Este patrón es altamente eficiente porque utiliza escuchadores permanentes y mantiene la gestión de estado con limpieza automática. Es perfecto para implementar sistemas como "Mejoras de Resurrección", "Protección de Aparición" o activadores de misiones complejos después de la muerte.
Mejores prácticas y consejos
- Usa Anotaciones: Son más fáciles de leer y son gestionadas automáticamente por
AbstractScript(p. ej., se desregistran automáticamente cuando se recarga un script). - Establece Prioridades con Cuidado:
Integer.MAX_VALUE: Para servicios que "deben ejecutarse primero", como registro o seguridad.- Valores positivos: Para la lógica que debe ocurrir antes de las mecánicas estándar del juego.
- Valores negativos: Para la lógica que solo debe ocurrir si nada más canceló el evento.
- Verifica nulos: Siempre verifica que
event.attacker()oevent.target()no sean nulos, ya que las muertes ambientales o las habilidades complejas pueden producir estados de evento inesperados. - Evita la lógica pesada: Dado que muchos eventos (como
ATTACK) se disparan con frecuencia, mantén la lógica dentro del escuchador rápida. UsaThreadPoolManagersi necesitas realizar cálculos pesados o operaciones de base de datos.