跳转到正文

监听器系统

L2J 监听器系统是一种基于优先级的事件驱动架构,旨在将游戏核心与基于脚本的逻辑解耦。它允许开发人员“挂钩”到特定的游戏事件(如战斗、角色登录或物品创建),并执行自定义代码,而无需直接修改核心引擎的源代码。

核心概念

该系统建立在四个主要支柱之上:

  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() 标志,分发器可以停止寻找进一步的监听器,或通知核心取消该事件。

注册监听器

有三种注册监听器的方法,从最常用的(注解)到手动控制。

1. 基于注解(脚本推荐)

在任何扩展 AbstractScript 的类(如 AI 脚本或任务)中,您可以使用注解来注册方法。这是最简洁且最常用的方法。

@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...):按特定的 NPC、物品或城堡 ID 过滤。
  • @Ids({@Id(1), @Id(2)}):允许指定多个 ID。
  • @Range(from=X, to=Y):按 ID 范围过滤。
  • @NpcLevelRange(from=X, to=Y):按 NPC 等级范围过滤。
  • @Priority(int):控制执行顺序(默认值为 0)。

2. 函数式(Lambda)注册

适用于在构造函数中快速注册。

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 模板。为该 ID 的所有 NPC 触发。
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;
}

TerminateReturn(boolean abort, boolean override, boolean value) 中的三个布尔值代表:

  1. Abort:停止通知其他监听器。
  2. Override:替换任何之前的返回值。
  3. Value:要返回给核心的实际布尔结果(例如:“此操作是否成功?”)。

高级示例:追踪重生

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())) {
// 玩家到达新位置时生成一个 Elpy
// 参数: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());
}
}

这种模式非常高效,因为它使用了永久性监听器并保持状态管理的自清理特性。它非常适合实现“复活 Buff”、“出生点保护”或死亡后的复杂任务触发器。


最佳实践与提示

  1. 使用注解:它们更易于阅读,并由 AbstractScript 自动管理(例如,当脚本重新加载时,它们会自动注销)。
  2. 设定合理的优先级
    • Integer.MAX_VALUE:用于“必须最先运行”的服务,如日志记录或安全检查。
    • 正值:用于需要在标准游戏机制之前发生的逻辑。
    • 负值:用于仅在没有其他逻辑取消事件时才发生的逻辑。
  3. 检查 Null 值:始终验证 event.attacker()event.target() 不为空,因为环境死亡或复杂的技能可能会产生意外的事件状态。
  4. 避免沉重的逻辑:由于许多事件(如 ATTACK)触发频率极高,请保持监听器内的逻辑快速。如果需要执行繁重的计算或数据库操作,请使用 ThreadPoolManager