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 undercrates/is a member, so adding a crate means adding a folder.[workspace.package]: shared metadata. Each crate writesversion.workspace = true,edition.workspace = trueand 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 cratepublish = falseso none can be uploaded to crates.io by accident.[workspace.dependencies]: versions are chosen once at the root. A member crate then writeskira.workspace = true, and can still add features locally, e.g.fboard_music = { workspace = true, features = ["sim"] }.- The two
profile.devsections: 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 incrawl_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_rulesneeds 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:
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:
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.