Skip to main content

extract_assets/
extract_assets.rs

1//! Generate `assets/external.rs` from a `*_components_assets.brdb` dump.
2//!
3//! Reads the world's `external_asset_references` (the assets the game embeds by
4//! name — weapons, pickups, projectiles, audio/font descriptors) and emits, for
5//! every asset, a named `BString` constant, plus a `&[BString]` slice per asset
6//! type and an `ASSET_TYPES` table tying the type names to their slices.
7//!
8//! Usage:
9//!   cargo run --example extract_assets -- path/to/cl#####_components_assets.brdb \
10//!       crates/brdb/src/assets/external.rs
11
12use brdb::{Brdb, IntoReader};
13use std::collections::BTreeMap;
14use std::fmt::Write;
15use std::path::PathBuf;
16
17/// `BRItemBase` -> `BR_ITEM_BASE`, `Weapon_Spatha` -> `WEAPON_SPATHA`,
18/// `BP_ItemPickup_SportingShotgun` -> `BP_ITEM_PICKUP_SPORTING_SHOTGUN`.
19fn 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            // End of an acronym run: `...BItem` -> `...B_Item`.
35            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    // Group references by asset type, sorted, with each type's names sorted.
58    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    // Assign a unique SCREAMING_SNAKE constant identifier to every asset name.
71    // Identifiers are unique within the whole file, so disambiguate the rare
72    // collision (two names that fold to the same identifier) with a suffix.
73    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}