Builds and starter packs
You can build your hubs and spokes on the platform instead of on your machine: send your
crates' sources to execBuild, poll execBuildStatus, and deploy the build with
execDeploy. You need no Rust toolchain. The starter packs, which replace the compute
templates, come the same way.
execStarters and execBuild need the organization's manage_compute permission;
execBuildStatus needs view_compute_diagnostics. None of them take an app token: use your own
session.
Starter packs
execStarters(appId) returns fifteen crates and a manifest that deploys them as one app,
seventeen node types (racing and territory each serve two):
| Crate | Node type | What it does | Replaces |
|---|---|---|---|
world-tick | world, the root hub | The world's clock (day and night), weather, resource nodes that empty and grow back, and liveops events your developers schedule. It publishes the conditions on its world topic. | world-engine, liveops-scheduler |
matchmaker | matchmaker | A rating queue for each key (ranked, casual). The rating window widens the longer a player waits. Every player in a proposed match must accept, and then the queue reserves a session for them. | matchmaking |
session | session | One game from lobby to result: a host, ready checks, turns with a time limit, points the hub decides, and a winner. | match-engine |
npc-mobs | npcs | One area's mobs and NPCs, keyed x,z, moved on a timer and shown to nearby players as realtime actors. Mobs chase, go home past their leash, take hits the hub referees, and respawn. NPCs walk routes and talk. | mob-engine, npc-engine |
leaderboard | leaderboard | A board other hubs submit to, ranked by the hub, with seasons on a schedule. | board-engine |
deck | deck | A card table with true hidden information: hands only their player reads. | deck-engine |
tactics | tactics | A grid battle in phases against the hub's units. | tms-battle |
director | director | Waves down a lane, towers that may not cut it, a boss with phases. | director |
minigame | minigame | A duel of hidden throws, revealed together. | minigame |
instance | instance | A party's private run: seeded floors, permadeath, expiry, depth to a leaderboard. | instance-engine |
arena | arena | Classes and abilities in real time, with a movement warden for reported positions. | abilities-engine, movement-warden |
racing | race, ball | Races (gates, laps, splits, a ghost) and ball games (possession, goals). | racing, possession |
territory | territory, zone | Control points captured by standing on them, and a shrinking circle. | territory |
player | player | A player's own hub, keyed by their id: inventory, soft currency, crafting, loot with pity, idle production, levels, achievements, quests. Not a ledger. | loot, production engines |
market | market | Vendors that restock, order books and trades between players, with escrow in each player's own hub. Not a ledger. | market-engine |
query {
execStarters(appId: "…") {
manifestJson
starters { crate nodeType description files { path content } }
}
}
Their endpoints:
| Type | Method | Who | What |
|---|---|---|---|
world | state | anyone | The clock, weather, active modifiers, resource nodes and events |
world | harvest { node } | players | Takes one from a resource node (twice a second at most) |
world | schedule { name, start_ms, end_ms, modifiers }, cancel { name } | developers | Liveops events. Modifiers multiply where events overlap; the NPC engine reads aggro |
matchmaker | join { rating, party?, leader? }, leave, accept, decline, status | players | Your own place in the queue: party (while a party forms), queued, proposed, then matched with the session key. A party is matched whole |
session | join, leave, ready { ready }, start (host), act { points } (whoever's turn it is), state | players | The game. points_for in the crate is the rule to replace with yours |
session | configure { .. } | the host in the lobby, or developers | Player counts, turn time, target score, rounds |
npcs | state, attack { mob }, talk { npc } | players | Mobs and NPCs, public properties only. A hit needs the player near the mob; the damage is the hub's |
npcs | adopt { species, name }, rename, summon, dismiss, release { pet }, pets | players | Pets that follow their owner while summoned; pets answers the caller's, with their bond |
Since ckx-sdk 0.10.0 the starters speak the Game Kit protocol: their topics
carry numbered snapshot and delta messages, their changes take an action_id they answer once,
and their refusals carry a kit:<code>: code. The SDKs' Game Kit drives them with typed
clients, and its topology composes, builds and deploys them for you: see the Game
Kit.
The genre and economy starters' endpoints and topics are on the Game Kit's starters page (ckx-sdk 0.11.0 and 0.12.0). The economy lives in hub state and is not a ledger: read its limits first.
Building
mutation {
execBuild(input: {
appId: "…"
crates: [{ name: "world-tick", files: [
{ path: "Cargo.toml", content: "…" },
{ path: "src/lib.rs", content: "…" }
] }]
}) {
buildId
status
}
}
It returns at once, with the build queued. Poll it until it is succeeded or failed:
query {
execBuildStatus(appId: "…", buildId: "…") {
status
sdkVersion
log
artifacts { crate digest sizeBytes }
}
}
Each crate becomes one module. log is the compiler's output, and a failed build says why; its
first line names the toolchain the build ran with. sdkVersion is the version of ckx-sdk the
build compiled against (of crowdy-client-sdk for a CLIENT half), once the build has started.
A build of the four starters with nothing cached takes about a minute; the same sources again
come from the cache.
What a crate may contain
-
Files:
Cargo.toml,README.md, and.rsfiles undersrc/, withsrc/lib.rsamong them. -
Cargo.tomlholds[package](name,version,edition,description,publish,license,authors,rust-version),[lib]withcrate-type = ["cdylib"], and[dependencies]onckx-sdk,serdeandserde_jsononly. Each key is on one line, and there is no workspace inheritance. The platform pointsckx-sdkat its own copy, whatever version the crate names, and adds the release profile. A build'ssdkVersionsays which version that was; code written for a newer SDK than the platform's fails to compile against it.[package]name = "lobby"version = "0.1.0"edition = "2024"[lib]crate-type = ["cdylib"][dependencies]ckx-sdk = "0.7.0"serde = { version = "1", features = ["derive"] } -
Code may not read the machine it is built on.
include!,include_str!,include_bytes!,env!,option_env!, theasm!macros,#[path]and#[link]are refused, even renamed, and so ismod name;inside amacro_rules!body. The names are fine in comments and strings. The compiler also runs in a sandbox with no network, no environment and nothing of the host but the toolchain. -
The module may import only the ck-exec guest ABI, which is what
ckx-sdkuses, and must export whatexport_hub!orexport_spoke!generates.
A mod's CLIENT half is built from one crowdy-client-sdk crate under
the same rules, with its own dependency list, and runs in browsers rather than on ck-exec.
Limits
| Crates in a build | 16 |
| Files in a crate | 64 |
| Source in a build | 2 MB |
| Build time | 10 minutes for the whole build: its crates compile one after another, about 25 seconds each, so the full starter pack (15 crates) takes about six |
| Module size | 16 MB |
| At once | One build per app, and one per person across every app, on each API instance; eight waiting per instance |
| Kept | 7 days, with their modules |
The requests are billed like any other; the compile itself is not, as the compute compile it replaces was not.
On your machine
The open dev kit runs this build on your own machine: the same checks on the
same files, the Rust release the platform pins, its release profile and its dependency
versions, then the same module check. ckx-kit check runs the source checks alone, and
cargo test tests your crates natively against a fake of the platform. When a crate passes
there, build it here from the same sources and deploy that build.
Deploying a build
Pass the build's id to execDeploy. A type may name its crate instead of a digest, and the
modules the manifest names come from the build. The starter pack's manifestJson is ready for
this.
mutation {
execDeploy(input: {
appId: "…"
buildId: "…"
manifestJson: "{\"root\":\"world\",\"types\":{\"world\":{\"kind\":\"hub\",\"crate\":\"world-tick\",\"client\":true}}}"
}) {
version
}
}
A build belongs to its app: another app cannot deploy it.
From an SDK
CrowdyJS does the whole flow for you:
const pack = await client.exec.starters(appId);
const build = await client.exec.build(
appId,
pack.starters.map((s) => ({ name: s.crate, files: s.files })),
);
const done = await client.exec.waitForBuild(appId, build.buildId);
if (done.status !== 'succeeded') throw new Error(done.log ?? 'build failed');
await client.exec.deploy({ appId, buildId: build.buildId, ...pack.manifest });
CrowdyJS client.exec | CrowdyCPP client.exec() |
|---|---|
starters(appId) | starters(appId) |
build(appId, crates), each crate's files a path map or a { path, content } list | build(appId, std::vector<ExecCrate>) |
buildStatus(appId, buildId) | buildStatus(appId, buildId) |
waitForBuild(appId, buildId, { intervalMs, timeoutMs }) | waitForBuild(appId, buildId, intervalMs, timeoutMs), blocking; from an event loop, poll buildStatusAsync |
deploy({ appId, root, types, buildId }), a type naming its crate | deploy(appId, root, types, buildId), an ExecNodeType with crate set |
Every CrowdyCPP method but waitForBuild has an …Async twin. Use CrowdyJS 18.1.0 or
CrowdyCPP 0.55.0 or later on dev.
Typed state
The legacy game models kept containers with typed properties in the database. In ck-exec, a hub
keeps them in its own state with ckx_sdk::model: declared container types with typed,
defaulted properties, edges between containers, and a visibility for each property that decides
what a player may read. The model is plain data, so it is snapshotted with the rest of the hub.
use ckx_sdk::prelude::*;
fn schema() -> Schema {
Schema::new().container("Character", |c| {
c.prop("hp", PropKind::Int, 100, Visibility::Public)
.prop("gold", PropKind::Int, 0, Visibility::Owner)
.prop("weakness", PropKind::Str, "", Visibility::Hidden)
})
}
// In a handler:
let hero = self.model.ensure(&schema(), "Character", &player.to_string(), Some(player))?;
self.model.add(&schema(), hero, "hp", -12)?;
let theirs = self.model.view(&schema(), hero, Viewer::Player(player))?; // hp and gold, not weakness
self.model.publish_changes(ctx, &schema(), "world"); // public changes, to the topic's subscribers
link, linked and traverse (five levels at most) relate containers. A property added in a
new version reads as its default in containers made before it. The npc-mobs starter keeps its
mobs and NPCs this way.
Coming from the legacy APIs
| Legacy | ck-exec |
|---|---|
computeTemplates, computeDeployTemplate | execStarters, then execBuild and execDeploy |
computeDeployVersion (compiled on the server) | execBuild, then execDeploy with the buildId |
Game-model schemas: container types, typed properties, visibility | ckx_sdk::model in a hub |
The Game Kit (client.kit(appId).deploy) | The starter packs, and the new Game Kit's kit.topology.deploy |
playerComputeDeploy (players' server code) | Mods: execModBuild, then execModDeploy |
playerComputeDeploy with the CLIENT target (players' browser code) | CLIENT halves: execModClientBuild, then execModClientDeploy |