Skip to content

Workspaces and crates

Written 2026-10-27 from the project's own docs and code.

In Rust, a crate is one unit of compilation: a library or a program. A Cargo workspace is a folder of several crates that are built together, share one lockfile (the record of exact dependency versions) and one target/ build folder. Both Fboard and The Crawl are workspaces, and they're laid out the same way.

The root Cargo.toml

Both projects' root manifests are nearly identical. This is Fboard's, trimmed:

[workspace]
resolver = "3"
members = ["crates/*"]

[workspace.package]
version = "0.1.0"
edition = "2024"
publish = false

[workspace.dependencies]
bevy = "0.19.0"
fboard_sim = { path = "crates/fboard_sim" }
fboard_music = { path = "crates/fboard_music" }
kira = { version = "0.12.5", default-features = false, features = ["cpal"] }

# Bevy's recommended dev setup: our code builds fast, dependencies run fast.
[profile.dev]
opt-level = 1

[profile.dev.package."*"]
opt-level = 3

What each part does:

  • members = ["crates/*"]: every folder under crates/ is a member, so adding a crate means adding a folder.
  • [workspace.package]: shared metadata. Each crate writes version.workspace = true, edition.workspace = true and so on instead of repeating the values. Both projects use the 2024 edition (an edition is a set of language rules a crate opts into) and mark every crate publish = false so none can be uploaded to crates.io by accident.
  • [workspace.dependencies]: versions are chosen once at the root. A member crate then writes kira.workspace = true, and can still add features locally, e.g. fboard_music = { workspace = true, features = ["sim"] }.
  • The two profile.dev sections: debug builds compile the project's own code with light optimization (fast to rebuild) and every dependency with full optimization (fast to run). Dependencies rarely change, so their slow build happens once. Fboard's handoff notes give the cost: the first build with Bevy takes about 4.5 minutes, and incremental builds take seconds.

The crate split

The two projects divide their code along the same line: rules and data in plain Rust crates, the engine only at the edge.

Fboard:

Crate Owns Depends on
fboard_sim Skate physics, tricks, combos, runs, levels, network message types bevy_math, serde, ron (no engine)
fboard_music The adaptive music engine (see A pattern engine in Rust) serde, serde_json, ron, hound (no engine, no audio device)
fboard_game Everything you see and hear: rendering, menus, input, audio, saves bevy, kira, the two crates above
fboard_tas A headless tool that searches for optimal inputs and writes replays fboard_sim, fboard_music

The Crawl:

Crate Owns
crawl_rules Season, death and production rules. "No engine, no database, no clock." No dependencies at all.
crawl_world Seeded floor generation and collision. No dependencies.
crawl_combat The combat design as a pure library (not yet wired into the server or client, by choice).
crawl_server The server module, compiled to WebAssembly and run inside a SpacetimeDB database.
crawl_net Generated client bindings for that server.
crawl_client The Bevy game client.
crawl_bots A headless crowd of bot connections for load testing.

Why keep the engine out

The payoff is that the same logic runs in every place it's needed, unchanged:

  • Fboard's simulation is stepped by the game, by the headless TAS tool, and (in the networking plan, not yet built) by a dedicated server that would use "only fboard_sim."
  • The Crawl's rules are called by the server module, the client and the bots. Its repo rules say: "Game rules live in crawl_rules, and nowhere else. The server, client and bots call it; they never re-decide a rule." Geometry lives in crawl_world, "so the server, client and bots all agree without sending maps over the wire."
  • Engine-free crates test fast and in isolation. cargo test -p crawl_rules needs no window, database or GPU.

Rules crates are written as functions that take state plus an event and return the effects, rather than doing input/output themselves. From crawl_rules:

//! It has no engine, database or clock: callers feed it events (create, descend,
//! die, collapse) in order, and it returns the effects.
//!
//! The rules are pure functions over a [`SeasonState`] and one [`Character`], so a
//! database can apply them a row at a time: the Spacetime module calls them directly.

Fboard's simulation follows the same idea: Skater::step(input, level, tuning, dt, events) advances one rider by one fixed tick, and even "button released" edges are derived inside the sim from the previous tick's input, "so the sim is a pure function of (state, input)."

Optional features for coupling

Sometimes two library crates need to know about each other, but only for some users. fboard_music converts gameplay state into music inputs, which needs fboard_sim's types. Rather than make the music crate always depend on the simulation, the dependency sits behind a Cargo feature (a named, optional piece of a crate):

[dependencies]
fboard_sim = { workspace = true, optional = true }

[features]
# Gameplay -> music inputs straight from fboard_sim types, shared by the game and the TAS.
sim = ["dep:fboard_sim"]

and in lib.rs:

#[cfg(feature = "sim")]
pub mod sim;

The game and the TAS both turn the feature on, so a TAS run "is judged on exactly what the game would play."

One crate, two outputs

The Crawl's server crate is built two ways:

[lib]
crate-type = ["cdylib", "rlib"]

cdylib produces a dynamic library suitable for WebAssembly (cargo build -p crawl_server --target wasm32-unknown-unknown --release), which is what gets published to the database. rlib is the normal Rust library form, so other crates and tests in the workspace can still link it.

Patch, don't fork

Fboard's architecture notes take a position on modifying dependencies: Bevy is a normal dependency, and "if that ever changes, patch the one crate with [patch.crates-io] rather than forking the engine." Nothing has needed it so far.