Skip to content

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.

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

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:

  1. Canwu checks the payload against the declared payload_schema.
  2. The handler reads the army through the read-only SimulationView and compares context.issuer with the army’s commander.
  3. It returns SystemDirective values that describe the change; Canwu applies them in the next step.
  4. Canwu checks each directive against the declared writes and commits the stance component.

Any other issuer gets InvalidAuthority, and no state changes. Run the example:

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

For a step-by-step walkthrough, see the command plugin tutorial.

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 schema
let 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”
  1. Choose a stable plugin name, version, and semantic hash.
  2. Register the domain schemas, commands, and boundary systems the plugin owns.
  3. Have handlers and systems read only through SimulationView and return declared changes.
  4. Send domain commands and plugin ingress through the public entry points; keep no live runtime reference.
  5. Test save and load, exact replay, a failed authority check, and atomic rollback.

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.

  • 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-api and the extension crates you use. Do not depend on canwu-sim.

Before you start, read the public boundaries in the architecture overview, then copy the public example closest to your case.