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 columns of a table as defined in the schema.
159    ///
160    /// 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.
161    pub fn table_definition(&self, request: &GetTableDefinition) -> Result<TableDefinition> {
162        let schema = loaded_schema(&self.schema)?;
163        let table_name = &request.table_name;
164        let definition = match request.version {
165            Some(version) => schema.definition_by_name_and_version(table_name, version),
166            None => self.dependencies.db_version(table_name)
167                .and_then(|version| schema.definition_by_name_and_version(table_name, version))
168                .or_else(|| schema.definitions_by_table_name(table_name).and_then(|definitions| definitions.first())),
169        }.ok_or_else(|| ApiError::DefinitionNotFound(table_name.to_owned()))?;
170
171        Ok(TableDefinition {
172            table_name: table_name.to_owned(),
173            version: *definition.version(),
174            columns: definition.fields_processed().to_vec(),
175        })
176    }
177
178    /// Returns the tables of the schema whose name starts with a prefix, with the versions they have definitions for.
179    pub fn schema_tables(&self, prefix: &str) -> Result<SchemaTables> {
180        let tables = loaded_schema(&self.schema)?.definitions().iter()
181            .filter(|(table_name, definitions)| table_name.starts_with(prefix) && !definitions.is_empty())
182            .map(|(table_name, definitions)| (table_name.to_owned(), definitions.iter().map(|definition| *definition.version()).collect()))
183            .collect();
184
185        Ok(SchemaTables { tables })
186    }
187
188    /// Saves a local patch for a column of a table, and reloads the schema so it applies.
189    ///
190    /// # Arguments
191    ///
192    /// * `request` - The patch.
193    /// * `disable_uuid_regeneration` - If tables keep their GUID when re-encoded to reload the schema.
194    ///
195    /// # Errors
196    ///
197    /// Fails if a key of the patch is not a valid one, or if the patch can't be saved.
198    pub fn patch_column(&mut self, request: &PatchColumn, disable_uuid_regeneration: bool) -> Result<()> {
199        if let Some(key) = request.patch.keys().find(|key| !PATCH_KEYS.iter().any(|(valid_key, _)| valid_key == key)) {
200            let valid_keys = PATCH_KEYS.iter().map(|(key, _)| *key).collect::<Vec<_>>().join(", ");
201            return Err(ApiError::InvalidParams(format!("Unknown patch key: {key}. Valid ones: {valid_keys}.")).into());
202        }
203
204        let column_patch = request.patch.iter().map(|(key, value)| (key.clone(), value.clone())).collect();
205        let patches = HashMap::from([(request.table_name.clone(), HashMap::from([(request.column.clone(), column_patch)]))]);
206        self.save_local_schema_patches(&patches)?;
207        self.reload_schema(disable_uuid_regeneration);
208        Ok(())
209    }
210
211    /// Removes the local patches of a table, or of one of its columns, and reloads the schema.
212    ///
213    /// # Arguments
214    ///
215    /// * `request` - Patches to remove.
216    /// * `disable_uuid_regeneration` - If tables keep their GUID when re-encoded to reload the schema.
217    pub fn remove_patches(&mut self, request: &RemovePatches, disable_uuid_regeneration: bool) -> Result<()> {
218        match request.column {
219            Some(ref column) => self.remove_local_schema_patches_for_table_and_field(&request.table_name, column)?,
220            None => self.remove_local_schema_patches_for_table(&request.table_name)?,
221        }
222
223        self.reload_schema(disable_uuid_regeneration);
224        Ok(())
225    }
226
227    /// Returns the definitions of a table as the schema stores them, newest first.
228    pub fn raw_definitions(&self, table_name: &str, version: Option<i32>) -> Result<RawDefinitions> {
229        let definitions = self.definitions_by_table_name(table_name)?.iter()
230            .filter(|definition| version.is_none_or(|version| *definition.version() == version))
231            .map(|definition| serde_json::to_value(definition.stored()))
232            .collect::<Result<Vec<_>, _>>()?;
233
234        Ok(RawDefinitions { definitions })
235    }
236
237    /// Adds a definition to the schema, or replaces the one with its version, then saves and reloads the schema.
238    ///
239    /// # Arguments
240    ///
241    /// * `request` - The definition.
242    /// * `disable_uuid_regeneration` - If tables keep their GUID when re-encoded to reload the schema.
243    pub fn set_definition(&mut self, request: &SetDefinition, disable_uuid_regeneration: bool) -> Result<()> {
244        let definition = serde_json::from_value::<Definition>(request.definition.clone())
245            .map_err(|error| ApiError::InvalidParams(format!("Invalid definition: {error}")))?;
246
247        let mut schema = loaded_schema(&self.schema)?.clone();
248        schema.add_definition(&request.table_name, &definition);
249        self.save_schema(schema)?;
250        self.reload_schema(disable_uuid_regeneration);
251        Ok(())
252    }
253
254    /// Removes a definition from the schema, then saves and reloads the schema.
255    ///
256    /// # Arguments
257    ///
258    /// * `request` - The definition to remove.
259    /// * `disable_uuid_regeneration` - If tables keep their GUID when re-encoded to reload the schema.
260    pub fn delete_definition_and_save(&mut self, request: &DeleteDefinition, disable_uuid_regeneration: bool) -> Result<()> {
261        let mut schema = loaded_schema(&self.schema)?.clone();
262        schema.remove_definition(&request.table_name, request.version);
263        self.save_schema(schema)?;
264        self.reload_schema(disable_uuid_regeneration);
265        Ok(())
266    }
267
268    /// Returns the columns of other tables referencing each column of a table.
269    pub fn referencing_columns_of(&self, request: &GetReferencingColumns) -> Result<ReferencingColumns> {
270        let table_name = &request.table_name;
271        let version = match request.version {
272            Some(version) => version,
273            None => self.table_definition(&GetTableDefinition { table_name: table_name.to_owned(), version: None })?.version,
274        };
275
276        let definition = loaded_schema(&self.schema)?.definition_by_name_and_version(table_name, version)
277            .ok_or_else(|| ApiError::DefinitionNotFound(table_name.to_owned()))?;
278
279        let columns = self.referencing_columns(table_name, definition)?.into_iter()
280            .map(|(column, tables)| (column, tables.into_iter().collect()))
281            .collect();
282
283        Ok(ReferencingColumns { columns })
284    }
285
286    /// Adds patches to the schema itself, saves it, and reloads it.
287    ///
288    /// # Arguments
289    ///
290    /// * `request` - The patches.
291    /// * `disable_uuid_regeneration` - If tables keep their GUID when re-encoded to reload the schema.
292    pub fn import_patches(&mut self, request: &ImportPatches, disable_uuid_regeneration: bool) -> Result<()> {
293        let patches = request.patches.iter()
294            .map(|(table, columns)| (table.clone(), columns.iter()
295                .map(|(column, patch)| (column.clone(), patch.iter().map(|(key, value)| (key.clone(), value.clone())).collect()))
296                .collect()))
297            .collect::<HashMap<String, DefinitionPatch>>();
298
299        self.import_schema_patches(&patches)?;
300        self.reload_schema(disable_uuid_regeneration);
301        Ok(())
302    }
303
304    /// Returns all the definitions of a table. Empty if the table is not in the schema.
305    pub fn definitions_by_table_name(&self, table_name: &str) -> Result<Vec<Definition>> {
306        Ok(loaded_schema(&self.schema)?.definitions_by_table_name(table_name)
307            .map(|definitions| definitions.to_vec())
308            .unwrap_or_default())
309    }
310
311    /// Returns the patches of a definition of a table, or of the table if the definition doesn't exist.
312    pub fn definition_patches(&self, table_name: &str, version: i32) -> Result<DefinitionPatch> {
313        let schema = loaded_schema(&self.schema)?;
314        Ok(match schema.definition_by_name_and_version(table_name, version) {
315            Some(definition) => definition.patches().clone(),
316            None => schema.patches().get(table_name).cloned().unwrap_or_default(),
317        })
318    }
319
320    /// Returns the patches of a definition of a table, by column name.
321    pub fn table_patches(&self, request: &GetTablePatches) -> Result<TablePatches> {
322        let patches = self.definition_patches(&request.table_name, request.version)?.into_iter()
323            .map(|(column, patch)| (column, patch.into_iter().collect()))
324            .collect();
325
326        Ok(TablePatches { patches })
327    }
328
329    /// Returns the columns of other tables referencing a table, by table name and column name.
330    pub fn referencing_columns(&self, table_name: &str, definition: &Definition) -> Result<HashMap<String, HashMap<String, Vec<String>>>> {
331        Ok(loaded_schema(&self.schema)?.referencing_columns_for_table(table_name, definition))
332    }
333}