Gotchas¶
Written 2026-10-27 from the project's own docs and code.
Problems that actually came up in Fboard and The Crawl, as recorded in their handoff notes. Engine-specific ones (Bevy API renames between versions, lighting defaults) are in the Bevy section.
gen is a reserved word in edition 2024¶
The 2024 edition reserves gen as a keyword, so it can't be used as a plain function or module name. The Crawl's floor generators would naturally have been called gen; they're named terrain instead.
Cargo trusts modification times¶
Cargo decides what to rebuild by comparing file modification times. During mutation testing on The Crawl (breaking code on purpose to check that a test fails, then restoring it), a file was restored by copying the original back. The copy carried an older timestamp, so Cargo didn't notice the change and tested a stale build. The fix recorded in the handoff: after restoring a file by copying it back, touch it.
This matters most exactly when you're checking your tests, because a stale build makes a broken test look like a working one or the reverse.
Floating point isn't the same everywhere¶
Fboard's simulation uses f32 and sin. Its architecture notes rule out lockstep networking (where every machine runs the same simulation from the same inputs and only inputs are sent) for that reason: floating-point results "aren't bit-identical across CPUs and compilers, so clients would desync." The planned design uses an authoritative server instead.
On one machine, though, Fboard does get bit-identical results, and it relies on that for replays. Two details make it work:
- Compute the timestep exactly the way the engine does. The headless tools don't use a hand-written
1.0 / 60.0. They reproduce Bevy's fixed clock arithmetic:
/// The sim's tick length, computed the way Bevy's fixed clock computes it
/// so headless runs use bit-identical `dt`.
pub fn tick_dt() -> f32 {
std::time::Duration::from_secs_f64(1.0 / crate::net::TICK_RATE as f64).as_secs_f32()
}
The point is to match the engine's arithmetic step for step rather than to pick a "correct" value, because a difference in the last bit of dt would compound over thousands of ticks.
- Hash a representation that shows every bit. The replay checkpoints hash the player's state serialized as RON (a Rust-flavoured text data format). RON prints floats in shortest round-trip form, so "any bit of drift changes the hash."
With these, a replay recorded headless in a release build plays back identically in the client's dev build, which uses different optimization levels.
Exact time for music¶
The same instinct applies to musical time, where it's easier to get right. Fboard's music engine stores time as an exact fraction (i64 numerator and denominator) instead of a float, so a cycle (one bar) divided into thirds adds back up to exactly one cycle:
Comparisons multiply across in i128 to avoid overflow. See A pattern engine in Rust.
Small things¶
- Line endings. Fboard's
.gitattributesnormalizes to LF. The CRLF warnings Git prints on commit under Windows are harmless. - The first Bevy build is slow (about 4.5 minutes in Fboard); incremental builds take seconds. The
profile.devsettings in Workspaces and crates are what keep later builds fast. - Loud errors beat silent differences. Fboard's music parser rejects anything outside its supported subset with an error naming the function or column, e.g.
mini-notation: unexpected character at column 5 in "...". The rule in its docs: "Anything outside the subset is a loud parse error, never a silent difference."