use brdb::{Brdb, IntoReader};
use std::collections::BTreeMap;
use std::fmt::Write;
use std::path::PathBuf;
fn screaming_snake(s: &str) -> String {
let chars: Vec<char> = s.chars().collect();
let mut out = String::with_capacity(s.len() + 8);
for (i, &c) in chars.iter().enumerate() {
if c == '_' {
if !out.is_empty() && !out.ends_with('_') {
out.push('_');
}
continue;
}
if c.is_ascii_uppercase() {
let prev = i.checked_sub(1).map(|j| chars[j]);
let next = chars.get(i + 1).copied();
let after_lower_or_digit =
matches!(prev, Some(p) if p.is_ascii_lowercase() || p.is_ascii_digit());
let acronym_end = matches!(prev, Some(p) if p.is_ascii_uppercase())
&& matches!(next, Some(n) if n.is_ascii_lowercase());
if (after_lower_or_digit || acronym_end) && !out.is_empty() && !out.ends_with('_') {
out.push('_');
}
}
out.push(c.to_ascii_uppercase());
}
out
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
let path = PathBuf::from(
args.get(1)
.expect("usage: extract_assets <components_assets.brdb> [output.rs]"),
);
let output_path = args.get(2).map(PathBuf::from);
let db = Brdb::open(&path)?.into_reader();
let gd = db.global_data()?;
let mut by_type: BTreeMap<String, Vec<String>> = BTreeMap::new();
for ty in &gd.external_asset_types {
by_type.entry(ty.clone()).or_default();
}
for (ty, name) in &gd.external_asset_references {
by_type.entry(ty.clone()).or_default().push(name.clone());
}
for names in by_type.values_mut() {
names.sort();
names.dedup();
}
let mut used_idents: BTreeMap<String, u32> = BTreeMap::new();
let mut ident_of: BTreeMap<(String, String), String> = BTreeMap::new();
for (ty, names) in &by_type {
for name in names {
let base = screaming_snake(name);
let n = used_idents.entry(base.clone()).or_insert(0);
let ident = if *n == 0 {
base.clone()
} else {
format!("{base}_{}", *n + 1)
};
*n += 1;
ident_of.insert((ty.clone(), name.clone()), ident);
}
}
let mut out = String::new();
macro_rules! w {
($($t:tt)*) => { writeln!(out, $($t)*).unwrap() };
}
w!("// Autogenerated from:");
w!("// cargo run --example extract_assets -- path/to/components_assets.brdb crates/brdb/src/assets/external.rs");
w!("//");
w!("// External asset references the game embeds by name (weapons, pickups,");
w!("// projectiles, audio/font descriptors), grouped by asset type. Each asset has");
w!("// a named `BString` constant; each type has a `&[BString]` slice of its assets.");
w!();
w!("use crate::wrapper::BString;");
for (ty, names) in &by_type {
w!();
w!("// === {ty} ({}) ===", names.len());
for name in names {
let ident = &ident_of[&(ty.clone(), name.clone())];
w!("pub const {ident}: BString = BString::str({name:?});");
}
w!();
w!("pub const {}_ASSETS: &[BString] = &[", screaming_snake(ty));
for name in names {
w!(" {},", ident_of[&(ty.clone(), name.clone())]);
}
w!("];");
}
w!();
w!("/// Every external asset type paired with the slice of its assets.");
w!("pub const ASSET_TYPES: &[(&str, &[BString])] = &[");
for ty in by_type.keys() {
w!(" ({ty:?}, {}_ASSETS),", screaming_snake(ty));
}
w!("];");
let total: usize = by_type.values().map(|v| v.len()).sum();
if let Some(ref p) = output_path {
std::fs::write(p, &out)?;
eprintln!("Wrote {} ({total} assets, {} types)", p.display(), by_type.len());
} else {
print!("{out}");
eprintln!("{total} assets, {} types", by_type.len());
}
Ok(())
}