Extend with plugins
Use this page when your game or model needs rules Canwu does not ship: a new player command, state your code owns, or logic that runs at every daily or monthly boundary. You add them as a simulation plugin, a Rust type that implements SimulationPlugin and registers its commands, schemas, and systems when the run starts. The simulation core stays domain-neutral; military, economic, political, and social rules live in plugins.
Terms used below:
- Semantic hash: a 64-character lowercase hex string you choose for each plugin release. Change it whenever handler behavior changes; Canwu uses it to refuse loading or replaying a save with different rules.
- Boundary system: a function that runs inside a boundary, in a fixed phase, with declared reads and writes.
- Domain record: typed, persisted state owned by one plugin, described by a domain schema (
DomainRecordType).
See the terminology reference for other terms.
Choose an extension path
Section titled “Choose an extension path”| What you need | What to register |
|---|---|
| A user action or service request makes a local change | A domain command: register_command() |
| Several systems act together on the same day, turn, or time point | A boundary system: register_boundary_system() |
| The plugin owns typed data | A record schema: register_record_schema(), then read and write through the domain-record APIs |
| Results need randomness that replays exactly | Random streams listed in the boundary system’s random_streams, so each draw is recorded as evidence |
| Several plugins must write one transition together, and a participant that stays silent must be caught | A transition manifest, with each participant staging its writes in phase 10; see transition manifests |
| Players or agents need to see why a rule produced a value | An evaluation trace recorded by the phase-7 or phase-12 system that computed the value; see evaluation traces |
Write a command plugin
Section titled “Write a command plugin”This excerpt from the plugin example adds a set_stance command that only an army’s commander may use. The full file adds the imports and a main that registers the plugin and submits the command.
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, ) }}What happens when the commander submits set_stance:
- Canwu checks the payload against the declared
payload_schema. - The handler reads the army through the read-only
SimulationViewand comparescontext.issuerwith the army’s commander. - It returns
SystemDirectivevalues that describe the change; Canwu applies them in the next step. - Canwu checks each directive against the declared
writesand commits the stance component.
Any other issuer gets InvalidAuthority, and no state changes. Run the example:
cargo run -p canwu-api --example pluginFor a step-by-step walkthrough, see the command plugin tutorial.
Add a boundary system
Section titled “Add a boundary system”Use a boundary system when supply, demand, allocation, and consequences must be settled together in one boundary. You describe it with a BoundarySystemContract. This one, from the reference world, applies queued army movements:
let state = schema.state_key(); // StateKey of the plugin's own record schemalet mut system = BoundarySystemContract::new( "apply_movement_transition_v1", BoundaryPhase::DomainDeltaProposal, // phase 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)?;The contract fields:
| Field | Declares |
|---|---|
phase, cadence |
Where in the 14-phase order the system runs, and how often (event-driven, daily, monthly, and so on) |
reads, writes |
The StateKey values the system may read and change |
visibility |
Whether its writes are visible later in the same boundary or from the next one |
emits |
The domain event kinds it may emit |
reservation_offers, reservation_requests, reservation_reads |
Resource pools it supplies, requests from, or reads allocation results for |
random_streams |
The random streams it may draw from |
knowledge_writes, plugin_ingress_targets |
Knowledge it may publish, and plugin ingress it may schedule |
Canwu runs systems in fixed phase order, rejects undeclared access, allocates reservations deterministically, and commits the whole boundary atomically: every change lands, or the boundary rolls back. The phased boundary tutorial builds a runnable example (cargo run -p canwu-api --example phased_boundary).
From a requirement to a replayable extension
Section titled “From a requirement to a replayable extension”- Choose a stable plugin name, version, and semantic hash.
- Register the domain schemas, commands, and boundary systems the plugin owns.
- Have handlers and systems read only through
SimulationViewand return declared changes. - Send domain commands and plugin ingress through the public entry points; keep no live runtime reference.
- Test save and load, exact replay, a failed authority check, and atomic rollback.
Domain extensions you can reuse
Section titled “Domain extensions you can reuse”These crates are optional extensions built on the public API. Application code depends on them directly; canwu-api does not re-export them. crates/README.md lists every crate.
| Crate | What it provides | Design page |
|---|---|---|
canwu-society |
Social diffusion across population cohorts | Society, culture, and law |
canwu-culture |
Culture authoring, compilation, and lifecycle, settled by the host (CulturePlugin) or in the engine (CultureBoundaryPlugin) |
Society, culture, and law |
canwu-law |
Experimental legal institutionalization, including weighted, unit-block, and consultation procedure stages | Society, culture, and law |
canwu-technology |
Technology evidence, local capability, and diffusion | Technology |
canwu-fiscal |
Fiscal procedure: law, assessment, authorization, receipts, audit. Resource balances and transport stay in other crates. | Fiscal institutions |
canwu-resource |
Conserved resource accounts, demand, allocation, transfer, delegated access grants, and fulfillment | Resources and production |
canwu-production |
Production processes, facilities, work orders, and output settlement | Resources and production |
canwu-military |
Forces, operations, combat, occupation, and military knowledge, as one optional military model | Military |
canwu-movement |
Movement lifecycle: movement orders, leg settlement, capacity pools, and holder-relative movement reports | Routing, transport, and movement |
canwu-information |
Information lifecycle, including authenticity findings during interpretation | Knowledge and information |
canwu-correspondence |
Correspondence with holder-relative planning, delegated carriers, and carrier seizure | Knowledge and information |
Before adopting an extension, check its version, dependencies, and persistence contract. Historical content, scenario parameters, and your game’s rules stay in content packs or your application.
Lifecycle rules
Section titled “Lifecycle rules”- Register plugins before any command, scenario validation, or load that depends on them.
- Pass the same plugin set, with the same semantic hashes, when you restore a save or replay a journal.
- Change state only through commands and ingress; plugins keep no live runtime reference.
- Treat a semantic-hash change as a behavior change: release a new plugin version, and expect saves written with the old hash to stop loading.
- Depend on
canwu-apiand the extension crates you use. Do not depend oncanwu-sim.
Before you start, read the public boundaries in the architecture overview, then copy the public example closest to your case.