通过插件扩展
当你的游戏或模型需要参伍自带之外的规则时,就读这一页:比如一条新的玩家命令、由你的代码管理的状态,或者每个日结、月结结算边界都要运行的逻辑。这些都以模拟插件的形式加入:模拟插件是实现了 SimulationPlugin 的 Rust 类型,在模拟运行开始时注册自己的命令、schema 和系统。模拟内核保持领域中立,军事、经济、政治和社会规则都放在插件里。
下文用到的术语:
- 语义哈希:你为插件每个发布版本指定的一串 64 个字符的小写十六进制值。处理逻辑的行为一变就要换新值;参伍据此拒绝用规则不同的插件加载或重放存档。
- 边界系统:在结算边界内、按固定阶段运行的函数,读写范围都要事先声明。
- 领域记录:归某个插件所有、类型化并持久化的状态,由领域 schema(
DomainRecordType)描述结构。
其他术语见术语表。
选择扩展方式
Section titled “选择扩展方式”| 需求 | 注册什么 |
|---|---|
| 一个用户操作或服务请求带来局部变化 | 领域命令:register_command() |
| 多个系统要在同一个日结、回合或时间点共同处理 | 边界系统:register_boundary_system() |
| 插件需要自己的类型化数据 | 记录 schema:register_record_schema(),再通过领域记录 API 读写 |
| 结果需要可精确重放的随机性 | 在边界系统的 random_streams 中列出随机流,每次抽样都会记为证据 |
| 多个插件要共同写入一次转移,并且要发现没有响应的参与者 | 转移清单,由各参与者在第 10 阶段暂存写入;见转移清单 |
| 玩家或智能体需要知道某条规则为什么算出某个值 | 由算出该值的第 7 或第 12 阶段系统记录评估轨迹;见评估轨迹 |
编写命令插件
Section titled “编写命令插件”下面节选自插件示例,它添加一条只有军队指挥官能用的 set_stance 命令。完整文件里还有导入语句,以及注册插件并提交命令的 main。
struct StancePlugin;
fn set_stance( view: &SimulationView<'_>, context: &CommandContext, payload: &Value,) -> Result<Vec<SystemDirective>, CanwuError> { let army = ArmyId::new( payload .get("army") .and_then(Value::as_u64) .expect("payload was validated before the handler ran"), ); let Some(army_state) = view.army(army)? else { return Err(CanwuError::new( ErrorCode::ArmyNotFound, format!("army {army} was not found"), )); }; if context.issuer != Issuer::Actor(army_state.commander) { return Err(CanwuError::new( ErrorCode::InvalidAuthority, "only the army commander may set its stance", )); } Ok(vec![SystemDirective::SetComponent { state: StateKey::new("military", "stance"), entity: EntityRef::Army(army), component: "stance".to_owned(), value: payload["stance"].clone(), summary: format!("Army {army} changed stance"), }])}
impl SimulationPlugin for StancePlugin { fn name(&self) -> &'static str { "example-stance" }
fn version(&self) -> &'static str { "1.0.0" }
fn semantic_hash(&self) -> &'static str { "a33fb7c59ae5a17685bd94f99154c5423dc7bf70d3dbde6ef4daf73670a37d26" }
fn register(&self, registrar: &mut PluginRegistrar<'_>) -> Result<(), CanwuError> { registrar.register_command( PluginActionDescriptor { name: "set_stance".to_owned(), description: "Set an army stance through an issuer-aware command".to_owned(), payload_schema: PayloadSchema::Object { properties: BTreeMap::from([ ( "army".to_owned(), PayloadProperty { value_type: PayloadValueType::Integer, required: true, }, ), ( "stance".to_owned(), PayloadProperty { value_type: PayloadValueType::String, required: true, }, ), ]), allow_additional: false, }, reads: vec![StateKey::core_armies()], writes: vec![StateKey::new("military", "stance")], }, set_stance, ) }}指挥官提交 set_stance 后的处理过程:
- 参伍按声明的
payload_schema检查载荷。 - 处理器通过只读的
SimulationView读取军队,并把context.issuer与军队指挥官比对。 - 处理器返回描述变化的
SystemDirective,由参伍在下一步应用。 - 参伍按声明的
writes检查每条指令,然后提交姿态组件。
其他签发者会得到 InvalidAuthority,状态不发生任何变化。运行示例:
cargo run -p canwu-api --example plugin逐步讲解见命令插件教程。
添加边界系统
Section titled “添加边界系统”当供给、需求、资源分配和后果必须在同一个结算边界内一起结算时,使用边界系统。边界系统用 BoundarySystemContract 描述。下面这个取自参考世界,负责应用排队中的军队移动:
let state = schema.state_key(); // 插件自有记录 schema 的 StateKeylet mut system = BoundarySystemContract::new( "apply_movement_transition_v1", BoundaryPhase::DomainDeltaProposal, // 第 7 阶段 SystemCadence::EventDriven,);system.reads = vec![StateKey::core_ingress(), state.clone()];system.writes = vec![state];system.visibility = StateVisibility::SameBoundary;registrar.register_boundary_system(system, apply_movement_transitions)?;契约字段:
| 字段 | 声明内容 |
|---|---|
phase、cadence |
系统在 14 个阶段中的位置,以及运行频率(事件驱动、每日、每月等) |
reads、writes |
系统可以读取和修改的 StateKey |
visibility |
写入结果在同一结算边界的后续阶段可见,还是从下一个结算边界起可见 |
emits |
系统可能发出的领域事件种类 |
reservation_offers、reservation_requests、reservation_reads |
系统提供供给、提出请求或读取分配结果的资源池 |
random_streams |
系统可以抽样的随机流 |
knowledge_writes、plugin_ingress_targets |
系统可以发布的知识,以及可以安排的插件输入 |
参伍按固定阶段顺序运行系统,拒绝未声明的访问,确定性地完成资源分配,并原子提交整个结算边界:要么全部变化生效,要么整个结算边界回滚。完整例子见阶段边界与资源分配教程(cargo run -p canwu-api --example phased_boundary)。
从需求到可重放的扩展
Section titled “从需求到可重放的扩展”- 为插件选定稳定的名称、版本和语义哈希。
- 注册插件拥有的领域 schema、命令和边界系统。
- 处理器和系统只通过
SimulationView读取,并返回已声明的变化。 - 领域命令和插件输入都从公开入口提交,不保留运行时的实时引用。
- 测试保存与加载、精确重放、权限校验失败和原子回滚。
可复用的领域扩展
Section titled “可复用的领域扩展”这些 crate 是建立在对外 API 之上的可选扩展。应用代码直接依赖它们,canwu-api 不会再导出它们。完整 crate 列表见 crates/README.md。
| crate | 提供的能力 | 设计页面 |
|---|---|---|
canwu-society |
cohort 之间的社会扩散 | 社会、文化与法律 |
canwu-culture |
文化的编写、编译和生命周期,可由上层应用结算(CulturePlugin),也可在引擎内结算(CultureBoundaryPlugin) |
社会、文化与法律 |
canwu-law |
实验性的法律制度化,支持加权、表决单元和征询程序阶段 | 社会、文化与法律 |
canwu-technology |
技术证据、地方能力和传播 | 技术系统 |
canwu-fiscal |
财政程序:法令、核定、授权、凭证和审计。资源余额和运输由其他 crate 负责。 | 财政制度 |
canwu-resource |
守恒的资源账户、需求、分配、转移、动用授权和履约 | 资源与生产系统 |
canwu-production |
生产工艺、设施、工单和产出结算 | 资源与生产系统 |
canwu-military |
部队、作战、战斗、占领和军事知识,作为一种可选的军事模型 | 军事系统 |
canwu-movement |
移动生命周期:移动命令、路段结算、运力池和持有人相对的移动报告 | 路线、运输与移动系统 |
canwu-information |
信息生命周期,包括解读时的真伪判定 | 知识与信息系统 |
canwu-correspondence |
通信,支持持有人相对规划、受托承运人和承运人扣押 | 知识与信息系统 |
采用某个扩展前,先确认它的版本、依赖关系和存档契约。历史内容、场景参数和游戏自身的规则放在内容包或你的应用里。
生命周期规则
Section titled “生命周期规则”- 在依赖插件的命令、场景验证或加载之前注册插件。
- 从存档恢复或重放日志时,提供同一组插件和相同的语义哈希。
- 状态只通过命令和输入改变;插件不持有运行时的实时引用。
- 语义哈希变化即行为变化:发布新的插件版本,并预期用旧哈希写出的存档将无法加载。
- 依赖
canwu-api和实际用到的扩展 crate,不要依赖canwu-sim。
动手之前,先读架构总览中的公开边界,再从最接近你需求的公开示例改起。