brdb 0.8.0

A library for reading and writing Brickadia's World files.
Documentation
//! Generate `assets/external.rs` from a `*_components_assets.brdb` dump.
//!
//! Reads the world's `external_asset_references` (the assets the game embeds by
//! name — weapons, pickups, projectiles, audio/font descriptors) and emits, for
//! every asset, a named `BString` constant, plus a `&[BString]` slice per asset
//! type and an `ASSET_TYPES` table tying the type names to their slices.
//!
//! Usage:
//!   cargo run --example extract_assets -- path/to/cl#####_components_assets.brdb \
//!       crates/brdb/src/assets/external.rs

use brdb::{Brdb, IntoReader};
use std::collections::BTreeMap;
use std::fmt::Write;
use std::path::PathBuf;

/// `BRItemBase` -> `BR_ITEM_BASE`, `Weapon_Spatha` -> `WEAPON_SPATHA`,
/// `BP_ItemPickup_SportingShotgun` -> `BP_ITEM_PICKUP_SPORTING_SHOTGUN`.
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());
            // End of an acronym run: `...BItem` -> `...B_Item`.
            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()?;

    // Group references by asset type, sorted, with each type's names sorted.
    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();
    }

    // Assign a unique SCREAMING_SNAKE constant identifier to every asset name.
    // Identifiers are unique within the whole file, so disambiguate the rare
    // collision (two names that fold to the same identifier) with a suffix.
    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(())
}