Command plugin
In this tutorial you add a new command, set_stance, that lets an army’s commander set the army’s stance. You will learn how a command plugin declares its payload schema and the state it reads and writes, how its handler checks authority, and how Canwu applies the change.
Open plugin.rs
Browse the example folder
Run it
Section titled “Run it”From the repository root:
cargo run -p canwu-api --example pluginThe example prints nothing. It exits with status 0 when submit accepts the command. Try next shows how to print the result and the rejections.
How a plugin command runs
Section titled “How a plugin command runs”View diagram source
flowchart LR
Env["CommandEnvelope<br/>Command::Plugin"] --> Schema["Validate payload<br/>against the schema"]
Schema --> Handler["Handler set_stance<br/>reads SimulationView"]
Handler --> Auth{"Issuer commands<br/>the army?"}
Auth -- "no" --> Reject["InvalidAuthority"]
Auth -- "yes" --> Directive["SystemDirective::SetComponent"]
Directive --> Writes["Check declared writes"]
Writes --> Apply["Canwu applies the change<br/>and records an event"]
How it works
Section titled “How it works”1. Give the plugin an identity
Section titled “1. Give the plugin an identity”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" } // register() follows}Canwu saves the name, version, and semantic hash with the run. When you load a snapshot, the plugin registers again and must produce the same identity and registrations; otherwise loading fails with PluginManifestMismatch. Change the semantic hash whenever the plugin’s behavior changes.
2. Declare the command
Section titled “2. Declare the command”register describes the command with a PluginActionDescriptor:
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,)(The properties are condensed here; the source spells each one out.)
payload_schemarequires an integerarmyand a stringstance. Withallow_additional: false, any other field is rejected. Canwu validates the payload before the handler runs.readslists the state the handler may read.StateKey::core_armies()is the built-in army table.writeslists the state the handler’s directives may change.
3. Check authority in the handler
Section titled “3. Check authority in the handler”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", ));}view is a read-only SimulationView limited to the declared reads; view.army returns an error if core_armies is missing from them. context is the CommandContext, which carries the issuer and the rest of the command’s metadata. The rule “only the commander may set the stance” lives in the handler, so every client that sends the command goes through the same check.
4. Return the change as a directive
Section titled “4. Return the change as a directive”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"),}])The handler has read access only, and it describes the change it wants. Canwu checks every directive against the declared writes, applies it, and records a stance_changed event.
5. Register the plugin and submit the command
Section titled “5. Register the plugin and submit the command”let mut canwu = Canwu::demo(35)?;let ids = Canwu::demo_ids();canwu.register_plugin(&StancePlugin)?;canwu.submit(CommandEnvelope::new( Issuer::Actor(ids.commander), Command::Plugin { plugin: "example-stance".to_owned(), command: "set_stance".to_owned(), payload: json!({ "army": ids.army, "stance": "hold" }), },))?;Canwu::demo builds the built-in demo world, whose army is commanded by ids.commander. Register plugins before the first command runs.
submit applies the command immediately at the current time. It is the shortest path for a demo. A game that records its inputs for replay uses enqueue_command on canonical ingress, as in Move an army. A run uses one path or the other; mixing them returns MixedCommandIngress.
Try next
Section titled “Try next”Print the event log after submit:
for event in canwu.events() { println!("{} {} {}", event.timestamp, event.kind.qualified_event_type(), event.summary);}Day 0, 00:00 example-stance.stance_changed Army 1 changed stanceThen submit a command that breaks each rule. submit returns an error with these codes and messages:
| Change to the command | Error code | Message |
|---|---|---|
Issuer Issuer::Actor(ids.observer) |
InvalidAuthority |
only the army commander may set its stance |
Payload "stance": 3 |
InvalidPayload |
payload field stance has the wrong type |
Extra field "extra": 1 |
InvalidPayload |
plugin command payload contains an undeclared field |
When several systems must act at the same simulation time in a set order, continue with phased boundary and allocation.