Skip to main content

banished_formats/
names.rs

1//! Banished's compact name-generation database (`.ndb`) resources.
2//!
3//! The database stores four-character transition records, an index of starting records, and a
4//! sorted string pool of accepted names. The game uses the transitions to compose candidates and
5//! rejects candidates already present in the pool. This decoder exposes both the transition data
6//! and the shipped names so callers can generate valid names without reimplementing the engine's
7//! candidate-selection policy.
8
9use crate::crc::type_id;
10use crate::crs::{self, Reader};
11use crate::error::{format_err, Error, Result};
12
13/// One record of a name database: a four-character state and the letters that may follow it.
14#[derive(Debug, Clone, Copy)]
15pub struct NameTransition {
16    /// Four ASCII bytes used as a transition lookup key; short personal-name prefixes are NUL padded.
17    pub prefix: [u8; 4],
18    /// Nonzero when the engine treats this prefix as a word boundary.
19    pub boundary: u8,
20    /// Bits 0..25 permit `a`..`z`; bit 26 permits termination.
21    pub next_characters: u32,
22}
23
24/// A name database (`Game/Names/*`): the records from which citizen and town names are made.
25#[derive(Debug, Clone)]
26pub struct NameDatabase {
27    /// The transition records.
28    pub transitions: Vec<NameTransition>,
29    /// Indices into `transitions`, sampled by the game's generator.
30    pub starting_transitions: Vec<u16>,
31    /// NUL-terminated packed ASCII strings from the resource.
32    pub string_pool: Vec<u8>,
33    /// Byte offsets into `string_pool`, sorted by name in the shipped resources.
34    pub name_offsets: Vec<u32>,
35    /// Decoded names in the resource's order.
36    pub names: Vec<String>,
37}
38
39impl NameDatabase {
40    /// The resource class name.
41    pub const CLASS: &'static str = "NameDatabase";
42
43    /// Read a [`Self`] from the bytes of its resource.
44    pub fn read(data: &[u8]) -> Result<Self> {
45        let (_, value) = crs::parse(data, |r: &mut Reader, header| {
46            if header.type_id != type_id(Self::CLASS) {
47                return format_err(format!(
48                    "expected NameDatabase, found {:#010x}",
49                    header.type_id
50                ));
51            }
52            r.block(|r, _| {
53                let transition_count = r.count()?;
54                let mut transitions = Vec::with_capacity(transition_count);
55                for _ in 0..transition_count {
56                    let prefix = r.bytes(4)?.try_into().unwrap();
57                    let boundary = r.u8()?;
58                    let next_characters = r.u32()?;
59                    transitions.push(NameTransition {
60                        prefix,
61                        boundary,
62                        next_characters,
63                    });
64                }
65
66                let starting_count = r.count()?;
67                let mut starting_transitions = Vec::with_capacity(starting_count);
68                for _ in 0..starting_count {
69                    starting_transitions.push(u16::from_le_bytes(r.bytes(2)?.try_into().unwrap()));
70                }
71
72                let pool_len = r.count()?;
73                let string_pool = r.bytes(pool_len)?.to_vec();
74                let name_count = r.count()?;
75                let mut name_offsets = Vec::with_capacity(name_count);
76                let mut names = Vec::with_capacity(name_count);
77                for _ in 0..name_count {
78                    let offset = r.u32()?;
79                    let start = offset as usize;
80                    let Some(rest) = string_pool.get(start..) else {
81                        return Err(Error::Format(format!(
82                            "name offset {offset:#x} exceeds string pool size {}",
83                            string_pool.len()
84                        )));
85                    };
86                    let Some(end) = rest.iter().position(|&b| b == 0) else {
87                        return Err(Error::Format(format!(
88                            "name at offset {offset:#x} is not NUL terminated"
89                        )));
90                    };
91                    let name = std::str::from_utf8(&rest[..end])
92                        .map_err(|_| Error::Format(format!("name at {offset:#x} is not UTF-8")))?;
93                    names.push(name.to_owned());
94                    name_offsets.push(offset);
95                }
96
97                if transitions
98                    .iter()
99                    .any(|entry| !entry.prefix.is_ascii() || entry.next_characters >> 27 != 0)
100                {
101                    return format_err("NameDatabase contains an invalid transition entry");
102                }
103                if starting_transitions
104                    .iter()
105                    .any(|&index| index as usize >= transitions.len())
106                {
107                    return format_err("NameDatabase starting index exceeds transition table");
108                }
109                if names.windows(2).any(|pair| pair[0] > pair[1]) {
110                    return format_err("NameDatabase name offsets are not sorted by name");
111                }
112
113                Ok(Self {
114                    transitions,
115                    starting_transitions,
116                    string_pool,
117                    name_offsets,
118                    names,
119                })
120            })
121        })?;
122        Ok(value)
123    }
124}