Theseus 模组文档
API 参考 / 事件系统

EventBus


想先跑起来?

这一页是签名与行为参考。订阅、优先级、退订与异常隔离。直接做一遍:教程 · 订阅游玩事件。

EventBus 是全进程唯一的事件总线。模组在 onInit 里经 ModContext#eventBus() 取得,返回的实例带模组归属(宿主内部是 EventBusImpl#forMod(modId) 的包装视图)。游玩事件与跨场景事件都走这一条总线,模组用事件类型区分,不需要选择「该用哪条总线」。

方法速览

方法用途
subscribe(Class<E>, EventPriority, Consumer<E>)按事件类型 + 优先级订阅,返回退订令牌
subscribe(Class<E>, Consumer<E>)同上,优先级固定为 NORMAL
post(E)同步分发事件并返回同一实例

subscribe(Class type, EventPriority priority, Consumer listener)

java
<E extends Event> Subscription subscribe(Class<E> type, EventPriority priority, Consumer<E> listener);

行为:登记一条订阅并立即返回 Subscription。匹配规则是 type.isInstance(事件),因此订阅父类会收到子类(订阅 TickEvent 会同时收到 TickEvent.Pre 与 TickEvent.Post;订阅 Event 会收到全部事件)。type 或 listener 为 null 时抛 IllegalArgumentException;priority 为 null 时按 EventPriority.NORMAL 处理。同一个监听器实例可以重复注册,每次注册各产生一条独立订阅,各自用返回的令牌退订。subscribe 可在任意线程调用(内部加锁重建有序订阅表)。

示例:

java
Subscription pauseSubscription = ctx.eventBus().subscribe(
        PauseEvent.class, EventPriority.HIGHEST, event -> {
            if (shouldBlockPause()) {
                event.setResult(EventResult.DENY);
            }
        });

subscribe(Class type, Consumer listener)

java
<E extends Event> Subscription subscribe(Class<E> type, Consumer<E> listener);

行为:接口上的 default 重载,等价于 subscribe(type, EventPriority.NORMAL, listener)。适合纯观察型监听器(只看不改)。

示例:

java
Subscription sub = ctx.eventBus().subscribe(SettingsChangedEvent.class, event -> {
    if (event.changedKeys().contains("bgmVolume")) {
        // 刷新本模组缓存的音量派生值
    }
});

post(E event)

java
<E extends Event> E post(E event);

行为:在调用线程内当场完成全部分发,并返回传入的同一实例(便于读回 result())。null 直接返回 null;当前订阅数为 0 时立即返回(不遍历、不分配)。分发顺序为:优先级降序(HIGHEST 最先),同一优先级内按注册顺序(稳定可复现)。若事件可取消且当前结果非 DEFAULT,在通知下一个监听器之前就停止(短路);不可取消事件永远通知全部监听器。每个监听器独立 try/catch(Throwable),异常只记一条限频日志(同事件类型 5 秒内只记一次),不影响其它监听器,也不会冒泡到宿主循环。投递过程中调用 unsubscribe() 会立即生效(条目带 alive 标志,后续条目不再被调用)。

模组也可以 post:适合投递自己定义的 Event 子类,用于模组内或模组间通信。不要伪造宿主事实事件(例如自己 post 一个 GameStartEvent),那会误导其它订阅者。

示例:

java
final class MyModTick extends Event {
    MyModTick() {
        super(false);
    }
}

MyModTick event = ctx.eventBus().post(new MyModTick());
if (event.isDecided()) {
    // 本次分发中已有监听器改写过结果(对本事件无实际意义,仅演示读回)
}

分发契约

  1. 同步投递:post 在调用线程内完成全部分发并返回,没有异步队列,也没有延迟投递。
  2. 匹配规则:订阅类型.isInstance(事件);订阅父类可捕获子类。
  3. 顺序:优先级降序,同一优先级按注册顺序。
  4. 短路:仅当事件可取消、且结果不是 DEFAULT 时停止通知后续监听器;不可取消事件永远通知全部监听器。
  5. 隔离:每个监听器独立 try/catch(Throwable),限频记录后继续。
  6. 零订阅零开销:无订阅时 post 立即返回(不分配、不遍历)。
  7. 线程:subscribe / unsubscribe 任意线程可调用(内部加锁);post 在调用线程同步分发,宿主在游戏线程 / 渲染线程投递,因此监听器回调也在该线程执行。监听器不得在回调里做阻塞 IO(网络、磁盘、future.get() 等)。

订阅与退订的要点:

示例

java
import com.seecmd.mod.api.Mod;
import com.seecmd.mod.api.ModContext;
import com.seecmd.mod.api.ModInfo;
import com.seecmd.mod.api.event.EventBus;
import com.seecmd.mod.api.event.EventPriority;
import com.seecmd.mod.api.event.EventResult;
import com.seecmd.mod.api.event.SettingsChangedEvent;
import com.seecmd.mod.api.event.Subscription;
import com.seecmd.mod.api.event.TickEvent;
import com.seecmd.mod.api.event.gameplay.PauseEvent;

import java.util.List;

public final class ExampleMod implements Mod {

    private double elapsedSeconds;
    private Subscription pauseSubscription;

    @Override
    public ModInfo info() {
        return ModInfo.of("example_mod", "示例模组", "1.0.0",
                List.of("author"), "演示事件订阅与退订");
    }

    @Override
    public void onInit(ModContext ctx) {
        EventBus bus = ctx.eventBus();

        // 每帧心跳:纯观察,放最低优先级,只看不改
        bus.subscribe(TickEvent.Pre.class, EventPriority.LOWEST, event -> {
            // 保持极轻:只做状态累加,不分配大对象、不做阻塞 IO
            elapsedSeconds += event.deltaSeconds();
        });

        // 可取消事件:用最高优先级抢先决定是否跳过本次暂停
        pauseSubscription = bus.subscribe(PauseEvent.class, EventPriority.HIGHEST, event -> {
            if (shouldBlockPause()) {
                event.setResult(EventResult.DENY);
            }
        });

        // 临时订阅:拿到需要的信息后立即退订
        Subscription once = bus.subscribe(SettingsChangedEvent.class, event -> {
            if (event.changedKeys().contains("bgmVolume")) {
                // 刷新本模组缓存的音量派生值
            }
        });
        once.unsubscribe();
    }

    @Override
    public void onUnload() {
        // 幂等:重复调用无副作用;显式释放长生命周期订阅
        pauseSubscription.unsubscribe();
    }

    private boolean shouldBlockPause() {
        return false;
    }
}

完整签名

java
public interface EventBus {
    <E extends Event> Subscription subscribe(Class<E> type, EventPriority priority, java.util.function.Consumer<E> listener);
    <E extends Event> Subscription subscribe(Class<E> type, java.util.function.Consumer<E> listener);
    <E extends Event> E post(E event);
}

线程:订阅表读写加锁,post 无锁读快照并在调用线程同步分发。

稳定性:2.0 起冻结,2.x 内只增不破。