extract_assets/
extract_assets.rs1use brdb::{Brdb, IntoReader};
13use std::collections::BTreeMap;
14use std::fmt::Write;
15use std::path::PathBuf;
16
17fn screaming_snake(s: &str) -> String {
20 let chars: Vec<char> = s.chars().collect();
21 let mut out = String::with_capacity(s.len() + 8);
22 for (i, &c) in chars.iter().enumerate() {
23 if c == '_' {
24 if !out.is_empty() && !out.ends_with('_') {
25 out.push('_');
26 }
27 continue;
28 }
29 if c.is_ascii_uppercase() {
30 let prev = i.checked_sub(1).map(|j| chars[j]);
31 let next = chars.get(i + 1).copied();
32 let after_lower_or_digit =
33 matches!(prev, Some(p) if p.is_ascii_lowercase() || p.is_ascii_digit());
34 let acronym_end = matches!(prev, Some(p) if p.is_ascii_uppercase())
36 && matches!(next, Some(n) if n.is_ascii_lowercase());
37 if (after_lower_or_digit || acronym_end) && !out.is_empty() && !out.ends_with('_') {
38 out.push('_');
39 }
40 }
41 out.push(c.to_ascii_uppercase());
42 }
43 out
44}
45
46fn main() -> Result<(), Box<dyn std::error::Error>> {
47 let args: Vec<String> = std::env::args().collect();
48 let path = PathBuf::from(
49 args.get(1)
50 .expect("usage: extract_assets <components_assets.brdb> [output.rs]"),
51 );
52 let output_path = args.get(2).map(PathBuf::from);
53
54 let db = Brdb::open(&path)?.into_reader();
55 let gd = db.global_data()?;
56
57 let mut by_type: BTreeMap<String, Vec<String>> = BTreeMap::new();
59 for ty in &gd.external_asset_types {
60 by_type.entry(ty.clone()).or_default();
61 }
62 for (ty, name) in &gd.external_asset_references {
63 by_type.entry(ty.clone()).or_default().push(name.clone());
64 }
65 for names in by_type.values_mut() {
66 names.sort();
67 names.dedup();
68 }
69
70 let mut used_idents: BTreeMap<String, u32> = BTreeMap::new();
74 let mut ident_of: BTreeMap<(String, String), String> = BTreeMap::new();
75 for (ty, names) in &by_type {
76 for name in names {
77 let base = screaming_snake(name);
78 let n = used_idents.entry(base.clone()).or_insert(0);
79 let ident = if *n == 0 {
80 base.clone()
81 } else {
82 format!("{base}_{}", *n + 1)
83 };
84 *n += 1;
85 ident_of.insert((ty.clone(), name.clone()), ident);
86 }
87 }
88
89 let mut out = String::new();
90 macro_rules! w {
91 ($($t:tt)*) => { writeln!(out, $($t)*).unwrap() };
92 }
93
94 w!("// Autogenerated from:");
95 w!("// cargo run --example extract_assets -- path/to/components_assets.brdb crates/brdb/src/assets/external.rs");
96 w!("//");
97 w!("// External asset references the game embeds by name (weapons, pickups,");
98 w!("// projectiles, audio/font descriptors), grouped by asset type. Each asset has");
99 w!("// a named `BString` constant; each type has a `&[BString]` slice of its assets.");
100 w!();
101 w!("use crate::wrapper::BString;");
102
103 for (ty, names) in &by_type {
104 w!();
105 w!("// === {ty} ({}) ===", names.len());
106 for name in names {
107 let ident = &ident_of[&(ty.clone(), name.clone())];
108 w!("pub const {ident}: BString = BString::str({name:?});");
109 }
110 w!();
111 w!("pub const {}_ASSETS: &[BString] = &[", screaming_snake(ty));
112 for name in names {
113 w!(" {},", ident_of[&(ty.clone(), name.clone())]);
114 }
115 w!("];");
116 }
117
118 w!();
119 w!("/// Every external asset type paired with the slice of its assets.");
120 w!("pub const ASSET_TYPES: &[(&str, &[BString])] = &[");
121 for ty in by_type.keys() {
122 w!(" ({ty:?}, {}_ASSETS),", screaming_snake(ty));
123 }
124 w!("];");
125
126 let total: usize = by_type.values().map(|v| v.len()).sum();
127 if let Some(ref p) = output_path {
128 std::fs::write(p, &out)?;
129 eprintln!("Wrote {} ({total} assets, {} types)", p.display(), by_type.len());
130 } else {
131 print!("{out}");
132 eprintln!("{total} assets, {} types", by_type.len());
133 }
134
135 Ok(())
136}