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//---------------------------------------------------------------------------//
27//                              Types
28//---------------------------------------------------------------------------//
29
30/// The tables available to the startpos build, by table name (without the `_tables` suffix).
31#[derive(Debug, Clone, Default)]
32pub struct StartposTables {
33    tables: HashMap<String, Table>,
34}
35
36/// A table: its columns and the rows of every fragment.
37#[derive(Debug, Clone, PartialEq)]
38pub struct Table {
39
40    /// Table name, without the `_tables` suffix.
41    name: String,
42
43    /// Column positions, by name.
44    columns: HashMap<String, usize>,
45
46    /// Rows, in fragment order and then in file order.
47    rows: Vec<Vec<DecodedData>>,
48}
49
50/// A row of a table.
51#[derive(Debug, Clone, Copy)]
52pub struct Row<'a> {
53    table: &'a Table,
54    data: &'a [DecodedData],
55}
56
57//---------------------------------------------------------------------------//
58//                              Implementations
59//---------------------------------------------------------------------------//
60
61impl StartposTables {
62
63    /// Creates an empty table set.
64    pub fn new() -> Self {
65        Self::default()
66    }
67
68    /// Adds the rows of a DB fragment. Fragments of the same table are merged in the order they're added.
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if a later fragment lacks a column of the first fragment of its table.
73    pub fn add_db(&mut self, db: &DB) -> Result<()> {
74        let name = db.table_name_without_tables();
75        let columns = db.definition().fields_processed().iter().map(|field| field.name().to_owned()).collect::<Vec<_>>();
76        self.add_rows(&name, &columns, &db.data())
77    }
78
79    /// Adds rows to a table, creating it if needed.
80    ///
81    /// # Arguments
82    ///
83    /// * `name` - Table name, without the `_tables` suffix.
84    /// * `columns` - Column names of the rows.
85    /// * `rows` - Rows, with their values in `columns` order.
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if the table already exists and `columns` lacks one of its columns.
90    pub fn add_rows<S: AsRef<str>>(&mut self, name: &str, columns: &[S], rows: &[Vec<DecodedData>]) -> Result<()> {
91        let table = self.tables.entry(name.to_owned()).or_insert_with(|| Table {
92            name: name.to_owned(),
93            columns: columns.iter().enumerate().map(|(index, column)| (column.as_ref().to_owned(), index)).collect(),
94            rows: vec![],
95        });
96
97        // Fragments may order their columns differently, so rows are rearranged into the table's order.
98        let mut order = vec![0; table.columns.len()];
99        for (column, &target) in &table.columns {
100            order[target] = columns.iter().position(|other| other.as_ref() == column)
101                .ok_or_else(|| RLibError::StartposMissingColumn(name.to_owned(), column.to_owned()))?;
102        }
103
104        table.rows.extend(rows.iter().map(|row| order.iter().map(|&source| row[source].clone()).collect()));
105        Ok(())
106    }
107
108    /// Returns a table by name.
109    ///
110    /// # Errors
111    ///
112    /// Returns an error if the table hasn't been added.
113    pub fn table(&self, name: &str) -> Result<&Table> {
114        self.tables.get(name).ok_or_else(|| RLibError::StartposMissingTable(name.to_owned()))
115    }
116}
117
118impl Table {
119
120    /// Returns the table name.
121    pub fn name(&self) -> &str {
122        &self.name
123    }
124
125    /// Returns the rows of the table, in order.
126    pub fn rows(&self) -> impl Iterator<Item = Row<'_>> {
127        self.rows.iter().map(|data| Row { table: self, data })
128    }
129
130    /// Returns the number of rows.
131    pub fn len(&self) -> usize {
132        self.rows.len()
133    }
134
135    /// Returns if the table has no rows.
136    pub fn is_empty(&self) -> bool {
137        self.rows.is_empty()
138    }
139
140    /// Returns the names of the columns, in no particular order.
141    pub fn column_names(&self) -> impl Iterator<Item = &str> {
142        self.columns.keys().map(String::as_str)
143    }
144
145    /// Returns the position of a column.
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if the table has no such column.
150    pub fn column(&self, column: &str) -> Result<usize> {
151        self.columns.get(column).copied().ok_or_else(|| RLibError::StartposMissingColumn(self.name.to_owned(), column.to_owned()))
152    }
153}
154
155impl<'a> Row<'a> {
156
157    /// Returns a value of the row.
158    ///
159    /// # Errors
160    ///
161    /// Returns an error if the table has no such column.
162    pub fn value(&self, column: &str) -> Result<&'a DecodedData> {
163        Ok(&self.data[self.table.column(column)?])
164    }
165
166    /// Returns a colour of the row, stored as an `RRGGBB` hex text, as its red, green and blue channels.
167    ///
168    /// # Errors
169    ///
170    /// Returns an error if the table has no such column, or the value isn't an `RRGGBB` hex text.
171    pub fn colour(&self, column: &str) -> Result<[u8; 3]> {
172        let hex = self.str(column)?;
173        let invalid = || RLibError::StartposInvalidValue(self.table.name.to_owned(), column.to_owned(), hex.to_string());
174        if hex.len() != 6 {
175            return Err(invalid());
176        }
177
178        let mut colour = [0; 3];
179        for (channel, component) in colour.iter_mut().enumerate() {
180            *component = u8::from_str_radix(hex.get(channel * 2..channel * 2 + 2).ok_or_else(invalid)?, 16).map_err(|_| invalid())?;
181        }
182        Ok(colour)
183    }
184
185    /// Returns a value of the row as text (numbers and booleans are formatted).
186    ///
187    /// # Errors
188    ///
189    /// Returns an error if the table has no such column.
190    pub fn str(&self, column: &str) -> Result<Cow<'a, str>> {
191        Ok(self.value(column)?.data_to_string())
192    }
193
194    /// Returns a boolean value of the row.
195    ///
196    /// # Errors
197    ///
198    /// Returns an error if the table has no such column, or the value isn't a boolean.
199    pub fn bool(&self, column: &str) -> Result<bool> {
200        match self.value(column)? {
201            DecodedData::Boolean(value) => Ok(*value),
202            other => Err(self.invalid(column, other)),
203        }
204    }
205
206    /// Returns an integer value of the row.
207    ///
208    /// # Errors
209    ///
210    /// Returns an error if the table has no such column, or the value isn't an integer.
211    pub fn int(&self, column: &str) -> Result<i64> {
212        match self.value(column)? {
213            DecodedData::I16(value) | DecodedData::OptionalI16(value) => Ok(i64::from(*value)),
214            DecodedData::I32(value) | DecodedData::OptionalI32(value) => Ok(i64::from(*value)),
215            DecodedData::I64(value) | DecodedData::OptionalI64(value) => Ok(*value),
216            other => Err(self.invalid(column, other)),
217        }
218    }
219
220    /// Returns a floating point value of the row, as stored.
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if the table has no such column, or the value isn't a float.
225    pub fn f32(&self, column: &str) -> Result<f32> {
226        match self.value(column)? {
227            DecodedData::F32(value) => Ok(*value),
228            DecodedData::F64(value) => Ok(*value as f32),
229            other => Err(self.invalid(column, other)),
230        }
231    }
232
233    /// Returns a numeric value of the row (integer or float) as a float.
234    ///
235    /// # Errors
236    ///
237    /// Returns an error if the table has no such column, or the value isn't a number.
238    pub fn number(&self, column: &str) -> Result<f64> {
239        match self.value(column)? {
240            DecodedData::F32(value) => Ok(f64::from(*value)),
241            DecodedData::F64(value) => Ok(*value),
242            _ => self.int(column).map(|value| value as f64),
243        }
244    }
245
246    /// Builds the error for a value of an unexpected type.
247    fn invalid(&self, column: &str, value: &DecodedData) -> RLibError {
248        RLibError::StartposInvalidValue(self.table.name.to_owned(), column.to_owned(), format!("{value:?}"))
249    }
250}