Skip to main content

banished_formats/
registry.rs

1//! The resources the game has loaded (`Object::ExternalFactory`'s objects), which it enumerates by
2//! type (`ExternalFactory::FindNext`): the toolbar entries, professions, raw materials and
3//! component descriptions (templates).
4//!
5//! At startup `Engine::UpdateLoop::LoadResources` loads `Game\GameResources.rsc` (the loop's name
6//! with `Resources.rsc`), then, for each enabled mod, the mod's `<file>Resources.rsc:resource` when
7//! the packages hold it. Loading a resource loads the resources it points at (`ExternalPtr`), so
8//! the loaded set is everything those roots reach.
9//!
10//! The port finds a resource's pointers by their encoding rather than by each type's serializer: a
11//! present flag (1) then a string naming an `.rsc` object.
12//!
13//! Original: Runtime `0x1800e0020`.
14
15use std::collections::{BTreeMap, BTreeSet};
16
17use crate::pkg::{ref_to_entry, Archive};
18
19/// `Path\File.rsc:object` with backslashes and in lower case: the factory's map compares
20/// names with `_stricmp` (`ExternalFactory::FindNext`, Runtime `0x1800c1240`), so this is
21/// also its order.
22fn key(reference: &str) -> String {
23    let r = reference.replace('/', "\\").to_ascii_lowercase();
24    if r.contains(':') {
25        r
26    } else {
27        format!("{r}:resource")
28    }
29}
30
31/// Every `Path\File.rsc[:object]` pointer in a compiled resource, in order.
32pub fn references(data: &[u8]) -> Vec<String> {
33    let mut out = Vec::new();
34    let mut i = 0;
35    while i + 5 <= data.len() {
36        if data[i] != 1 {
37            i += 1;
38            continue;
39        }
40        let n = u32::from_le_bytes(data[i + 1..i + 5].try_into().unwrap());
41        let found = if n & 0x8000_0000 != 0 {
42            let len = (n & 0x7fff_ffff) as usize;
43            data.get(i + 5..i + 5 + len).and_then(|b| {
44                let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
45                let s = std::str::from_utf8(&b[..end]).ok()?;
46                Some((s.to_string(), 5 + len))
47            })
48        } else if (4..512).contains(&n) {
49            let len = n as usize * 2;
50            data.get(i + 5..i + 5 + len).map(|b| {
51                let units: Vec<u16> = b
52                    .chunks_exact(2)
53                    .map(|c| u16::from_le_bytes([c[0], c[1]]))
54                    .take_while(|&u| u != 0)
55                    .collect();
56                (String::from_utf16_lossy(&units), 5 + len)
57            })
58        } else {
59            None
60        };
61        match found {
62            Some((s, len)) if is_reference(&s) => {
63                out.push(s);
64                i += len;
65            }
66            _ => i += 1,
67        }
68    }
69    out
70}
71
72/// Each resource `roots` reach (by key), with its type id and pointers, or `None` when it
73/// cannot be read. Reading and scanning a resource depends only on its bytes, so each wave of
74/// newly found resources is read in parallel; [`Registry::build`] then walks them in the
75/// factory's order.
76type Parsed = BTreeMap<String, Option<(u32, Vec<String>)>>;
77
78fn parse_reachable(archive: &Archive, roots: &[String]) -> Parsed {
79    let mut parsed = Parsed::new();
80    let mut wave: Vec<String> = roots.iter().map(|r| key(r)).collect();
81    while !wave.is_empty() {
82        wave.sort_unstable();
83        wave.dedup();
84        let results = crate::par::map(&wave, |k| {
85            let data = archive.read(&ref_to_entry(k)).ok()?;
86            let header = crate::crs::header(&data).ok()?;
87            Some((header.type_id, references(&data)))
88        });
89        let mut next = Vec::new();
90        for (k, result) in wave.into_iter().zip(results) {
91            if let Some((_, children)) = &result {
92                next.extend(
93                    children
94                        .iter()
95                        .map(|c| key(c))
96                        .filter(|c| !parsed.contains_key(c)),
97                );
98            }
99            parsed.insert(k, result);
100        }
101        next.retain(|c| !parsed.contains_key(c));
102        wave = next;
103    }
104    parsed
105}
106
107fn is_reference(s: &str) -> bool {
108    let path = s.split(':').next().unwrap_or("");
109    path.len() > 4
110        && path.to_ascii_lowercase().ends_with(".rsc")
111        && s.chars()
112            .all(|c| c.is_ascii_alphanumeric() || " _-.\\/:()'&".contains(c))
113}
114
115/// The loaded resources: each `Path\File.rsc:object` (keyed in lower case) with its type id
116/// and the reference as first written.
117#[derive(Debug, Default, Clone)]
118pub struct Registry {
119    /// Each resource, by lower-case reference: its type id and the reference as first written.
120    pub loaded: BTreeMap<String, (u32, String)>,
121}
122
123impl Registry {
124    /// Load `roots` and everything they reach.
125    pub fn build(archive: &Archive, roots: &[String]) -> Registry {
126        let parsed = parse_reachable(archive, roots);
127        let mut loaded = BTreeMap::new();
128        let mut seen = BTreeSet::new();
129        let mut stack: Vec<String> = roots.iter().rev().cloned().collect();
130        while let Some(r) = stack.pop() {
131            let k = key(&r);
132            if !seen.insert(k.clone()) {
133                continue;
134            }
135            let Some(Some((type_id, children))) = parsed.get(&k) else {
136                continue;
137            };
138            let written = if r.contains(':') {
139                r.replace('/', "\\")
140            } else {
141                format!("{}:resource", r.replace('/', "\\"))
142            };
143            loaded.insert(k, (*type_id, written));
144            for child in children.iter().rev() {
145                stack.push(child.clone());
146            }
147        }
148        Registry { loaded }
149    }
150
151    /// `ExternalFactory::FindNext` over one type: the references of that type, in the
152    /// factory's order (by name, without case).
153    pub fn of_type(&self, type_id: u32) -> Vec<&str> {
154        self.loaded
155            .values()
156            .filter(|(t, _)| *t == type_id)
157            .map(|(_, r)| r.as_str())
158            .collect()
159    }
160
161    /// Whether a resource is loaded.
162    pub fn contains(&self, reference: &str) -> bool {
163        self.loaded.contains_key(&key(reference))
164    }
165}
166
167/// The game's own root list and each enabled mod's (`<file>Resources.rsc:resource`), as
168/// `LoadResources` names them.
169pub fn roots(mod_files: &[String]) -> Vec<String> {
170    let mut out = vec![r"Game\GameResources.rsc:resource".to_string()];
171    out.extend(
172        mod_files
173            .iter()
174            .map(|f| format!("{f}Resources.rsc:resource")),
175    );
176    out
177}