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.