Skip to main content

extract_components/
extract_components.rs

1//! Generate `src/assets/component_catalog.rs` — the ergonomic per-component
2//! catalog (type name + host brick asset(s) + wire port names) — from a "zoo"
3//! save: a world with every component placed and every wire port wired (built
4//! in-game by the ue4ss inventory tool). Mirrors brs-js's COMPONENTS map.
5//!
6//! Usage:
7//!   cargo run --example extract_components -- <zoo.brdb> src/assets/component_catalog.rs
8//!
9//! Regenerate whenever the game's components change (see the inventory pipeline
10//! runbook). Host bricks come from each component's placed brick; ports come
11//! from the zoo's wires (every port is wired), so both are complete only when
12//! run against a fully-built, saved zoo.
13use brdb::{AsBrdbValue, Brdb, IntoReader, WireChunkSoA};
14use std::collections::{BTreeMap, BTreeSet};
15use std::fmt::Write;
16use std::path::PathBuf;
17
18fn main() -> Result<(), Box<dyn std::error::Error>> {
19    let args: Vec<String> = std::env::args().collect();
20    let path = PathBuf::from(
21        args.get(1)
22            .expect("usage: extract_components <zoo.brdb> [out.rs]"),
23    );
24    let out_path = args.get(2).map(PathBuf::from);
25    let db = Brdb::open(path)?.into_reader();
26    let data = db.global_data()?;
27
28    // The standalone phase attaches otherwise-brickless components to this generic host for
29    // probing; it is never a real component host, so it is filtered out of the host lists.
30    let standalone_host_brick = brdb::assets::bricks::B_1X1F_ROUND;
31    let standalone_host: &str = standalone_host_brick.asset().as_ref();
32
33    // component type index -> host brick asset name(s) / wire port indices.
34    // Host names are resolved inline (basic and procedural bricks index different
35    // name sets), so this stores names rather than type indices.
36    let mut bricks: BTreeMap<u16, BTreeSet<String>> = BTreeMap::new();
37    let mut inputs: BTreeMap<u16, BTreeSet<u16>> = BTreeMap::new();
38    let mut outputs: BTreeMap<u16, BTreeSet<u16>> = BTreeMap::new();
39
40    // Grid 1 is the main grid; higher ids are microchip inner grids. Probe
41    // until a grid id is missing (same convention as read_components).
42    for gid in 1..64 {
43        let chunks = match db.brick_chunk_index(gid) {
44            Ok(c) => c,
45            Err(_) => break,
46        };
47        for chunk in &chunks {
48            // brick index -> brick type index (basic-brick asset), for the
49            // host-brick mapping.
50            let bsoa = db.brick_chunk_soa(gid, chunk.index)?;
51            let pb_start = bsoa.procedural_brick_starting_index;
52            // Procedural brick type index (>= pb_start) -> procedural asset index, via the
53            // per-size run-length counters (same expansion SoA::iter_bricks uses).
54            let proc_asset_by_size: Vec<u32> = bsoa
55                .brick_size_counters
56                .iter()
57                .flat_map(|c| std::iter::repeat(c.asset_index).take(c.num_sizes as usize))
58                .collect();
59            let brick_types = bsoa.brick_type_indices;
60
61            if chunk.num_components > 0 {
62                let (csoa, components) = db.component_chunk_soa(gid, chunk.index)?;
63                let brick_indices = csoa.component_brick_indices;
64                // Expand run-length (type_index, num_instances) into a flat
65                // per-instance list of component type indices.
66                let type_indices = csoa
67                    .component_type_counters
68                    .iter()
69                    .flat_map(|v| {
70                        let ti = v.type_index as u16;
71                        (0..v.num_instances).map(move |_| ti)
72                    })
73                    .collect::<Vec<_>>();
74                for i in 0..components.len() {
75                    let comp_ty = type_indices[i];
76                    // ensure every placed component is present even with no wires
77                    inputs.entry(comp_ty).or_default();
78                    outputs.entry(comp_ty).or_default();
79                    let brick_index = brick_indices[i].as_brdb_u32()? as usize;
80                    if let Some(&bt) = brick_types.get(brick_index) {
81                        // Basic bricks index basic_brick_asset_names directly; procedural
82                        // bricks (type index >= pb_start) map through the size run-lengths to
83                        // a procedural_brick_asset_names index.
84                        let host = if bt < pb_start {
85                            data.basic_brick_asset_names.get_index(bt as usize).cloned()
86                        } else {
87                            proc_asset_by_size
88                                .get((bt - pb_start) as usize)
89                                .and_then(|&ai| {
90                                    data.procedural_brick_asset_names.get_index(ai as usize).cloned()
91                                })
92                        };
93                        if let Some(host) = host {
94                            if host != standalone_host {
95                                bricks.entry(comp_ty).or_default().insert(host);
96                            }
97                        }
98                    }
99                }
100            }
101
102            if chunk.num_wires > 0 {
103                let soa = db.wire_chunk_soa(gid, chunk.index)?.to_value();
104                let soa: WireChunkSoA = (&soa).try_into()?;
105                // local and remote ports are distinct types, so handle each in
106                // its own loop (they share component_type_index / port_index).
107                for p in &soa.local_wire_sources {
108                    outputs
109                        .entry(p.component_type_index)
110                        .or_default()
111                        .insert(p.port_index);
112                }
113                for p in &soa.remote_wire_sources {
114                    outputs
115                        .entry(p.component_type_index)
116                        .or_default()
117                        .insert(p.port_index);
118                }
119                for p in &soa.local_wire_targets {
120                    inputs
121                        .entry(p.component_type_index)
122                        .or_default()
123                        .insert(p.port_index);
124                }
125                for p in &soa.remote_wire_targets {
126                    inputs
127                        .entry(p.component_type_index)
128                        .or_default()
129                        .insert(p.port_index);
130                }
131            }
132        }
133    }
134
135    // Resolve indices to names.
136    let name_of = |idx: u16| data.component_type_names.get_index(idx as usize).cloned();
137    let port_of = |idx: u16| data.component_wire_port_names.get_index(idx as usize).cloned();
138
139    let all: BTreeSet<u16> = bricks
140        .keys()
141        .chain(inputs.keys())
142        .chain(outputs.keys())
143        .copied()
144        .collect();
145
146    // (name, host bricks, input ports, output ports), sorted by name.
147    let mut rows: Vec<(String, Vec<String>, Vec<String>, Vec<String>)> = Vec::new();
148    for ty in all {
149        let Some(name) = name_of(ty) else { continue };
150        let mut bs: Vec<String> = bricks
151            .get(&ty)
152            .map(|s| s.iter().cloned().collect())
153            .unwrap_or_default();
154        bs.sort();
155        bs.dedup();
156        let mut ins: Vec<String> = inputs
157            .get(&ty)
158            .map(|s| s.iter().filter_map(|&p| port_of(p)).collect())
159            .unwrap_or_default();
160        ins.sort();
161        ins.dedup();
162        let mut outs: Vec<String> = outputs
163            .get(&ty)
164            .map(|s| s.iter().filter_map(|&p| port_of(p)).collect())
165            .unwrap_or_default();
166        outs.sort();
167        outs.dedup();
168        rows.push((name, bs, ins, outs));
169    }
170    rows.sort_by(|a, b| a.0.cmp(&b.0));
171
172    let mut out = String::new();
173    macro_rules! w {
174        ($($t:tt)*) => { writeln!(out, $($t)*).unwrap() };
175    }
176    let slice = |v: &[String]| {
177        v.iter()
178            .map(|s| format!("{s:?}"))
179            .collect::<Vec<_>>()
180            .join(", ")
181    };
182
183    w!("// Autogenerated from a zoo save:");
184    w!("//   cargo run --example extract_components -- <zoo.brdb> src/assets/component_catalog.rs");
185    w!("// Do not edit by hand.");
186    w!();
187    w!("/// Per-component catalog entry: the full component type name, its host");
188    w!("/// brick asset(s), and its wire input/output port names. Extracted from a");
189    w!("/// fully-placed, fully-wired \"zoo\" save (mirrors brs-js's COMPONENTS).");
190    w!("#[derive(Debug, Clone, Copy, PartialEq, Eq)]");
191    w!("pub struct ComponentInfo {{");
192    w!("    /// e.g. \"BrickComponentType_WireGraph_Exec_Branch\".");
193    w!("    pub name: &'static str,");
194    w!("    /// Host brick asset name(s) that carry this component.");
195    w!("    pub bricks: &'static [&'static str],");
196    w!("    /// Wire input port names.");
197    w!("    pub inputs: &'static [&'static str],");
198    w!("    /// Wire output port names.");
199    w!("    pub outputs: &'static [&'static str],");
200    w!("}}");
201    w!();
202    w!("impl ComponentInfo {{");
203    w!("    /// The primary host brick asset (first, if any).");
204    w!("    pub const fn brick(&self) -> Option<&'static str> {{");
205    w!("        self.bricks.first().copied()");
206    w!("    }}");
207    w!("}}");
208    w!();
209    w!(
210        "/// Every component present in the zoo, sorted by `name` (binary-searchable)."
211    );
212    w!("pub static COMPONENTS: &[ComponentInfo] = &[");
213    for (name, bs, ins, outs) in &rows {
214        w!(
215            "    ComponentInfo {{ name: {name:?}, bricks: &[{}], inputs: &[{}], outputs: &[{}] }},",
216            slice(bs),
217            slice(ins),
218            slice(outs),
219        );
220    }
221    w!("];");
222    w!();
223    w!("/// Look up a component by its full type name.");
224    w!("pub fn component(name: &str) -> Option<&'static ComponentInfo> {{");
225    w!("    COMPONENTS");
226    w!("        .binary_search_by(|c| c.name.cmp(name))");
227    w!("        .ok()");
228    w!("        .map(|i| &COMPONENTS[i])");
229    w!("}}");
230
231    if let Some(ref p) = out_path {
232        std::fs::write(p, &out)?;
233        eprintln!("Wrote {}", p.display());
234    } else {
235        print!("{out}");
236    }
237    eprintln!(
238        "Extracted {} components ({} with host bricks)",
239        rows.len(),
240        rows.iter().filter(|r| !r.1.is_empty()).count(),
241    );
242    Ok(())
243}