Paired mod reference
Start with the first-hour guide. Full code lives in
examples/mods/cosmic.
Entry points
| Source | Contract |
|---|---|
build.rs | Calls experience_sdk::project::generate() |
src/lib.rs | Enables 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.rs | Generator 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.
| Field | Required bounds |
|---|---|
bool, i8/i16/i32/i64, u8/u16/u32 | Optional 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.
| Directory | Included in |
|---|---|
assets/server/ | Server artifact only; constants unavailable to client builds |
assets/client/ | Client bundle only |
Other assets/ paths | Both 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:
| Path | Contents |
|---|---|
server/ | Server WASM, generated manifest and files.toml index, server/shared assets |
client/<id>.cxb | Signed client bundle |
resource.mcpack | Generated block/item art |
registry.json, messages/ | Shared definitions and Go bindings |
package.json | Package 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.