Skip to main content

rpfm_server/state/
schema.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//! Schema operations: definitions, local patches, and updating the schema from the Assembly Kit.
12
13use anyhow::Result;
14use itertools::Itertools;
15use rayon::prelude::*;
16
17use std::collections::HashMap;
18
19use rpfm_ipc::api::ApiError;
20use rpfm_ipc::api::schema::{DeleteDefinition, GetReferencingColumns, GetTablePatches, ImportPatches, PATCH_KEYS, PatchColumn, RawDefinitions, ReferencingColumns, RemovePatches, SchemaTables, SetDefinition, TablePatches};
21use rpfm_ipc::api::tables::{GetTableDefinition, TableDefinition};
22
23use rpfm_lib::files::{Container, db::DB, FileType, RFileDecoded};
24use rpfm_lib::integrations::assembly_kit::update_schema_from_raw_files;
25use rpfm_lib::schema::{Definition, DefinitionPatch, Schema};
26
27use rpfm_ipc::settings::{schemas_path, table_patches_path, Settings};
28
29use super::{SessionState, load_schema, loaded_schema, loaded_schema_mut};
30
31impl SessionState {
32
33    /// Updates the schema of the selected game with the table definitions of its Assembly Kit, and saves it.
34    ///
35    /// # Arguments
36    ///
37    /// * `settings` - Settings, to find the game's Assembly Kit.
38    /// * `ignore_game_files_in_ak` - If tables in the game files are skipped.
39    /// * `disable_uuid_regeneration` - If tables keep their GUID when re-encoded to reload the schema.
40    pub fn update_schema_from_asskit(&mut self, settings: &Settings, ignore_game_files_in_ak: bool, disable_uuid_regeneration: bool) -> Result<()> {
41        let schema = loaded_schema_mut(&mut self.schema)?;
42        let asskit_path = settings.assembly_kit_path(&self.game)?;
43        let schema_path = schemas_path()?.join(self.game.schema_file_name());
44
45        // If there are packs open, also add the packs' tables to it. That way we can treat some special tables, like starpos tables.
46        let mut tables_to_check = self.dependencies.db_and_loc_data(true, false, true, false)?;
47        for pack in self.packs.values() {
48            if !pack.disk_file_path().is_empty() {
49                tables_to_check.append(&mut pack.files_by_type(&[FileType::DB]));
50            }
51        }
52
53        // Split the tables by name, merging tables of the same name and version, so we got more chances of loc data being found.
54        let mut tables_to_check_split: HashMap<String, Vec<DB>> = HashMap::new();
55        for table_to_check in tables_to_check {
56            if let Ok(RFileDecoded::DB(table)) = table_to_check.decoded() {
57                let tables = tables_to_check_split.entry(table.table_name().to_owned()).or_default();
58                match tables.iter_mut().find(|source| source.definition().version() == table.definition().version()) {
59                    Some(db_source) => *db_source = DB::merge(&[db_source, table])?,
60                    None => tables.push(table.clone()),
61                }
62            }
63        }
64
65        let tables_to_skip = if ignore_game_files_in_ak {
66            self.dependencies.vanilla_loose_tables().keys().chain(self.dependencies.vanilla_tables().keys()).map(|name| &**name).collect::<Vec<_>>()
67        } else {
68            vec![]
69        };
70
71        let possible_loc_fields = update_schema_from_raw_files(schema, &self.game, &asskit_path, &schema_path, &tables_to_skip, &tables_to_check_split)?;
72
73        // The update deletes all loc fields, so we need to get them again from the TExc_LocalisableFields.xml, if said file exists.
74        // That's why it does the update again, to re-populate the loc fields list with the ones not bruteforced.
75        let local_packs = if self.packs.is_empty() { None } else { Some(&self.packs) };
76        if self.dependencies.bruteforce_loc_key_order(schema, possible_loc_fields, local_packs, None).is_err() {
77            return Ok(());
78        }
79
80        let _ = update_schema_from_raw_files(schema, &self.game, &asskit_path, &schema_path, &tables_to_skip, &tables_to_check_split);
81
82        // This generates the automatic patches in the schema (like ".png are files" kinda patches).
83        if self.dependencies.generate_automatic_patches(schema, &self.packs).is_err() {
84            return Ok(());
85        }
86
87        // Fix for old file relative paths using incorrect separators.
88        schema.definitions_mut().par_iter_mut().for_each(|(_, definitions)| {
89            definitions.iter_mut().for_each(|definition| {
90                definition.edit_fields(|fields| fields.iter_mut().for_each(|field| {
91                    if let Some(path) = field.filename_relative_path(None) {
92                        if path.len() == 1 && path[0].contains(',') {
93                            let new_paths = path[0].split(',').map(|path| path.trim()).join(";");
94                            field.set_filename_relative_path(Some(new_paths));
95                        }
96                    }
97                }));
98            });
99        });
100
101        schema.save(&schema_path)?;
102
103        // The update clears the definitions' patches, so reload the saved schema to apply them again.
104        load_schema(&mut self.schema, &mut self.packs, &self.game, disable_uuid_regeneration);
105        Ok(())
106    }
107
108    /// Reloads the schema of the selected game from disk, re-decoding the tables of the open packs.
109    ///
110    /// # Arguments
111    ///
112    /// * `disable_uuid_regeneration` - If tables keep their GUID when re-encoded.
113    pub fn reload_schema(&mut self, disable_uuid_regeneration: bool) {
114        load_schema(&mut self.schema, &mut self.packs, &self.game, disable_uuid_regeneration);
115    }
116
117    /// Saves a schema to disk as the schema of the selected game, and loads it.
118    pub fn save_schema(&mut self, mut schema: Schema) -> Result<()> {
119        schema.save(&schemas_path()?.join(self.game.schema_file_name()))?;
120        self.schema = Some(schema);
121        Ok(())
122    }
123
124    /// Saves local patches for the schema of the selected game.
125    pub fn save_local_schema_patches(&self, patches: &HashMap<String, DefinitionPatch>) -> Result<()> {
126        Schema::save_patches(patches, &table_patches_path()?.join(self.game.schema_file_name()))?;
127        Ok(())
128    }
129
130    /// Removes the local schema patches of a table.
131    pub fn remove_local_schema_patches_for_table(&self, table_name: &str) -> Result<()> {
132        Schema::remove_patches_for_table(table_name, &table_patches_path()?.join(self.game.schema_file_name()))?;
133        Ok(())
134    }
135
136    /// Removes the local schema patches of a field of a table.
137    pub fn remove_local_schema_patches_for_table_and_field(&self, table_name: &str, field_name: &str) -> Result<()> {
138        Schema::remove_patches_for_table_and_field(table_name, field_name, &table_patches_path()?.join(self.game.schema_file_name()))?;
139        Ok(())
140    }
141
142    /// Adds patches to the schema of the selected game, and saves it.
143    pub fn import_schema_patches(&mut self, patches: &HashMap<String, DefinitionPatch>) -> Result<()> {
144        let schema = loaded_schema_mut(&mut self.schema)?;
145        Schema::add_patches_to_patch_set(schema.patches_mut(), patches);
146        schema.save(&schemas_path()?.join(self.game.schema_file_name()))?;
147        Ok(())
148    }
149
150    /// Returns the names of the startpos and twad tables in the schema.
151    pub fn custom_table_names(&self) -> Result<Vec<String>> {
152        Ok(loaded_schema(&self.schema)?.definitions().par_iter()
153            .filter(|(key, definitions)| !definitions.is_empty() && (key.starts_with("start_pos_") || key.starts_with("twad_")))
154            .map(|(key, _)| key.to_owned())
155            .collect())
156    }
157
158    /// Returns the definition of a table with a specific version.
159    ///
160    /// # Arguments
161    ///
162    /// * `table_name` - Name of the table, like `units_tables`.
163    /// * `version` - Version of the definition.
164    ///
165    /// # Returns
166    ///
167    /// The definition from the schema or, if the schema doesn't have it, the one of the Assembly Kit table with that name and version.
168    ///
169    /// # Errors
170    ///
171    /// Fails if there is no schema, or neither it nor the Assembly Kit tables have the definition.
172    pub fn definition_by_name_and_version(&self, table_name: &str, version: i32) -> Result<&Definition> {
173        loaded_schema(&self.schema)?.definition_by_name_and_version(table_name, version)
174            .or_else(|| self.dependencies.asskit_only_db_tables().get(table_name)
175                .map(|table| table.definition())
176                .filter(|definition| *definition.version() == version))
177            .ok_or_else(|| ApiError::DefinitionNotFound(table_name.to_owned()).into())
178    }
179
180    /// Returns the columns of a table as defined in the schema, or in the Assembly Kit for its tables.
181    ///
182    /// If the request has no version, the version of the table in the game files is used, or the newest one if there is none.
183    pub fn table_definition(&self, request: &GetTableDefinition) -> Result<TableDefinition> {
184        let schema = loaded_schema(&self.schema)?;
185        let table_name = &request.table_name;
186        let definition = match request.version {
187            Some(version) => self.definition_by_name_and_version(table_name, version)?,
188            None => self.dependencies.db_version(table_name)
189                .and_then(|version| schema.definition_by_name_and_version(table_name, version))
190                .or_else(|| schema.definitions_by_table_name(table_name).and_then(|definitions| definitions.first()))
191                .ok_or_else(|| ApiError::DefinitionNotFound(table_name.to_owned()))?,
192        };
193
194        Ok(TableDefinition {
195            table_name: table_name.to_owned(),
196            version: *definition.version(),
197            columns: definition.fields_processed().to_vec(),
198        })
199    }
200
201    /// Returns the tables of the schema whose name starts with a prefix, with the versions they have definitions for.
202    pub fn schema_tables(&self, prefix: &str) -> Result<SchemaTables> {
203        let tables = loaded_schema(&self.schema)?.definitions().iter()
204            .filter(|(table_name, definitions)| table_name.starts_with(prefix) && !definitions.is_empty())
205            .map(|(table_name, definitions)| (table_name.to_owned(), definitions.iter().map(|definition| *definition.version()).collect()))
206            .collect();
207
208        Ok(SchemaTables { tables })
209    }
210
211    /// Saves a local patch for a column of a table, and reloads the schema so it applies.
212    ///
213    /// # Arguments
214    ///
215    /// * `request` - The patch.
216    /// * `disable_uuid_regeneration` - If tables keep their GUID when re-encoded to reload the schema.
217    ///
218    /// # Errors
219    ///
220    /// Fails if a key of the patch is not a valid one, or if the patch can't be saved.
221    pub fn patch_column(&mut self, request: &PatchColumn, disable_uuid_regeneration: bool) -> Result<()> {
222        if let Some(key) = request.patch.keys().find(|key| !PATCH_KEYS.iter().any(|(valid_key, _)| valid_key == key)) {
223            let valid_keys = PATCH_KEYS.iter().map(|(key, _)| *key).collect::<Vec<_>>().join(", ");
224            return Err(ApiError::InvalidParams(format!("Unknown patch key: {key}. Valid ones: {valid_keys}.")).into());
225        }
226
227        let column_patch = request.patch.iter().map(|(key, value)| (key.clone(), value.clone())).collect();
228        let patches = HashMap::from([(request.table_name.clone(), HashMap::from([(request.column.clone(), column_patch)]))]);
229        self.save_local_schema_patches(&patches)?;
230        self.reload_schema(disable_uuid_regeneration);
231        Ok(())
232    }
233
234    /// Removes the local patches of a table, or of one of its columns, and reloads the schema.
235    ///
236    /// # Arguments
237    ///
238    /// * `request` - Patches to remove.
239    /// * `disable_uuid_regeneration` - If tables keep their GUID when re-encoded to reload the schema.
240    pub fn remove_patches(&mut self, request: &RemovePatches, disable_uuid_regeneration: bool) -> Result<()> {
241        match request.column {
242            Some(ref column) => self.remove_local_schema_patches_for_table_and_field(&request.table_name, column)?,
243            None => self.remove_local_schema_patches_for_table(&request.table_name)?,
244        }
245
246        self.reload_schema(disable_uuid_regeneration);
247        Ok(())
248    }
249
250    /// Returns the definitions of a table as the schema stores them, newest first.
251    pub fn raw_definitions(&self, table_name: &str, version: Option<i32>) -> Result<RawDefinitions> {
252        let definitions = self.definitions_by_table_name(table_name)?.iter()
253            .filter(|definition| version.is_none_or(|version| *definition.version() == version))
254            .map(|definition| serde_json::to_value(definition.stored()))
255            .collect::<Result<Vec<_>, _>>()?;
256
257        Ok(RawDefinitions { definitions })
258    }
259
260    /// Adds a definition to the schema, or replaces the one with its version, then saves and reloads the schema.
261    ///
262    /// # Arguments
263    ///
264    /// * `request` - The definition.
265    /// * `disable_uuid_regeneration` - If tables keep their GUID when re-encoded to reload the schema.
266    pub fn set_definition(&mut self, request: &SetDefinition, disable_uuid_regeneration: bool) -> Result<()> {
267        let definition = serde_json::from_value::<Definition>(request.definition.clone())
268            .map_err(|error| ApiError::InvalidParams(format!("Invalid definition: {error}")))?;
269
270        let mut schema = loaded_schema(&self.schema)?.clone();
271        schema.add_definition(&request.table_name, &definition);
272        self.save_schema(schema)?;
273        self.reload_schema(disable_uuid_regeneration);
274        Ok(())
275    }
276
277    /// Removes a definition from the schema, then saves and reloads the schema.
278    ///
279    /// # Arguments
280    ///
281    /// * `request` - The definition to remove.
282    /// * `disable_uuid_regeneration` - If tables keep their GUID when re-encoded to reload the schema.
283    pub fn delete_definition_and_save(&mut self, request: &DeleteDefinition, disable_uuid_regeneration: bool) -> Result<()> {
284        let mut schema = loaded_schema(&self.schema)?.clone();
285        schema.remove_definition(&request.table_name, request.version);
286        self.save_schema(schema)?;
287        self.reload_schema(disable_uuid_regeneration);
288        Ok(())
289    }
290
291    /// Returns the columns of other tables referencing each column of a table.
292    pub fn referencing_columns_of(&self, request: &GetReferencingColumns) -> Result<ReferencingColumns> {
293        let table_name = &request.table_name;
294        let version = match request.version {
295            Some(version) => version,
296            None => self.table_definition(&GetTableDefinition { table_name: table_name.to_owned(), version: None })?.version,
297        };
298
299        let definition = self.definition_by_name_and_version(table_name, version)?;
300        let columns = self.referencing_columns(table_name, definition)?.into_iter()
301            .map(|(column, tables)| (column, tables.into_iter().collect()))
302            .collect();
303
304        Ok(ReferencingColumns { columns })
305    }
306
307    /// Adds patches to the schema itself, saves it, and reloads it.
308    ///
309    /// # Arguments
310    ///
311    /// * `request` - The patches.
312    /// * `disable_uuid_regeneration` - If tables keep their GUID when re-encoded to reload the schema.
313    pub fn import_patches(&mut self, request: &ImportPatches, disable_uuid_regeneration: bool) -> Result<()> {
314        let patches = request.patches.iter()
315            .map(|(table, columns)| (table.clone(), columns.iter()
316                .map(|(column, patch)| (column.clone(), patch.iter().map(|(key, value)| (key.clone(), value.clone())).collect()))
317                .collect()))
318            .collect::<HashMap<String, DefinitionPatch>>();
319
320        self.import_schema_patches(&patches)?;
321        self.reload_schema(disable_uuid_regeneration);
322        Ok(())
323    }
324
325    /// Returns all the definitions of a table. Empty if the table is not in the schema.
326    pub fn definitions_by_table_name(&self, table_name: &str) -> Result<Vec<Definition>> {
327        Ok(loaded_schema(&self.schema)?.definitions_by_table_name(table_name)
328            .map(|definitions| definitions.to_vec())
329            .unwrap_or_default())
330    }
331
332    /// Returns the patches of a definition of a table, or of the table if the definition doesn't exist.
333    pub fn definition_patches(&self, table_name: &str, version: i32) -> Result<DefinitionPatch> {
334        let schema = loaded_schema(&self.schema)?;
335        Ok(match schema.definition_by_name_and_version(table_name, version) {
336            Some(definition) => definition.patches().clone(),
337            None => schema.patches().get(table_name).cloned().unwrap_or_default(),
338        })
339    }
340
341    /// Returns the patches of a definition of a table, by column name.
342    pub fn table_patches(&self, request: &GetTablePatches) -> Result<TablePatches> {
343        let patches = self.definition_patches(&request.table_name, request.version)?.into_iter()
344            .map(|(column, patch)| (column, patch.into_iter().collect()))
345            .collect();
346
347        Ok(TablePatches { patches })
348    }
349
350    /// Returns the columns of other tables referencing a table, by table name and column name.
351    pub fn referencing_columns(&self, table_name: &str, definition: &Definition) -> Result<HashMap<String, HashMap<String, Vec<String>>>> {
352        Ok(loaded_schema(&self.schema)?.referencing_columns_for_table(table_name, definition))
353    }
354}