Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Paired mod reference

Start with the first-hour guide. Full code lives in examples/mods/cosmic.

Entry points

SourceContract
build.rsCalls experience_sdk::project::generate()
src/lib.rsEnables one side and includes experience_sdk::include_shared!()
src/server.rs#[experience_sdk::entry::server] on impl Experience; generates registration and registry replies
src/client.rs#[experience_sdk::entry::client] on impl ClientPart; exports client callbacks
src/shared.rsGenerator input: project metadata, imports, typed ID constants and message structs

Build with exactly one of the server and client features, with defaults disabled. Each forwards to its SDK feature. The SDK dependency also needs messages; the build dependency needs project. Keep private logic in the server module.

Return Some(&shared::REGISTRY) from ClientPart::registry. Authored initialization and callbacks wait for the server’s matching fingerprint; a mismatch stops the part and asks for a rebuild. Missing confirmation leaves it waiting. A recovered guest negotiates again. Fields, directions, revisions and typed IDs affect the digest.

Declarations

From Cosmic’s src/shared.rs:

#![allow(unused)]
#![project(id = "cosmic", permissions(messaging, ui))]

fn main() {
use experience_sdk::{id, message::{BlockId, ItemId}};

#[block(name = "Moon rock", texture = assets::ASSET_MOON_PNG, hardness = 2.0)]
pub const MOON_ROCK: BlockId = id!("cosmic:moon_rock");

#[item(name = "Moon key", icon = assets::ASSET_MOON_PNG, max_stack = 1)]
pub const MOON_KEY: ItemId = id!("cosmic:moon_key");
}

Omit hardness for an unbreakable cube. Item stacks default to 64. Textures and icons must name generated shared PNG constants. The builder runs server registration and validates it before publishing. No handwritten Experience TOML or pack JSON is needed. EntityId and WorldId name definitions; they do not register or create instances.

Messages

#![allow(unused)]
fn main() {
#[message(id = "cosmic.ping", schema = 1, to_server)]
pub struct Ping {
    pub request: u32,
}
}

Use to_client for replies. Add control to a to-client negotiation or acknowledgment message to retain it when transport credit is exhausted; ordinary messages use best-effort delivery. The generator produces signed channels, Rust Message implementations and Go bindings from the same ordered fields.

FieldRequired bounds
bool, i8/i16/i32/i64, u8/u16/u32Optional integer min/max, within the Rust type
String, BlockId, ItemId, EntityId, WorldId#[field(max_bytes = N)], counting UTF-8 bytes
Vec<T> of a supported scalar#[field(max_items = N)]; strings and IDs also need max_bytes

Unsupported types and conditional definitions fail generation. Inside the entry impls, write one #[on_message] method per received type:

#![allow(unused)]
fn main() {
// In #[server] impl Experience for Server:
#[on_message]
fn ping(ctx: &Callback, player: PlayerId, ping: Ping) -> Result<(), GuestError> {
    ctx.send(&player, &Pong { request: ping.request })
}

// In #[client] impl ClientPart for Client:
#[on_message]
fn pong(pong: Pong) {
    // Update the UI with pong.request.
}
}

Client code sends with client::send(&Ping { request: 1 }). Wrong-direction sends and handlers fail compilation. The generated routers check channel, revision and payload before calling handlers. The client host checks the signed revision before its unchanged dispatch ABI runs. Unknown or malformed messages are rejected on the server; client dispatch silently drops unknown channels and records that fail to decode. Handlers receive decoded values; manual channel dispatch and raw sends are outside the paired-project API.

Always validate requests against server state; a matching registry is no player authorization.

Assets

assets/sky/stars.bin generates shared::assets::ASSET_SKY_STARS_BIN, a typed Asset with path and compile-checked bytes. Missing files, ambiguous constant names and symlinks fail generation or compilation.

DirectoryIncluded in
assets/server/Server artifact only; constants unavailable to client builds
assets/client/Client bundle only
Other assets/ pathsBoth halves; use for block textures and item icons

examples/mods/cosmic/assets/moon.png and the terminal fixture’s examples/experiences/terminal-client/assets/terminal.png are original sample art covered by this repository’s license. Game assets are not included.

Packaging and installation

cinnabar-cxb mod build --project <dir> [--release] writes dist/<id>.cmod:

PathContents
server/Server WASM, generated manifest and files.toml index, server/shared assets
client/<id>.cxbSigned client bundle
resource.mcpackGenerated block/item art
registry.json, messages/Shared definitions and Go bindings
package.jsonPackage format 2 and SHA-256 inventory

mod install refuses a package of another format; rebuild it with the installed cinnabar-cxb.

The first build creates .local/publisher.seed; keep it private and stable. Install with cinnabar-cxb mod install <file.cmod> --server <world>. Start bedrock-local-server -dir <world> -addr 127.0.0.1:19132; it discovers installed mods. Keep experience-runtime and cinnabar-cxb beside the server, or set -experience-runtime and CINNABAR_CXB_BIN. To replace an installation, stop the server and remove <world>/mods/<id> first. Preserve saved block identities.

Existing Go servers

The generated module is cinnabar.local/<id>/messages. Use a local replace or place it under your server module. The generated go.mod pins the helper and compatible Dragonfly version. Import the helper as github.com/cinnabar-mc/cinnabar/tools/localserver/cinnabarext; regenerate older bindings and their module together to update the module identity.

Before listening, call messages.RegisterRegistry(ext) and check its error. Use cinnabarext.OnMessage(ext, messages.PingType, callback) to receive, and ext.Send/ext.Broadcast with generated messages to send. Typed OnMessage receivers get (uuid.UUID, T); world access belongs in Dragonfly transactions. EncodeRecord and DecodeRecord expose canonical wire-v2 records for other transports.

Current low-level OnClientMessage/OnEpoch hooks also receive the originating cinnabarext.PlayerSession; match it to the admitted session before routing input. The generated helper pin predates these signatures. To use them, update the main server module’s helper dependency to the Cinnabar revision deployed with the host; regenerating bindings alone is insufficient.

SendControl retains a refused negotiation/acknowledgment reply until credit refills; newer replies on the route replace it. Epoch changes and disconnects clear pending replies. Check its error: BurstExceeded means it cannot fit the maximum burst and was not retained. Ordinary Send and Broadcast are best effort.