跳转到内容

事件系统

参伍中的事件是一条可序列化的记录,写明发生了什么以及原因。事件属于权威证据,快照、边界记录和精确重放都包含它们。插件需要发出事件、安排后续工作,或者要向玩家解释某件事为什么发生时,可以参考本页。

客户端如果需要通知,应从其角色有权看到的事件中生成。事件日志归引擎所有,客户端只有读取权限。

已接受的命令产生事件,受众规则决定哪些角色视图能看到它,下一个边界准入它. 查看图表源码.
查看图表源码
flowchart LR
  Command["已接受的命令"] --> Event["SimEvent<br/>cause = CauseRef::Command"]
  Event --> Audience{"EventAudience<br/>由插件声明"}
  Audience -- "该角色可见" --> View["CanwuViewer<br/>visible_changes_since"]
  Audience -- "私有" --> Hidden["仅受信任读取可见"]
  Event --> Next["下一个边界<br/>准入该事件"]
  Next --> Systems["事件驱动的<br/>系统运行"]

事件记录了自己的原因,工具可以从任何一个事件回溯到产生它的命令或系统。事件的受众决定哪些角色相对视图会显示它。事件记录下来之后,由下一个边界准入,EventDriven 边界系统在那里对它作出反应。

每个 SimEvent 都包含:

字段 含义
id 按递增顺序分配的稳定事件身份
timestamp 事件发生的模拟时间
kind 事件类型和结构化载荷
affected_entities 受事件影响的实体
summary 给工具和调试界面看的简短说明
cause 产生该事件的命令、事件、边界或系统
correlation_id 把同一条处理链上的证据归为一组

EventKind 保存一个稳定的 type 标签和一组扁平的结构化字段,各领域自行定义自己的事件类型。你可以在自己的 crate 里定义强类型载荷,用 EventKind::from_payload 编码,再用 decode_payload 解码。插件事件带有插件名和事件类型,可以通过 plugin_identity 取出。这些标签和字段顺序属于快照格式的一部分,改动它们会使已保存的运行无法加载。

插件用 PluginRegistrar::register_event_audience 为自己发出的每种事件类型声明受众,这项声明保存在插件描述符中。EventAudience 有以下几种:

  • Public:所有角色;
  • Actor(person) 或 Actors(people):列出的人物;
  • KnowledgeHolder(holder):某一个知识持有人,例如一个人物或一个机构;
  • AffectedActors:事件 affected_entities 中列出的人物;
  • Private:不进入任何角色相对视图。没有声明的事件类型默认如此。

CanwuViewer::visible_changes_since 按这些规则过滤,只返回某个角色可以看到的变化。调试工具等受信任的上层应用代码仍然可以读取全部事件。

CauseRef 记录以下四类来源之一:

  • Command:已接受的引擎命令或领域命令;
  • Event:之前已经记录的事件;
  • Boundary:产生该事件的系统所在的边界(BoundaryId);
  • System:内核系统或受控的插件系统。

一个由插件拥有的领域动作可以形成如下证据链:

plugin command
└─ typed domain event
└─ scheduled plugin work
├─ domain-state change
├─ follow-up domain event
└─ actor-scoped report
└─ knowledge publication

链上每一环都保留时间、实体、原因和相关标识。调试界面因此能说明发生了什么,重放也能检查事情为什么按这个顺序发生。

参伍维护两个有序队列:

  • 内部调度工作按 (simulation timestamp, insertion sequence) 排序。
  • 规范化输入(canonical ingress)是上层应用和插件输入的持久化队列,依次按到期时间、输入类别、优先级(从高到低)、签发时间和输入 ID 排序。输入类别的顺序是:命令、通信、确认、信息、决策、调度系统。

排队的插件输入在到期之前可以由其签发者撤回。撤回本身记为一条终态输入记录,被撤回的输入永远不会被准入,详见插件输入撤回。

准入决定哪些排队项属于某个边界。到期时间早于已提交模拟时间的输入会以 LateIngress 被拒绝,所以任何输入都不会插到已经提交的边界之后。边界自己生成的零延迟输入,要等当前准入切点结束,再进入同一模拟时间的下一个边界。

有些插件的写入系统按较粗的节奏运行,所以会先在某个阶段准入输入,之后再应用。canwu-society 和文化结算插件由事件驱动的第 12 阶段输入队列系统接收输入,在下一次每日或每月结算时应用;canwu-movement 则通过内部调度输入 movement_leg_due_v1 结算每个路段。详见扩展系统的运行位置。

对大规模事件系统来说,可持久化的事件机会比“每帧扫描所有实体、逐个掷骰子”更容易扩展。当状态变化、已准入事件或声明的节奏使某个事件有可能发生时,领域插件创建一个事件机会,并把它安排到一个确定的到期时间。到期时,插件只结算已到期的候选,并把随机流、因果来源、去重键、冷却和结果写入边界证据。

生成事件机会、结算事件结果、向角色发布报告,这三步应当分开。同一候选在重新加载后结果不变;从同一个检查点发出不同的命令或决策,则会形成新的平行现实。

权威领域状态和角色知识分开存储。军队抵达后,指挥官可能马上知道新位置,远方的角色则要等报告送到才会更新。CanwuViewer 只读取它所绑定持有人的知识,客户端看到的就是该持有人知道的内容。

边界系统发出的事件,由下一个边界按正常准入流程接收。每个成功的边界都保存其事件、命令、输入、随机抽样、变化、生产者、阶段、可见性和哈希。加载和重放时,模拟内核会重新计算这些证据,并与保存的副本比较。

边界记录里还有两类证据。评估轨迹说明某条应用规则如何为一个对象算出一个结果,转移审计记录一份转移清单的结算结果。二者都纳入边界哈希链,并随边界一起重放。它们既不是 SimEvent,也不属于状态:没有系统会读回轨迹,审计只是供协调者和参与者的系统读取的只读证据。

持有人只能通过 CanwuViewer::evaluation_traces 看到轨迹:一类是关于自身实体的轨迹,另一类是关于其他对象、且在提及该对象的知识发布进入其账本之后才评估的轨迹。研究主体和开发者主体可以看到全部轨迹;公开主体读取时会以 InvalidKnowledgeAuthority 失败。