Build mods with Crowdy Studio
Players write mods in Crowdy Studio, inside a running game whose page embeds Studio's dock. A mod is written in Rust and has two halves: the server half is a ck-exec mod on a grid you own, and the optional client half is that mod's CLIENT half, which runs in each visitor's browser. This guide is for a mod developer working in a game, not a studio operator deploying platform infrastructure.
Start from a game you run
Studio runs inside a game, so you need a running game that embeds it. Blocks with Friends, the game this guide used to follow, is no longer hosted by Crowded Kingdoms. Use either of these:
-
The Construct, on your own machine. The public template embeds Studio and its agent, and its setup gives you an app you can mod:
git clone -b dev https://github.com/CrowdedKingdoms/the-construct.gitcd the-constructnpm installCONSTRUCT_EMAIL=you@example.com CONSTRUCT_PASSWORD=... \npm run setup -- --org "My studio" --app "My game"Setup creates your organization and app, a Constructor access tier with the code keys and
use_studio_agent(granted to you), and the app's Studio Agent policy. It prints an app id: copy.env.exampleto.env.localand setVITE_APP_IDto it. Then runnpm run dev, open http://localhost:5175 and sign in. See the companion repository. -
A game whose developer hosts it with Crowdy Studio embedded. The game's access tiers decide who may write mods there. A developer who wants to offer mods reads what a game needs for mods.
Open Crowdy Studio
- Sign in to the game, so it holds an app-scoped game token for you.
- Stand inside a grid you own and that grants the code keys:
write_server_codeandrun_server_codefor a server mod,write_client_codeandrun_client_codefor a client mod. - Open Studio the way the game offers it. In The Construct, walk onto the
Claim & Studio pad and press
E: you claim the chunk you stand on as a grid of your own, and Studio docks beside the game.Mtoggles it after that. - Open or create a server, client, or full-stack project. You can copy an app-provided starter from Common Files into either target.
Crowdy Studio opens Monaco with target-aware Cargo.toml and src/*.rs tabs.
Rust syntax colors, parser diagnostics, workspace completion, hover, symbols,
and workspace-local navigation run in a lazily loaded browser module worker.
The worker loads local parser/grammar WASM; it has no authoring endpoint and
receives no game token. Its feedback is advisory. Test draft and Deploy
live run the authoritative platform build.
On desktop, Crowdy Studio opens in a resizable right-hand dock so the running game remains visible. Drag the divider (or focus it and use the arrow keys) to balance the game and editor. Click the game to return to pointer-lock controls, then click the editor to code; the panes isolate their keyboard input. Narrow screens use the full-screen editor.
Edits autosave to the cloud, but they do not change the running grid until you select Test draft or Deploy live. This keeps builds and neighbor-visible effects explicit while still letting you observe the updated server mod or hot-swapped CLIENT worker without closing the studio.
The local worker is a parser and indexed-symbol service, not rustc or rust-analyzer. It cannot prove borrow/lifetime correctness, perform complete trait resolution or type inference, expand procedural macros, run Cargo build scripts, or reproduce full crate/build-target semantics. A locally clean file can still fail deployment, and a local warning does not block deployment.
A new project's server target starts from the platform's mod starter. For a client mod, download:
Project shape
Each target is a crate, and every deployment sends it with this minimum shape:
Cargo.toml
src/lib.rs
A server mod is a ckx-sdk crate whose dependencies are ckx-sdk, serde
and serde_json only; see what a crate may
contain. A client mod is a
crowdy-client-sdk crate whose dependencies are crowdy-client-sdk, serde
and serde_json only; see the crate:
[dependencies]
crowdy-client-sdk = "0.1.0"
serde_json = "1"
A client crate still on the legacy crowdy-compute-sdk is refused before any
build.
The platform builds both offline, in a sandbox, against its pinned SDKs. The browser worker's embedded platform index helps with names, signatures, and hover text, but does not resolve arbitrary crates. Do not add arbitrary dependencies; the authoritative build refuses anything outside its allow-list.
Server mod
A server mod is a ck-exec mod: a hub that runs as the current
grid owner, never as a visitor or the original marketplace author, in the
owner's own sandbox. The mod starter is a ckx-sdk crate that greets visitors
and follows what happens in its grid. Players standing in the grid call the
mod's endpoints by name. It reads the grid's chunks, voxels and actors, writes
voxels inside the grid, in each chunk while its owner holds update_voxel_data
on the most specific grid covering that chunk, and hears the grid's world
events; it calls no other node and sends no realtime events.
See what a mod can do.
Test draft and Deploy live both build the server crate, deploy it to
the grid as the mod mod:<server module name>, and switch it on. Switching a
mod on needs the app's code admission.
Client HUD mod
Client mods run in the visitor's browser worker. They have no DOM, token, network, or unrestricted world access. Presentation crosses a host call and the game renders it in a mod-owned HUD region:
use crowdy_client_sdk::{api, host_call};
use serde_json::json;
fn on_init() {}
fn on_tick(_dt: u32) {
let info = match host_call("grid_info", json!({})) {
Ok(value) => value,
Err(_) => return,
};
let low = &info["low"];
let parse = |v: &serde_json::Value| v.as_str()?.parse::<i64>().ok();
let Some(x) = parse(&low["x"]) else { return };
let Some(y) = parse(&low["y"]) else { return };
let Some(z) = parse(&low["z"]) else { return };
let actors = api::actors_list(x, y, z)
.unwrap_or_else(|_| json!({ "actors": [] }));
let _ = host_call(
"hud_set",
json!({ "payload": { "kind": "presence", "actors": actors["actors"] } }),
);
}
fn on_invoke(_payload: &[u8]) -> Vec<u8> { Vec::new() }
crowdy_client_sdk::register_module!(
init: on_init,
tick: on_tick,
invoke: on_invoke
);
The downloadable client helper uses compile-ready control flow without the abbreviations in this explanation. What it can call lists every host call.
Tick rate and mouse input
A CLIENT worker ticks once a second by default. Ask for a faster loop in the
crate's Cargo.toml — it is the only key admitted under
[package.metadata.crowdy], a whole number of milliseconds, clamped to
16–1000:
[package.metadata.crowdy]
tick_interval_ms = 50 # 16..1000; default 1000
Inside on_tick, api::pointer_clicks() drains the mouse clicks the host game
collected on its canvas since the previous call. It is CLIENT-only and
lives in the input capability group (rate cap 400 calls/s). The value is
{ nowMs, buttons, holdingMs, clicks }: buttons is the live MouseEvent.buttons
bitfield (1 = left held), holdingMs maps a button index to how long it has been
held ("0" = left), and clicks is the drained list of
{ t: "down" | "up", button, atMs, heldMs?, nx, ny } with canvas NDC (nx/ny
in −1..1, +ny up). Clicks on Studio chrome are omitted, so a mod can be tested
with the IDE open. A click-to-charge shot: start on a left down, draw the power
bar from holdingMs["0"], fire on the left up using its heldMs.
fn on_tick(_dt: u32) {
let input = match api::pointer_clicks() { Ok(v) => v, Err(_) => return };
if let Some(clicks) = input["clicks"].as_array() {
for c in clicks {
if c["t"] == "up" && c["button"] == 0 {
let charge_ms = c["heldMs"].as_u64().unwrap_or(0);
// fire toward (nx, ny) with charge_ms …
}
}
}
}
Bundle server and client halves
In a full-stack project the client target is the server mod's CLIENT half: the mod is named for the server module, and its CLIENT half rides it. Deploy live autosaves one coherent revision and builds CLIENT first and SERVER second, so a client failure never deploys a new server version. Only after both builds succeed does it deploy and switch the server mod on, attach the CLIENT half to it, and hot-swap your preview to the served module. There is no pairing to set: a CLIENT half belongs to its mod.
A visitor's browser runs your CLIENT half only once they have agreed to it: consented to it at its capability hash, or trusted you as its author on this grid. The capability summary they are shown is derived from the built module, and a version that can do something new is asked about again. See what a visitor is asked.
Debugging
- Build failed: read the build log in the panel; the platform's build is authoritative even if the local parser showed no problem.
- No completion/diagnostics: deployment still works. Check that the browser can load the same-origin module-worker and parser/grammar WASM assets, then reopen the panel. If local language startup fails, the editor deliberately falls back to the textarea, which is the only fallback.
- Deploy refused: read the message. You have one build at a time, server
or client (
RATE_LIMITEDwhile another runs); attaching needs you to own the grid withwrite_client_codeand the app's code admission; see CLIENT half errors. - Client HUD does not appear: confirm the visitor consented or trusts
you, holds
run_client_codeand stands in the grid, and that the mod is switched on with its CLIENT half attached. - No CLIENT target in Studio: the page is not cross-origin isolated. Type
crossOriginIsolatedin the browser console; when it printsfalse, the game's host is not sending the isolation headers. - Server mod does not answer: check that it is switched on and that the app admits it. A mod that is switched on but not running starts when an actor arrives in its grid or a player calls it.
See Crowdy Studio and mods in the browser for host integration and sandbox details, what a game needs for mods for the game's side, mods for the server-side API, and CLIENT halves for the client side.