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}