Pular para o conteúdo principal

Sistema de Ouvintes

O sistema de Eventos/Ouvintes é uma arquitetura baseada em eventos e priorizada concebida para dissociar o núcleo do jogo da lógica baseada em scripts. Permite aos programadores "ligarem-se" a eventos específicos do jogo (como combate, início de sessão de personagens ou criação de itens) e executar código personalizado sem modificar diretamente o código-fonte do motor principal.

Conceitos Principais​

O sistema baseia-se em quatro pilares principais:

  1. EventType: Uma enumeração (constante) que representa uma ocorrência específica no jogo (ex: CREATURE_KILL, PLAYER_LOGIN, NPC_TALK).
  2. BaseEvent: O transportador de dados. Cada tipo de evento tem uma implementação correspondente de BaseEvent (ex: evento CreatureKill) que contém toda a informação relevante sobre essa ocorrência específica.
  3. ListenersContainer: Um contentor de armazenamento especializado que guarda ouvintes para vários eventos. Muitos objetos no L2J (Jogadores, NPCs, Castelos, Clãs) implementam ou possuem um ListenersContainer.
  4. EventDispatcher: O motor que gere o processo de notificação, assegurando que os ouvintes são chamados na ordem correta de prioridade.

Como funciona: O ciclo de vida da execução​

Quando ocorre uma ação no mundo (por exemplo, um Jogador ataca um Monstro):

  1. Instanciação do Evento: O motor principal cria um novo objeto de evento AttackableAttack, preenchendo-o com o atacante, o alvo, o dano e a habilidade utilizada.
  2. Despacho: O núcleo chama EventDispatcher.getInstance().notifyEvent(event, targetContainer).
  3. Recuperação de Ouvintes: O despachante obtém todos os ouvintes registados para esse EventType de:
    • Contentor Local: O alvo específico envolvido (ex: o Monstro atingido).
    • Contentores Globais: Contentores centralizados como Containers.Global() ou Containers.Players().
  4. Ordenação por Prioridade: Os ouvintes são ordenados pelo seu valor de prioridade (valores mais elevados executam primeiro).
  5. Execução: O despachante percorre os ouvintes e executa-os sequencialmente.
  6. Retorno/Abortar: Se um ouvinte devolver um TerminateReturn ou utilizar a flag abort(), o despachante pode parar de procurar por mais ouvintes ou sinalizar o núcleo para cancelar o evento.

Registar Ouvintes​

Existem três formas de registar um ouvinte, desde a mais comum (anotações) até ao controlo manual.

1. Baseado em Anotações (Recomendado para Scripts)​

Em qualquer classe que estenda AbstractScript (como scripts de IA ou Quests), pode usar anotações para registar métodos. Este é o método mais limpo e comum.

@RegisterEvent(EventType.CREATURE_KILL)
@RegisterType(ListenerRegisterType.NPC)
@Id(20432) // Elpy
@Priority(100)
public void onElpyKill(CreatureKill event) {
LOG.info("Um Elpy foi morto por {}", event.attacker().getName());
}

Anotações de Filtragem Disponíveis:​

  • @Id(int...): Filtra por um ID de NPC, Item ou Castelo específico.
  • @Ids({@Id(1), @Id(2)}): Permite múltiplos IDs.
  • @Range(from=X, to=Y): Filtra por um intervalo de IDs.
  • @NpcLevelRange(from=X, to=Y): Filtra pelo nível do NPC.
  • @Priority(int): Controla a ordem de execução (o padrão é 0).

2. Registo Funcional (Lambda)​

Útil para um registo rápido durante um construtor.

public MyScript() {
registerConsumer((PlayerLogin event) -> {
event.player().sendMessage("Bem-vindo de volta!");
}, EventType.PLAYER_LOGIN, ListenerRegisterType.GLOBAL_PLAYERS);
}

3. Registo Manual​

Raramente utilizado, mas potente para anexar ouvintes a instâncias específicas de forma dinâmica.

L2PcInstance player = ...;
player.addListener(new ConsumerEventListener(player, EventType.ATTACK, (Attack event) -> {
// Lógica personalizada para este jogador específico
}, this));

Tipos de Registo (Escopo)​

O ListenerRegisterType define onde o ouvinte é anexado:

TipoDescrição
GLOBALOuve o evento em todo o mundo.
NPCAnexado a Modelos de NPC. Ativado para todos os NPCs desse ID.
ITEMAnexado a Modelos de Itens.
PLAYERAnexado a uma instância de jogador específica.
GLOBAL_NPCSOuve eventos para TODOS os NPCs globalmente.
GLOBAL_MONSTERSOuve eventos para TODOS os monstros globalmente.
GLOBAL_PLAYERSOuve eventos para TODOS os jogadores globalmente.
CASTLE / FORTAnexado a uma residência específica.
ZONEAnexado a uma zona específica.

Controlar a Execução: TerminateReturn​

O sistema permite que os ouvintes devolvam valores ao núcleo. Um TerminateReturn pode ser utilizado para cancelar uma ação inteiramente ou interromper a cadeia de ouvintes.

Exemplo: Prevenir a Morte​

Ao definir uma prioridade elevada, pode intercetar um evento antes de este acontecer.

@RegisterEvent(EventType.CREATURE_KILL)
@RegisterType(ListenerRegisterType.GLOBAL_PLAYERS)
@Priority(Integer.MAX_VALUE) // Garante que este corre PRIMEIRO
public TerminateReturn onPlayerDeath(CreatureKill event) {
if (event.target().isGM()) {
event.target().setCurrentHp(event.target().getMaxHp());
return new TerminateReturn(true, true, true); // Aborta a morte!
}
return null;
}

Os três booleanos em TerminateReturn(boolean abort, boolean override, boolean value) representam:

  1. Abort: Parar de notificar outros ouvintes.
  2. Override: Substituir qualquer valor de retorno anterior.
  3. Value: O resultado booleano real a devolver ao núcleo (ex: "Esta ação foi bem-sucedida?").

Exemplo Avançado: Rastreamento de Respawn​

O L2J não possui um único evento PLAYER_RESPAWN porque o renascimento é um processo complexo que envolve várias etapas (morrer, escolher um ponto e teletransportar-se). No entanto, você pode usar o poder do Sistema de Ouvintes para rastrear esse ciclo de vida de forma eficiente.

O script a seguir, RespawnListener.java, demonstra uma abordagem robusta. Em vez de ouvintes dinâmicos complexos, ele usa um estado compartilhado (Set) para rastrear jogadores mortos e os processa quando eles se teletransportam em seguida (o que significa um renascimento ao clicar em "Para a Vila").

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())) {
// Criar o NPC na nova localização do jogador.
// 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 padrão é altamente eficiente porque utiliza ouvintes permanentes e mantém o gerenciamento de estado com limpeza automática. É perfeito para implementar sistemas como "Buffs de Ressurreição", "Proteção de Spawn" ou gatilhos de missões complexos após a morte.


Melhores Práticas e Dicas​

  1. Use Anotações: São mais fáceis de ler e geridas automaticamente pelo AbstractScript (ex: desregistam-se automaticamente quando um script é recarregado).
  2. Defina Prioridades com Cuidado:
    • Integer.MAX_VALUE: Para serviços que "têm de correr primeiro", como registo ou segurança.
    • Valores positivos: Para lógica que tem de acontecer antes das mecânicas padrão do jogo.
    • Valores negativos: Para lógica que só deve acontecer se mais nada cancelou o evento.
  3. Verifique nulos: Verifique sempre se event.attacker() ou event.target() não são nulos, pois mortes ambientais ou habilidades complexas podem produzir estados de evento inesperados.
  4. Evite lógica pesada: Uma vez que muitos eventos (como ATTACK) disparam frequentemente, mantenha a lógica dentro do ouvinte rápida. Use ThreadPoolManager se precisar de realizar cálculos pesados ou operações de base de dados.