DOCUMENTATION · 事件总线
事件总线
引擎通过 EventBus 广播事件。本文列出引擎/运行时内置发出的全部事件、payload 结构与来源, 供插件(JS 通过 ctx.events 监听、WASM 通过 host…
权威实现:
src/runtime/EventBus.ts、src/runtime/StoryEngine.ts、src/plugins/PluginManager.ts、src/plugins/WasmPluginLoader.ts、src/runtime/Runtime.ts、src/runtime/WorldLoop.ts。 若本文与源码冲突,以源码为准。
事件元信息(EventMeta)
每次 emit 都会附带元信息(监听器签名 (payload, meta) => void):
interface EventMeta {
name: string // 事件名,例如 "story:nodeEnter"
source: 'engine' | 'ui' | 'plugin' | 'system' // 来源
pluginId?: string // 来源是插件时记录插件 id
}通配监听 *
监听事件名传 WILDCARD(即 "*")可收到所有事件。
ctx.events.on('*', (payload, meta) => {
console.log(meta.source, meta.name, payload)
})监听器隔离
单个监听器抛错不会影响其它监听器;错误会交给 Runtime 注入的 error reporter, 若监听器归属某个插件,还会计入该插件的连续失败次数。
按来源分组的事件清单
1. 剧情引擎事件(source: 'engine')
由 StoryEngine 在主循环推进时广播。
| 事件名 | 触发时机 | payload |
|---|---|---|
story:loaded | 剧情文件解析校验通过、准备运行前 | { storyId, title, nodeCount } |
engine:start | 主循环开始 | { storyId, nodeId } |
story:nodeEnter | 进入某个节点 | { storyId, nodeId, node }(node 为该节点原始定义) |
story:nodeExit | 离开某个节点 | { storyId, nodeId, node, result }(result 为处理器返回下一跳信息,无则 null) |
story:say | 渲染对白(say 节点或插件返回的 message) | { nodeId, speaker, text, from }(from 仅在插件 message 时为 'plugin') |
story:choice | 用户做出选择 | { nodeId, index, text, option } |
story:jump | 发起显式跳转 | { from, to } |
story:branch | if 分支被求值并决定去向 | { nodeId, condition, next } |
story:waitSkipped | 可跳过的 wait 被玩家提前跳过 | nodeId / ms |
story:waitOverrun | { nodeId, requestedMs, actualMs, lateByMs } | wait 实际等了远超要求(典型原因:标签页被后台限流/冻结 ✗)—— 引擎只报事实,怎么处理由剧情决定 |
story:observe | 世界事件触发 observe 节点并跳转 | { event, nodeId, next, payload }(nodeId 为 observe 节点 id,next 为跳转目标) |
story:observeEnter | 主循环途经一个 observe 节点(作为普通节点时的兜底) | { nodeId, event? } |
story:pause | 剧情被暂停 | { nodeId } |
story:end | 剧情结束 | { storyId, lastNodeId } |
engine:stop | 主循环真正退出 | { storyId, nodeId, ended } |
story:error | 节点/条件/效果执行出错 | { storyId?, nodeId?, error, expression? } |
插件/剧情主动广播的自定义事件也会以
source: 'engine'发出(见下文「引擎侧广播自定义事件」)。
2. 插件生命周期事件(source: 'engine')
由 PluginManager 广播。
| 事件名 | 触发时机 | payload |
|---|---|---|
plugin:loaded | 插件加载完成(manifest 解析通过) | { info }(info 为 PluginInfo) |
plugin:status | 插件状态变化 | { id, status, name? } |
plugin:error | 插件加载/调用出错(含熔断前) | { id, error, phase? } 或 { id, error, failures } |
plugin:disposed | 插件被卸载 | { id } |
plugin:crashed | 连续失败达到阈值后熔断禁用、回收能力 | { id, error } |
3. 运行时状态事件(source: 'engine')
| 事件名 | 触发时机 | payload |
|---|---|---|
state:change | StateStore 任何写入触发(由 Runtime 转发) | { snapshot }(snapshot 为完整 StateSnapshot) |
4. 世界循环事件(source: 'engine')
由 WorldLoop 广播,是「世界模拟器」的核心信号:
| 事件名 | 触发时机 | payload |
|---|---|---|
world:start | 世界循环启动(WorldLoop.start()) | { intervalMs } |
world:stop | 世界循环停止 | { tick }(停止时的累计拍号) |
world:pause | 世界循环暂停(WorldLoop.pause();插件可经 ctx.world.pause() 触发) | { tick, reason }(reason 缺省为 null) |
world:resume | 世界循环恢复(WorldLoop.resume()) | { tick } |
world:tick | 世界循环每拍广播 | { tick, elapsedMs }(tick 自启动起从 1 递增,elapsedMs 为累计毫秒) |
engine:quality | { level: 'high'|'medium'|'low', reason: string, policy: QualityPolicy } | 引擎(性能看门狗) |
world:tick 的消费方:
- WASM 插件:在
plugin_init返回 / manifestnarrative.events里声明监听world:tick,每拍推进自己的状态机; - JS 插件:
ctx.world.registerRule(name, handler)注册「每拍计算」的规则(在广播前执行),或ctx.world.subscribe('world:tick', ...); - 剧情 JSON:不直接听
world:tick,而是听插件发布的高层世界事件(见下),由observe节点响应。
5. 世界业务事件(source: 'plugin',示例)
插件用 ctx.world.publish / ctx.events.emit / WASM host_emit 发布的世界事件,事件名与结构由插件自定义。
引擎不关心语义,只负责广播。以下事件仅示意契约,非内置事件:
| 事件名 | 触发时机 | payload |
|---|---|---|
alert:emergency | 某种阈值告警 | { level, targets: string[] } |
agent:conflict | 两名智能体冲突 | { a, b, reason } |
agent:moved | 智能体移动位置 | { agent, from, to } |
task:complete | 任务完成 | { agent, task, reward } |
agent:sick | 智能体状态异常(连续多日不佳) | { agent, days } |
report:periodic | 周期性统计报告 | { tick, summary } |
剧情侧的 observe 节点、JS 插件侧的 ctx.world.subscribe、WASM 插件侧的事件代理可以同时监听同一事件(三路平权)。
6. 插件发出的业务事件(source: 'plugin')
JS 插件用 ctx.events.emit(name, payload)、WASM 插件用 host_emit 广播。事件名/结构由插件自定义,例如 wasm:greeted、story:nodeEnter 的插件侧监听等。
WASM 插件回传的 host_emit 事件由 WasmPluginLoader 转为 source: 'plugin'、pluginId: <manifest.id> 的事件。
引擎侧广播自定义事件
剧情节点的 emit 节点类型,以及剧情文件触发的 plugin 效果,都会以 source: 'engine' 广播到 EventBus。
{ "type": "emit", "name": "my:event", "payload": { "k": "v" } }引擎效果 { "type": "emit", "name": "...", "payload": {...} } 亦是同样语义。
JS 插件若要触发剧情节点跳转、结束等流程控制,应使用 ctx.story.jumpTo / end(需 story:control 权限),而不是依赖事件。
监听示例
// JS 插件内
ctx.events.on('story:nodeEnter', (p, m) => {
console.log(`进入节点 ${p.nodeId}(来源 ${m.source})`)
})
ctx.events.on('plugin:crashed', (p) => {
console.warn(`插件 ${p.id} 已熔断:${p.error}`)
})相关链接
- ABI.md:WASM 插件
host_emit的请求/返回格式。 - PLUGIN_JS.md:JS 插件
ctx.events用法与events:listen/events:emit权限。 - ARCHITECTURE.md:EventBus 定位与错误隔离设计。