事件原因

事件系统适合为游戏中的每个动作插入附加的逻辑,但它们天生缺少提供”触发这个事件的起因“的能力。通过使用 Cause 对象,我们可以获取到一些上下文信息,这些上下文信息有助于决定你的事件监听器改变的行为。

例如,一个世界保护插件需要了解是什么玩家导致了 ChangeBlockEvent 从而可以决定这一事件到底是否需要取消,相应的信息将直接由 Cause 提供,而不是像传统的事件处理系统一样,通过创建大量的不同子事件以提供信息。

每个事件都提供一个 Cause 对象用于描述触发该事件的相关信息。你可以直接通过调用 Event#getCause() 方法获取到一个 Cause 对象。

事件原因和上下文信息

每一个 Cause 对象都包含两组不同的信息:事件因果链,和一个 EventContext 对象,也就是事件上下文。

  • 事件因果链存储的是一组事件的直接起因,它们按重要程度依次排列。事件因果链中的对象没有具体名字。
  • 事件上下文中则包含有事件的更多信息。上下文中的对象均与特定的键一一对应,但没有具体顺序,因为它们都一样重要。

举个例子。如果玩家的一只羊吃了草,那么(这个方块更新事件的)直接起因就是这只羊。而玩家则会以 EventContextKeys#OWNER 的形式出现在 EventContext 里供事件消费者使用,因为它确实和事件有某种关系,但玩家并非这个事件的直接起因。

另一个你可能会关注的例子:模拟玩家。你模拟的玩家不见得就是某个动作的直接起因,但它仍然会以 EventContextKeys#PLAYER_SIMULATED 的形式出现在 EventContext 中。

从直接起因中取得对象

从它结构上来说,一个 Cause 对象包含了一有序列表,其内有若干对象。这里将介绍几种从这个对象中获取具体信息的方法。参阅 Javadocs 以获得完整列表。

注解

在事件原因中的对象会按照这样的顺序排序:第一个对象是最直接的原因,第二个对象是次直接的原因,依此类推。靠后的对象可能仅仅用于提供上下文信息。

Cause#root() 返回事件的直接起因,即距离事件发生最近的或者最直接的原因。由于 Cause 不能为空,所以 Sponge 保证 root 是存在的。

Cause#first(Class) 返回第一个满足给定类型或者其子类的事件原因。比如我们假设有一个 Cause 包含了一个玩家,随后包含了一个实体,即 [Player, Entity, ...]

@Listener
public void onEvent(ExampleCauseEvent event) {
    Cause cause = event.getCause(); // [Player, Entity]
    Optional<Player> firstPlayer = cause.first(Player.class); // 1
    Optional<Entity> firstEntity = cause.first(Entity.class); // 2
}

这两个 Optional 都将返回玩家对象,因为不管是玩家还是实体都满足玩家对象作为其子类。

Cause#last(Class) 方法和 Cause#first(Class) 类似,只不过它返回最后一个满足对应类型的对象。

我们继续上面的例子。如果我们转而去使用 Cause#last(Class) ,那么第一个 Optional 将仍然返回玩家对象,不过第二个返回的对象就不同了:它将返回上面我们提到的,处于第二个位置的那个实体。

Cause#containsType(Class) 返回一个布尔值表示其是否包含这样一个满足给定类型的对象。

Cause#all() 直接返回所有的对象以支持更高级的操作。

事件上下文

有的时候对象的顺序并不足以让我们搞清楚对象在整个事件中的地位。因此我们这里需要事件上下文,也就是 EventContext 这一概念。在事件上下文中,不同的对象会基于 EventContextKeys 使用不同的独特名称标记自身,从而方便开发者获取。一些示例可以在 ChangeBlockEvent.Grow 事件中的 Notifier ,以及 DamageEntityEventSource 等处找到。

事件因果链不对其中的对象类型作出任何保证,但事件上下文不同,一个和特定 EventContextKey 对应的对象保证了其拥有特定的类型。

从事件上下文中检索指定的对象

@Listener
public void onGrow(ChangeBlockEvent.Grow event) {
    Optional<User> notifier = event.getCause().getContext().get(EventContextKeys.NOTIFIER);
}

这一示例使用了 EventContext#get(EventContextKey) 以试图从事件上下文中检索与特定键对应的对象,如果对应的对象存在的话。此外,通过使用 EventContext#asMap() 方法,我们可以得到一个 Map<EventContextKey<?>, Object>,从而可以获取所有可用的 EventContextKey 以及与之对应的所有对象。

注解

一些特定名称的 EventContextKey 会以 EventContextKeys 类的静态字段的方式提供。

自定义事件原因

创建一个事件原因很容易,但是这会根据你是在服务端主线程上还是以异步的方式创建事件原因而有所不同。

注解

因为 Cause 对象是不可变的,所以一旦创建则不可修改。

使用事件因果链管理器

警告

事件因果链管理器,即 CauseStackManager 只能在服务端主线程正常工作。因此如果你在其他线程调用它,它只会抛出一个 IllegalStateException 异常。在调用 CauseStackManager 上的方法时,请先保证你的代码目前正在主线程上执行。

如果你在主线程上创建你的事件,那么请通过 Sponge#getCauseStackManager() 方法获取并使用 CauseStackManager。事件上下文管理器会在游戏运行时跟踪一些潜在的事件原因,从而可以方便地获取当前的 Cause 而不需要额外的工作。请通过 CauseStackManager#getCurrentCause() 方法获取当前跟踪的事件原因。你会注意到你的插件的 PluginContainer 总是在返回的事件原因中,因为事件因果链管理器本身也会跟踪你的插件本身。通过 CauseStackManager 创建事件原因可以免去很多不必要的代码,因此你得以集中你的注意力在你本身想要添加的事件原因上。

在添加你自己的事件原因之前,你需要首先把事件因果链冻结。添加一个事件因果链关键帧将冻结当前的事件因果链状态,当你接下来处理并创建完你的事件原因后,把该事件因果链关键帧移除将使原先的事件因果链回到原有的冻结时的状态。

小技巧

添加一个事件因果链关键帧不会移除事件因果链管理器已有的东西,因此,所有在添加事件因果链关键帧之前已有的事件因果链以及事件上下文等,都会接着出现在事件因果链管理器中。你可以通过在添加关键帧前和添加关键帧后分别调用 Sponge.getCauseStackManager().getCurrentCause() 方法以验证这一点。

例如一个事件因果链如果包含有一个 PluginContainer 和一个 CommandSource,并在此之后添加了一个事件因果链关键帧,那么这两个对象将接着保持在因果链中,并在获取 Cause 成为整个事件原因的一部分。

比如说你想要在一个类似于 sudo 的命令中模拟一个玩家触发事件,那么你可能需要把你想要模拟的玩家加入到事件原因中,同时你还会把玩家对应的 GameProfile 添加到事件上下文中(被模拟玩家并不直接对被触发的事件负责)。

使用事件因果链管理器创建自定义事件原因

在这个例子中,在为变量赋值后,事件原因会包含作为根原因的 playerToSimulate、作为第二个原因的 sourceRunningSudo、以及除 CauseStackManager 之外,以 EventContextKeys#PLAYER_SIMULATED 的键出现的 GameProfile 上下文。 你的事件相关的代码应出现在这些方法调用之后。

CommandSource sourceRunningSudo = ...;
Player playerToSimulate = ...;
try (CauseStackManager.StackFrame frame = Sponge.getCauseStackManager().pushCauseFrame()) {

  frame.pushCause(sourceRunningSudo);
  frame.pushCause(playerToSimulate);

  frame.addContext(EventContextKeys.PLAYER_SIMULATED, playerToSimulate.getProfile());

  Cause cause = frame.getCurrentCause();
}

注意,你传入的最后一个事件原因将成为根原因,因为栈是“后进先出”(LIFO)的容器。

小技巧

关于栈数据结构的信息及元素顺序的问题可参阅 Stack 的 Javadocs 或是这篇维基百科上的文章中文版)。

使用事件原因生成器

如果你在新建一个不在主线程上触发的事件,你不能使用 CauseStackManager,而是手动创建 Cause 对象。

新建事件原因对象只需要用 Cause.Builder 即可。你可以通过 Cause.builder() 的调用来获得这个 Builder。使用 Cause.Builder#append(Object) 方法来追加原因,但需要注意的是,使用这个 Builder 时,第一个对象将会是根原因,不像是 CauseStackManager 中的最后一个。

如果你想添加上下文,你需要使用专门的 Buildr,即 EventContext.Builder,可通过调用 EventContext#builder() 获取。构造完 Cause 对象后,你便可以通过 Cause.Builder#build(EventContext) 添加你的 EventContext

还是用刚才的例子来说明,下面是我们如果使用原因构建器来构建这样一个对象的方法:

使用事件原因生成器和事件上下文生成器创建自定义事件原因

注意在这个示例中,在为变量赋值后,第一个传入的事件原因将会成为根原因。

CommandSource sourceRunningSudo = ...;
Player playerToSimulate = ...;
PluginContainer plugin = ...;

EventContext context = EventContext.builder()
  .add(EventContextKeys.PLAYER_SIMULATED, playerToSimulate.getProfile())
  .add(EventContextKeys.PLUGIN, plugin)
  .build();

Cause cause = Cause.builder()
  .append(playerToSimulate)
  .append(sourceRunningSudo)
  .append(plugin)
  .build(context);

在决定把什么信息放进事件起因时要三思而后行。如果你的插件想要触发一个经常被以其他方式触发的事件的话,那么你可能需要在事件原因中囊括一下你的插件主类(PluginContainer),以保证其他的插件可以得知事件的来源是你的插件。此外,如果你想要代表一个玩家触发一个事件,那么往往你还需要把玩家置入插件原因中。