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}