Architecture
A brand plugin is a self-contained simulation module that plugs into the Plantangenet runtime. It owns all game-specific logic — activities, scoring, signals — while the host provides clock, input, and frame lifecycle services.
Brands are loaded as WebAssembly Components via the WIT contract, making them portable, sandboxed, and independent of the host platform.
Key Concepts
| Term | Definition |
|---|---|
| Brand | A complete game experience (e.g. "Rally") built on top of Plantangenet services. |
| Brand Core | A pure Rust library containing all game logic. No DOM, no host bindings. |
| Brand Component | A thin WASM shim that implements the WIT guest interface, backed by the brand core. Targets wasm32-wasip2. |
| Brand Browser Shim | A thin wasm-bindgen shim over the brand core for browser builds. Targets wasm32-unknown-unknown. |
| Host | Either world-runner (server) or a browser runtime that provides clock, inputs, and frame dispatch. |
Crate Layout
A brand plugin is split across three crates. The core contains the logic; the two shims adapt it to different hosts.
my-brand/
├── brand-core/ # Pure game logic (rlib)
│ ├── Cargo.toml
│ └── src/lib.rs # MyBrandSim struct, tick(), signals, queries
│
├── brand-component/ # WASM Component Model guest (cdylib, wasm32-wasip2)
│ ├── Cargo.toml
│ └── src/lib.rs # Implements WIT brand-lifecycle via brand-core
│
└── brand/ # Browser wasm-bindgen shim (cdylib + rlib, wasm32-unknown-unknown)
├── Cargo.toml
└── src/lib.rs # #[wasm_bindgen] entry point over brand-core
Why Three Crates?
The server and browser need different API surfaces from the same game logic:
- Server (world-runner): Coarse lifecycle hooks —
provision,on_frame_active,drain_signals. The host drives the clock; the brand responds passively. - Browser: Fine-grained methods — constructor,
tick(),add_hob(), query methods. JavaScript drives the clock and reads state interactively.
Both targets compile the same brand-core. No game logic is duplicated.
Data Flow
sequenceDiagram
participant Host as Host (world-runner)
participant FM as FrameManager
participant WC as BrandComponentService
participant WASM as Brand Component (.wasm)
Host->>FM: clock step
FM->>WC: on_frame_active(frame_id)
WC->>WASM: call on-frame-active(frame_id)
WASM->>WASM: tick game logic
WC->>WASM: call drain-signals()
WASM-->>WC: Vec<SignalEnvelope>
WC-->>Host: broadcast StreamEvent to /stream
On each frame tick, the FrameManager dispatches lifecycle hooks to all registered services (see Frame Lifecycle). The BrandComponentService wraps a wasmtime component instance and translates these hooks into WIT calls on the guest.
Signals flow out of the brand via drain-signals — JSON-serialised envelopes with a schema key (e.g. "racing.signal.v1"). The host forwards these to WebSocket clients via a broadcast channel.
Inputs flow in via host-inputs::drain-pending-patches — the host queues input patches and the brand consumes them during on-frame-active.
Relationship to ForgeService
A brand component is registered as a ForgeService like any other service. The wasmtime loader wraps the component in a BrandComponentService that implements ForgeService:
provision()→ calls the WITprovisionexporton_frame_active()→ callson-frame-active, thendrain-signalson_frame_init(),on_frame_aggregation(),on_frame_cleanup()→ forwarded to the corresponding WIT exports
This means brand plugins participate in the same ordered, phase-aligned dispatch as Tier 1 and Tier 2 services. A brand typically runs alongside the bundled services, not instead of them.
Next Steps
- WIT Contract Reference — the typed interface between host and guest
- Hello World: Building a Brand Plugin — step-by-step tutorial
- world-runner Host — running brands on the server
- Browser Integration — the wasm-bindgen path