跳转到内容

通过插件扩展

当你的游戏或模型需要参伍自带之外的规则时,就读这一页:比如一条新的玩家命令、由你的代码管理的状态,或者每个日结、月结结算边界都要运行的逻辑。这些都以模拟插件的形式加入:模拟插件是实现了 SimulationPlugin 的 Rust 类型,在模拟运行开始时注册自己的命令、schema 和系统。模拟内核保持领域中立,军事、经济、政治和社会规则都放在插件里。

下文用到的术语:

  • 语义哈希:你为插件每个发布版本指定的一串 64 个字符的小写十六进制值。处理逻辑的行为一变就要换新值;参伍据此拒绝用规则不同的插件加载或重放存档。
  • 边界系统:在结算边界内、按固定阶段运行的函数,读写范围都要事先声明。
  • 领域记录:归某个插件所有、类型化并持久化的状态,由领域 schema(DomainRecordType)描述结构。

其他术语见术语表。

需求 注册什么
一个用户操作或服务请求带来局部变化 领域命令:register_command()
多个系统要在同一个日结、回合或时间点共同处理 边界系统:register_boundary_system()
插件需要自己的类型化数据 记录 schema:register_record_schema(),再通过领域记录 API 读写
结果需要可精确重放的随机性 在边界系统的 random_streams 中列出随机流,每次抽样都会记为证据
多个插件要共同写入一次转移,并且要发现没有响应的参与者 转移清单,由各参与者在第 10 阶段暂存写入;见转移清单
玩家或智能体需要知道某条规则为什么算出某个值 由算出该值的第 7 或第 12 阶段系统记录评估轨迹;见评估轨迹

下面节选自插件示例,它添加一条只有军队指挥官能用的 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 后的处理过程:

  1. 参伍按声明的 payload_schema 检查载荷。
  2. 处理器通过只读的 SimulationView 读取军队,并把 context.issuer 与军队指挥官比对。
  3. 处理器返回描述变化的 SystemDirective,由参伍在下一步应用。
  4. 参伍按声明的 writes 检查每条指令,然后提交姿态组件。

其他签发者会得到 InvalidAuthority,状态不发生任何变化。运行示例:

Terminal window
cargo run -p canwu-api --example plugin

逐步讲解见命令插件教程。

当供给、需求、资源分配和后果必须在同一个结算边界内一起结算时,使用边界系统。边界系统用 BoundarySystemContract 描述。下面这个取自参考世界,负责应用排队中的军队移动:

let state = schema.state_key(); // 插件自有记录 schema 的 StateKey
let 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)。

  1. 为插件选定稳定的名称、版本和语义哈希。
  2. 注册插件拥有的领域 schema、命令和边界系统。
  3. 处理器和系统只通过 SimulationView 读取,并返回已声明的变化。
  4. 领域命令和插件输入都从公开入口提交,不保留运行时的实时引用。
  5. 测试保存与加载、精确重放、权限校验失败和原子回滚。

这些 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 通信,支持持有人相对规划、受托承运人和承运人扣押 知识与信息系统

采用某个扩展前,先确认它的版本、依赖关系和存档契约。历史内容、场景参数和游戏自身的规则放在内容包或你的应用里。

  • 在依赖插件的命令、场景验证或加载之前注册插件。
  • 从存档恢复或重放日志时,提供同一组插件和相同的语义哈希。
  • 状态只通过命令和输入改变;插件不持有运行时的实时引用。
  • 语义哈希变化即行为变化:发布新的插件版本,并预期用旧哈希写出的存档将无法加载。
  • 依赖 canwu-api 和实际用到的扩展 crate,不要依赖 canwu-sim。

动手之前,先读架构总览中的公开边界,再从最接近你需求的公开示例改起。