Multi-file Projects
Small games fit in one .bub file. Real ones don’t. Bubbles handles this with compile_many, which stitches multiple files into a single Program where every node can see every other node.
The API
use bubbles::compile_many;
let program = compile_many(&[
("tavern.bub", tavern_src),
("merchants.bub", merchants_src),
("intro.bub", intro_src),
])?;
Each entry is a (name, source) pair. The name only appears in error messages - it’s how you’ll find the file when something goes wrong. The source is the raw .bub text.
compile_many produces a single Program that sees every node in every file as if they were all written in one big document.
Loading from disk
use std::fs;
fn load_program(dir: &str) -> Result<bubbles::Program, Box<dyn std::error::Error>> {
let mut files = Vec::new();
for entry in fs::read_dir(dir)? {
let path = entry?.path();
if path.extension().map_or(false, |e| e == "bub") {
let name = path.file_name().unwrap().to_string_lossy().into_owned();
let src = fs::read_to_string(&path)?;
files.push((name, src));
}
}
let refs: Vec<(&str, &str)> = files.iter()
.map(|(n, s)| (n.as_str(), s.as_str()))
.collect();
Ok(bubbles::compile_many(&refs)?)
}
Point it at assets/dialogue/, ship.
Organising files
Bubbles doesn’t care how you split your files. Common patterns:
- By scene:
tavern.bub,market.bub,forest.bub. - By character:
aria.bub,barkeep.bub,merchant.bub. - By chapter:
ch1_arrival.bub,ch2_the_king.bub,ch3_the_cave.bub. - By concern:
story.bub,barks.bub,tutorial.bub.
Pick the split that makes it easy for writers to find what they need. Bubbles itself is flat - jumps and detours don’t care which file the target lives in.
Duplicate titles
If two files define a node with the same title - and neither has a when: header that differentiates them - compile_many returns an error. That’s the “you probably typed the same name twice” check.
Intentional duplicates (node groups) need distinct when: conditions. Bubbles picks that up as a group and everyone is happy.
# greetings.bub
title: Greet
when: $reputation >= 50
---
Baker: Ah, my favourite customer!
===
# greetings.bub
title: Greet
---
Baker: Morning. What'll it be?
===
Two “Greet” nodes in the same file is fine - they form a group. Two without when: at all is the duplicate that trips the error.
Validation
validate(program) runs the same reference-checking pass compile does, but on an existing Program. Handy for tools and editors that want to verify a file without throwing away the result.
use bubbles::validate;
validate(&program)?;
Catches things like:
<<jump>>/<<detour>>to an unknown node.- References to undeclared variables in expressions (where possible).
- Duplicate group titles that aren’t
when:-differentiated.
Introspection
Once compiled, the Program is queryable:
for title in program.node_titles() {
let tags = program.node_tags(title).unwrap_or_default();
println!("{title} [{}]", tags.join(", "));
}
for decl in program.variable_declarations() {
println!("{} = {}", decl.name, decl.default_src);
}
if program.node_exists("TavernEntry") {
runner.start("TavernEntry")?;
}
Useful for:
- Building editor tooling.
- Generating a scene index.
- Validating against external data (achievements, VO manifests).
Hot reloading during development
A nice dev-loop trick: rebuild the program whenever a .bub file changes, and spin up a fresh runner for the current scene.
use notify::{RecursiveMode, Watcher};
let mut watcher = notify::recommended_watcher(move |res| {
if let Ok(_event) = res {
match load_program("assets/dialogue/") {
Ok(prog) => reload_program(prog),
Err(e) => eprintln!("dialogue compile error: {e}"),
}
}
})?;
watcher.watch("assets/dialogue/", RecursiveMode::Recursive)?;
Writers edit a file, save, and the game picks up the new script - often without losing their current scene state if you snapshot before reloading.
Tip: In production, compile once at load time and cache the
Program. Recompiling is fast but not free, and writers only need hot reload during development.
Try it: The harbour showcase is a two-file project you can run right now:
cargo run -p bubbles-tui -- examples/harbour/harbour.bub examples/harbour/services.bub
harbour.bubdetours intoMapSeller, which is defined inservices.bub, a cross-file jump working exactly as described above. See The Harbour for a full walkthrough.
Next: WebAssembly