Skip to main content

rpfm_extensions/startpos/
tables.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Database tables used to build a startpos, accessed by column name.
12//!
13//! The startpos build reads the `start_pos_*` tables of the campaign pack plus many game tables (units, factions,
14//! buildings...). This module collects them from decoded DB files, merging the fragments of each table, and
15//! keeps the decoded values so numbers keep their exact binary value (the game copies some `f32` values as is).
16
17use std::borrow::Cow;
18use std::collections::HashMap;
19
20use rpfm_lib::error::{RLibError, Result};
21use rpfm_lib::files::db::DB;
22use rpfm_lib::files::table::DecodedData;
23
24#[cfg(test)] #[path = "tables_test.rs"] pub(crate) mod tables_test;
25
26/// Buckets of a new chained hash set of the game (see [`hash_set_order`]).
27const HASH_SET_FIRST_BUCKETS: usize = 4;
28
29//---------------------------------------------------------------------------//
30//                              Types
31//---------------------------------------------------------------------------//
32
33/// The tables available to the startpos build, by table name (without the `_tables` suffix).
34#[derive(Debug, Clone, Default)]
35pub struct StartposTables {
36    tables: HashMap<String, Table>,
37}
38
39/// A table: its columns and the rows of every fragment.
40#[derive(Debug, Clone, PartialEq)]
41pub struct Table {
42
43    /// Table name, without the `_tables` suffix.
44    name: String,
45
46    /// Column positions, by name.
47    columns: HashMap<String, usize>,
48
49    /// Rows, in fragment order and then in file order.
50    rows: Vec<Vec<DecodedData>>,
51}
52
53/// A row of a table.
54#[derive(Debug, Clone, Copy)]
55pub struct Row<'a> {
56    table: &'a Table,
57    data: &'a [DecodedData],
58}
59
60//---------------------------------------------------------------------------//
61//                              Implementations
62//---------------------------------------------------------------------------//
63
64impl StartposTables {
65
66    /// Creates an empty table set.
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// Adds the rows of a DB fragment. Fragments of the same table are merged in the order they're added.
72    ///
73    /// # Errors
74    ///
75    /// Returns an error if a later fragment lacks a column of the first fragment of its table.
76    pub fn add_db(&mut self, db: &DB) -> Result<()> {
77        let name = db.table_name_without_tables();
78        let columns = db.definition().fields_processed().iter().map(|field| field.name().to_owned()).collect::<Vec<_>>();
79        self.add_rows(&name, &columns, &db.data())
80    }
81
82    /// Adds rows to a table, creating it if needed.
83    ///
84    /// # Arguments
85    ///
86    /// * `name` - Table name, without the `_tables` suffix.
87    /// * `columns` - Column names of the rows.
88    /// * `rows` - Rows, with their values in `columns` order.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if the table already exists and `columns` lacks one of its columns.
93    pub fn add_rows<S: AsRef<str>>(&mut self, name: &str, columns: &[S], rows: &[Vec<DecodedData>]) -> Result<()> {
94        let table = self.tables.entry(name.to_owned()).or_insert_with(|| Table {
95            name: name.to_owned(),
96            columns: columns.iter().enumerate().map(|(index, column)| (column.as_ref().to_owned(), index)).collect(),
97            rows: vec![],
98        });
99
100        // Fragments may order their columns differently, so rows are rearranged into the table's order.
101        let mut order = vec![0; table.columns.len()];
102        for (column, &target) in &table.columns {
103            order[target] = columns.iter().position(|other| other.as_ref() == column)
104                .ok_or_else(|| RLibError::StartposMissingColumn(name.to_owned(), column.to_owned()))?;
105        }
106
107        table.rows.extend(rows.iter().map(|row| order.iter().map(|&source| row[source].clone()).collect()));
108        Ok(())
109    }
110
111    /// Adds a column to a table that lacks it, with the same value in every row. Does nothing if the table has the
112    /// column or doesn't exist.
113    pub fn add_missing_column(&mut self, name: &str, column: &str, value: DecodedData) {
114        let Some(table) = self.tables.get_mut(name) else { return };
115        if table.columns.contains_key(column) {
116            return;
117        }
118        table.columns.insert(column.to_owned(), table.columns.len());
119        for row in &mut table.rows {
120            row.push(value.clone());
121        }
122    }
123
124    /// Returns a table by name.
125    ///
126    /// # Errors
127    ///
128    /// Returns an error if the table hasn't been added.
129    pub fn table(&self, name: &str) -> Result<&Table> {
130        self.tables.get(name).ok_or_else(|| RLibError::StartposMissingTable(name.to_owned()))
131    }
132}
133
134impl Table {
135
136    /// Returns the table name.
137    pub fn name(&self) -> &str {
138        &self.name
139    }
140
141    /// Returns the rows of the table, in order.
142    pub fn rows(&self) -> impl Iterator<Item = Row<'_>> {
143        self.rows.iter().map(|data| Row { table: self, data })
144    }
145
146    /// Returns the rows of the table in the order of the game's key lookup table, given the key column.
147    ///
148    /// The game indexes each table by key in an open addressing table with one slot per row (13 if empty): a
149    /// row goes to the slot [`key_hash`] modulo the row count gives, or the next free one. Code iterating that
150    /// index visits the rows in slot order.
151    ///
152    /// # Errors
153    ///
154    /// Returns an error if the table has no such column.
155    pub fn rows_in_key_order(&self, column: &str) -> Result<Vec<Row<'_>>> {
156        self.rows_in_key_order_by(|row| Ok(row.str(column)?.into_owned()))
157    }
158
159    /// Returns the rows of the table in the order of the game's key lookup table, given each row's key (tables with
160    /// several key columns join their values, as in `province` + `region`). See [`Self::rows_in_key_order`].
161    ///
162    /// # Errors
163    ///
164    /// Returns an error if the key of a row can't be built.
165    pub fn rows_in_key_order_by(&self, key: impl Fn(&Row<'_>) -> Result<String>) -> Result<Vec<Row<'_>>> {
166        let keys = self.rows().map(|row| key(&row)).collect::<Result<Vec<_>>>()?;
167        let mut slots: Vec<Option<usize>> = vec![None; keys.len().max(1)];
168        for (row, key) in keys.iter().enumerate() {
169            let mut slot = key_hash(key.as_bytes()) as usize % slots.len();
170            while slots[slot].is_some() {
171                slot = (slot + 1) % slots.len();
172            }
173            slots[slot] = Some(row);
174        }
175        Ok(slots.into_iter().flatten().map(|row| Row { table: self, data: &self.rows[row] }).collect())
176    }
177
178    /// Returns the number of rows.
179    pub fn len(&self) -> usize {
180        self.rows.len()
181    }
182
183    /// Returns if the table has no rows.
184    pub fn is_empty(&self) -> bool {
185        self.rows.is_empty()
186    }
187
188    /// Returns the names of the columns, in no particular order.
189    pub fn column_names(&self) -> impl Iterator<Item = &str> {
190        self.columns.keys().map(String::as_str)
191    }
192
193    /// Returns the position of a column.
194    ///
195    /// # Errors
196    ///
197    /// Returns an error if the table has no such column.
198    pub fn column(&self, column: &str) -> Result<usize> {
199        self.columns.get(column).copied().ok_or_else(|| RLibError::StartposMissingColumn(self.name.to_owned(), column.to_owned()))
200    }
201}
202
203/// Returns the hash the game gives a table key (`FUN_100e9730` in Thrones): the bytes from last to first, each
204/// sign-extended and xored in at a shift that cycles through 0, 8, 16 and 24 bits.
205pub fn key_hash(key: &[u8]) -> u32 {
206    key.iter().rev().zip([0, 8, 16, 24].into_iter().cycle())
207        .fold(0xf73a_51c9, |hash, (&byte, shift)| hash ^ ((byte as i8 as i32 as u32) << shift))
208}
209
210/// Returns keys in the order the game's chained hash sets iterate them, given their insertion order.
211///
212/// The set starts with [`HASH_SET_FIRST_BUCKETS`] buckets and grows to twice plus one when it holds more keys than
213/// buckets (its maximum load is 1); keys go to the end of the chain of bucket [`key_hash`] modulo the bucket count,
214/// and growing moves them bucket by bucket. Repeated keys are kept once. Verified in Thrones' DLL (constructor
215/// `FUN_10b46290`, inserts like `FUN_10b65220`).
216pub fn hash_set_order<'k>(keys: impl IntoIterator<Item = &'k str>) -> Vec<&'k str> {
217    hash_set_order_by(keys, |key| key_hash(key.as_bytes()))
218}
219
220/// Returns items in the order the game's chained hash sets iterate them, given their insertion order and their hash.
221/// See [`hash_set_order`].
222pub fn hash_set_order_by<T: PartialEq + Copy>(items: impl IntoIterator<Item = T>, hash: impl Fn(&T) -> u32) -> Vec<T> {
223    let mut buckets: Vec<Vec<T>> = vec![vec![]; HASH_SET_FIRST_BUCKETS];
224    let mut count = 0;
225    for item in items {
226        let bucket = hash(&item) as usize % buckets.len();
227        if buckets[bucket].contains(&item) {
228            continue;
229        }
230        count += 1;
231        if count > buckets.len() {
232            let mut grown = vec![vec![]; buckets.len() * 2 + 1];
233            for moved in buckets.into_iter().flatten() {
234                let bucket = hash(&moved) as usize % grown.len();
235                grown[bucket].push(moved);
236            }
237            buckets = grown;
238        }
239        let bucket = hash(&item) as usize % buckets.len();
240        buckets[bucket].push(item);
241    }
242    buckets.into_iter().flatten().collect()
243}
244
245impl<'a> Row<'a> {
246
247    /// Returns a value of the row.
248    ///
249    /// # Errors
250    ///
251    /// Returns an error if the table has no such column.
252    pub fn value(&self, column: &str) -> Result<&'a DecodedData> {
253        Ok(&self.data[self.table.column(column)?])
254    }
255
256    /// Returns a colour of the row, stored as an `RRGGBB` hex text, as its red, green and blue channels.
257    ///
258    /// # Errors
259    ///
260    /// Returns an error if the table has no such column, or the value isn't an `RRGGBB` hex text.
261    pub fn colour(&self, column: &str) -> Result<[u8; 3]> {
262        let hex = self.str(column)?;
263        let invalid = || RLibError::StartposInvalidValue(self.table.name.to_owned(), column.to_owned(), hex.to_string());
264        if hex.len() != 6 {
265            return Err(invalid());
266        }
267
268        let mut colour = [0; 3];
269        for (channel, component) in colour.iter_mut().enumerate() {
270            *component = u8::from_str_radix(hex.get(channel * 2..channel * 2 + 2).ok_or_else(invalid)?, 16).map_err(|_| invalid())?;
271        }
272        Ok(colour)
273    }
274
275    /// Returns a value of the row as text (numbers and booleans are formatted).
276    ///
277    /// # Errors
278    ///
279    /// Returns an error if the table has no such column.
280    pub fn str(&self, column: &str) -> Result<Cow<'a, str>> {
281        Ok(self.value(column)?.data_to_string())
282    }
283
284    /// Returns a boolean value of the row.
285    ///
286    /// # Errors
287    ///
288    /// Returns an error if the table has no such column, or the value isn't a boolean.
289    pub fn bool(&self, column: &str) -> Result<bool> {
290        match self.value(column)? {
291            DecodedData::Boolean(value) => Ok(*value),
292            other => Err(self.invalid(column, other)),
293        }
294    }
295
296    /// Returns an integer value of the row.
297    ///
298    /// # Errors
299    ///
300    /// Returns an error if the table has no such column, or the value isn't an integer.
301    pub fn int(&self, column: &str) -> Result<i64> {
302        match self.value(column)? {
303            DecodedData::I16(value) | DecodedData::OptionalI16(value) => Ok(i64::from(*value)),
304            DecodedData::I32(value) | DecodedData::OptionalI32(value) => Ok(i64::from(*value)),
305            DecodedData::I64(value) | DecodedData::OptionalI64(value) => Ok(*value),
306            other => Err(self.invalid(column, other)),
307        }
308    }
309
310    /// Returns a floating point value of the row, as stored.
311    ///
312    /// # Errors
313    ///
314    /// Returns an error if the table has no such column, or the value isn't a float.
315    pub fn f32(&self, column: &str) -> Result<f32> {
316        match self.value(column)? {
317            DecodedData::F32(value) => Ok(*value),
318            DecodedData::F64(value) => Ok(*value as f32),
319            other => Err(self.invalid(column, other)),
320        }
321    }
322
323    /// Returns a numeric value of the row (integer or float) as a float.
324    ///
325    /// # Errors
326    ///
327    /// Returns an error if the table has no such column, or the value isn't a number.
328    pub fn number(&self, column: &str) -> Result<f64> {
329        match self.value(column)? {
330            DecodedData::F32(value) => Ok(f64::from(*value)),
331            DecodedData::F64(value) => Ok(*value),
332            _ => self.int(column).map(|value| value as f64),
333        }
334    }
335
336    /// Builds the error for a value of an unexpected type.
337    fn invalid(&self, column: &str, value: &DecodedData) -> RLibError {
338        RLibError::StartposInvalidValue(self.table.name.to_owned(), column.to_owned(), format!("{value:?}"))
339    }
340}