Skip to main content

rpfm_extensions/dependencies/
mod.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//! Dependencies management system for Total War modding.
12//!
13//! This module provides a comprehensive system for managing and querying dependencies
14//! between mods and vanilla game files. It serves as a central cache for all reference
15//! data needed when editing DB tables, running diagnostics, or performing other
16//! operations that require knowledge of game data.
17//!
18//! # Overview
19//!
20//! The [`Dependencies`] struct is the core of this module. It manages three categories
21//! of data:
22//!
23//! 1. **Vanilla Files**: Data from the game's official PackFiles, cached on disk for
24//!    fast loading. This includes all DB tables, Loc files, and other resources.
25//!
26//! 2. **Vanilla Loose Files**: Files in the game's `/data` folder that aren't packed,
27//!    such as user-placed mods or extracted files.
28//!
29//! 3. **Parent Mod Files**: Files from mods that the current pack depends on,
30//!    loaded recursively based on the pack's dependency list.
31//!
32//! # Cache Structure
33//!
34//! The dependencies cache is stored on disk in three `.pak` files for efficient
35//! parallel loading:
36//!
37//! - `.pak1`: Build metadata and half of the vanilla files
38//! - `.pak2`: Other half of vanilla files
39//! - `.pak3`: Table/Loc indices, folder structure, and Assembly Kit tables
40//!
41//! The cache is versioned and includes a build date to automatically invalidate
42//! when game files change or RPFM is updated.
43//!
44//! # Reference Data
45//!
46//! A key feature is building reference data for DB table foreign key relationships.
47//! When a column references another table (e.g., `unit_key` referencing `main_units`),
48//! the dependencies system provides:
49//!
50//! - All valid values for the referenced column
51//! - Lookup data for displaying human-readable names
52//! - Detection of Assembly Kit-only tables
53//! - Localised column value resolution
54//!
55//! # Assembly Kit Integration
56//!
57//! Some tables exist only in the Assembly Kit and not in game files. These are
58//! processed separately and stored in the cache as "asskit-only" tables. They're
59//! useful for reference lookups but shouldn't be used as templates for new tables.
60//!
61//! # Usage Example
62//!
63//! ```ignore
64//! use rpfm_extensions::dependencies::Dependencies;
65//! use rpfm_lib::schema::Schema;
66//!
67//! // Load or generate dependencies cache
68//! let mut deps = Dependencies::default();
69//! deps.rebuild(
70//!     &Some(schema),
71//!     &["parent_mod.pack".to_string()],
72//!     Some(cache_path),
73//!     &game_info,
74//!     game_path,
75//!     secondary_path,
76//! )?;
77//!
78//! // Query a specific file
79//! let file = deps.file("db/units_tables/data__", true, true, false)?;
80//!
81//! // Get all DB tables of a specific type
82//! let units = deps.db_data("units_tables", true, true)?;
83//! ```
84//!
85//! # Startpos Generation
86//!
87//! This module provides functions to build startpos files for campaign mods:
88//!
89//! - `build_starpos_pre`: Prepares and launches the game with a user script that
90//!   triggers startpos generation for the specified campaign
91//! - `build_starpos_post`: Called after the game closes to import the generated
92//!   startpos file back into the pack
93//!
94//! The process handles game-specific quirks like different output paths, victory
95//! objectives extraction, and HLP/SPD data generation.
96
97use getset::{Getters, MutGetters};
98use itertools::{Either, Itertools};
99use log::{info, error};
100use rayon::prelude::*;
101use serde_derive::{Serialize, Deserialize};
102
103use std::borrow::Cow;
104use std::collections::{BTreeMap, HashMap, HashSet};
105use std::fs::{DirBuilder, File};
106use std::io::{BufReader, BufWriter, Read, Write};
107use std::sync::mpsc::channel;
108use std::path::{Path, PathBuf};
109use std::process::Command;
110use std::{thread, thread::{spawn, JoinHandle}};
111use std::time::Duration;
112
113use rpfm_lib::binary::WriteBytes;
114use rpfm_lib::error::{Result, RLibError};
115use rpfm_lib::files::{Container, ContainerPath, db::DB, DecodeableExtraData, FileType, pack::Pack, RFile, RFileDecoded, table::Table};
116use rpfm_lib::games::{GameInfo, supported_games::*};
117use rpfm_lib::integrations::assembly_kit::table_data::RawTable;
118use rpfm_lib::schema::{Definition, DefinitionPatch, Field, FieldType, Schema};
119use rpfm_lib::utils::{current_time, files_from_subdir, last_modified_time_from_files, starts_with_case_insensitive};
120
121use crate::optimizer::{OptimizableContainer, OptimizerOptions};
122use crate::START_POS_WORKAROUND_THREAD;
123use crate::VERSION;
124
125/// Table name for the key deletes table used in datacoring.
126///
127/// The `twad_key_deletes` table is used to remove specific rows from vanilla tables
128/// without replacing the entire table. This is the preferred method for removing
129/// vanilla content as it's more compatible with other mods.
130pub const KEY_DELETES_TABLE_NAME: &str = "twad_key_deletes_tables";
131
132/// Filename for the user script file used in startpos generation.
133pub const USER_SCRIPT_FILE_NAME: &str = "user.script.txt";
134
135/// Path to the victory objectives file within a pack.
136pub const VICTORY_OBJECTIVES_FILE_NAME: &str = "db/victory_objectives.txt";
137
138/// Extracted filename for victory objectives.
139pub const VICTORY_OBJECTIVES_EXTRACTED_FILE_NAME: &str = "victory_objectives.txt";
140
141/// List of games that require victory objectives file handling.
142///
143/// These games need special processing for the `victory_objectives.txt` file
144/// during startpos generation.
145pub const GAMES_NEEDING_VICTORY_OBJECTIVES: [&str; 9] = [
146    KEY_PHARAOH_DYNASTIES,
147    KEY_PHARAOH,
148    KEY_TROY,
149    KEY_THREE_KINGDOMS,
150    KEY_WARHAMMER_2,
151    KEY_WARHAMMER,
152    KEY_THRONES_OF_BRITANNIA,
153    KEY_ATTILA,
154    KEY_ROME_2
155];
156
157//-------------------------------------------------------------------------------//
158//                              Enums & Structs
159//-------------------------------------------------------------------------------//
160
161/// Central dependencies manager for all reference data relevant to a Pack.
162///
163/// This struct caches and manages all data needed for reference lookups, diagnostics,
164/// and other operations that require knowledge of vanilla game data or parent mods.
165///
166/// # Data Categories
167///
168/// The dependencies are organized into three persistence levels:
169///
170/// ## Serialized to Disk (cached)
171///
172/// These fields are saved to `.pak` files and only regenerated when the game
173/// files change or RPFM is updated:
174///
175/// - `vanilla_files` - All files from CA's official PackFiles
176/// - `vanilla_tables` - Index of DB table paths by table name
177/// - `vanilla_locs` - Set of Loc file paths
178/// - `vanilla_folders` - Set of folder paths for existence checks
179/// - `vanilla_paths` - Case-insensitive path lookup map
180/// - `asskit_only_db_tables` - Tables only present in Assembly Kit
181///
182/// ## Regenerated on Rebuild
183///
184/// These fields are rebuilt each time `rebuild()` is called, as they depend
185/// on the current environment:
186///
187/// - `vanilla_loose_*` - Files from the game's `/data` folder (not in packs)
188/// - `parent_*` - Files from parent mods the current pack depends on
189///
190/// ## Runtime Cache
191///
192/// Built on-demand during editing and not persisted:
193///
194/// - `local_tables_references` - Cached reference data for edited tables
195/// - `localisation_data` - Merged Loc data for quick lookups
196#[derive(Default, Debug, Clone, Getters, Serialize, Deserialize)]
197#[getset(get = "pub")]
198pub struct Dependencies {
199
200    /// Date of the generation of this dependencies cache. For checking if it needs an update.
201    build_date: u64,
202
203    /// Version of the program used to generate the dependencies, so they're properly invalidated on update.
204    version: String,
205
206    /// Data to quickly load loose files as part of the dependencies.
207    ///
208    /// Not serialized, regenerated on rebuild because these can frequently change.
209    #[serde(skip_serializing, skip_deserializing)]
210    vanilla_loose_files: HashMap<String, RFile>,
211
212    /// Data to quickly load CA dependencies from disk.
213    vanilla_files: HashMap<String, RFile>,
214
215    /// Data to quickly load dependencies from parent mods from disk.
216    ///
217    /// Not serialized, regenerated from parent Packs on rebuild.
218    #[serde(skip_serializing, skip_deserializing)]
219    parent_files: HashMap<String, RFile>,
220
221    /// List of DB tables on the CA loose files. Not really used, but just in case.
222    #[serde(skip_serializing, skip_deserializing)]
223    vanilla_loose_tables: HashMap<String, Vec<String>>,
224
225    /// List of DB tables on the CA files.
226    vanilla_tables: HashMap<String, Vec<String>>,
227
228    /// List of DB tables on the parent files.
229    ///
230    /// Not serialized, regenerated from parent Packs on rebuild.
231    #[serde(skip_serializing, skip_deserializing)]
232    parent_tables: HashMap<String, Vec<String>>,
233
234    /// List of Loc tables on the CA loose files. Not really used, but just in case.
235    #[serde(skip_serializing, skip_deserializing)]
236    vanilla_loose_locs: HashSet<String>,
237
238    /// List of Loc tables on the CA files.
239    vanilla_locs: HashSet<String>,
240
241    /// List of Loc tables on the parent files.
242    ///
243    /// Not serialized, regenerated from parent Packs on rebuild.
244    #[serde(skip_serializing, skip_deserializing)]
245    parent_locs: HashSet<String>,
246
247    /// Data to quickly check if a path exists in the vanilla loose files.
248    #[serde(skip_serializing, skip_deserializing)]
249    vanilla_loose_folders: HashSet<String>,
250
251    /// Data to quickly check if a path exists in the vanilla files.
252    vanilla_folders: HashSet<String>,
253
254    /// Data to quickly check if a path exists in the parent mod files.
255    #[serde(skip_serializing, skip_deserializing)]
256    parent_folders: HashSet<String>,
257
258    /// List of vanilla loose paths lowercased, with their casing counterparts. To quickly find files.
259    #[serde(skip_serializing, skip_deserializing)]
260    vanilla_loose_paths: HashMap<String, Vec<String>>,
261
262    /// List of vanilla paths lowercased, with their casing counterparts. To quickly find files.
263    vanilla_paths: HashMap<String, Vec<String>>,
264
265    /// List of parent paths lowercased, with their casing counterparts. To quickly find files.
266    ///
267    /// Not serialized, regenerated from parent Packs on rebuild.
268    #[serde(skip_serializing, skip_deserializing)]
269    parent_paths: HashMap<String, Vec<String>>,
270
271    /// Cached data for local tables.
272    ///
273    /// This is for runtime caching, and it must not be serialized to disk.
274    #[serde(skip_serializing, skip_deserializing)]
275    local_tables_references: HashMap<String, HashMap<i32, TableReferences>>,
276
277    /// Data from all the locs, so we can quickly search for a loc entry.
278    #[serde(skip_serializing, skip_deserializing)]
279    localisation_data: HashMap<String, String>,
280
281    /// DB Files only available on the assembly kit. Usable only for references. Do not use them as the base for new tables.
282    asskit_only_db_tables: HashMap<String, DB>,
283}
284
285/// Reference data for a single column in a DB table.
286///
287/// When a column has a foreign key reference to another table (or lookup data
288/// for display purposes), this struct holds the valid values and their
289/// human-readable representations.
290///
291/// # Example
292///
293/// For a column referencing `main_units_tables.key`:
294/// - `data` would contain entries like `("wh_main_emp_inf_swordsmen", "Empire Swordsmen")`
295/// - The key is the actual value stored in the DB
296/// - The value is the lookup/display text (if available)
297#[derive(Eq, PartialEq, Clone, Default, Debug, Getters, MutGetters, Serialize, Deserialize)]
298#[getset(get = "pub", get_mut = "pub")]
299pub struct TableReferences {
300
301    /// Name of the column these references are for.
302    ///
303    /// This is primarily for debugging purposes. Do not rely on it for
304    /// programmatic column identification.
305    field_name: String,
306
307    /// Whether the referenced table exists only in the Assembly Kit.
308    ///
309    /// When `true`, the reference data comes from Assembly Kit tables rather
310    /// than game files. This is useful for diagnostics to identify references
311    /// to tables that may not be fully supported.
312    referenced_table_is_ak_only: bool,
313
314    /// Whether the referenced column has been localised.
315    ///
316    /// Some columns that originally contained text are moved to Loc files
317    /// when exported from the Assembly Kit (Dave). This flag indicates that
318    /// the lookup values should be fetched from localisation data.
319    referenced_column_is_localised: bool,
320
321    /// The reference data mapping keys to display values.
322    ///
323    /// - **Key**: The actual value that can be stored in the column
324    /// - **Value**: Human-readable lookup text (may be empty if no lookup defined)
325    data: HashMap<String, String>,
326}
327
328/// Shape of the `.pak3` cache file, mirroring the `vanilla_tables` / `vanilla_locs`
329/// / `vanilla_folders` / `vanilla_paths` / `asskit_only_db_tables` fields of
330/// [`Dependencies`] in that order.
331type Pak3Cache = (
332    HashMap<String, Vec<String>>,
333    HashSet<String>,
334    HashSet<String>,
335    HashMap<String, Vec<String>>,
336    HashMap<String, DB>,
337);
338
339//-------------------------------------------------------------------------------//
340//                             Implementations
341//-------------------------------------------------------------------------------//
342
343impl Dependencies {
344
345    //-----------------------------------//
346    // Generation and disk IO
347    //-----------------------------------//
348
349    /// This function takes care of rebuilding the whole dependencies cache to be used with a new Pack.
350    ///
351    /// If a file path is passed, the dependencies cache at that path will be used, replacing the currently loaded dependencies cache.
352    /// If a schema is not passed, no tables/locs will be pre-decoded. Make sure to decode them later with [Dependencies::decode_tables].
353    pub fn rebuild(&mut self, schema: &Option<Schema>, parent_pack_names: &[String], file_path: Option<&Path>, game_info: &GameInfo, game_path: &Path, secondary_path: &Path) -> Result<()> {
354
355        // If we only want to reload the parent mods, not the full dependencies, we can skip this section.
356        if let Some(file_path) = file_path {
357
358            // First, clear the current data, so we're not left with broken data afterwards if the next operations fail.
359            *self = Self::default();
360
361            // Try to load the binary file and check if it's even valid.
362            let stored_data = Self::load(file_path, schema)?;
363            if !stored_data.needs_updating(game_info, game_path)? {
364                *self = stored_data;
365            }
366        }
367
368        // Clear the table's cached data, to ensure it gets rebuild properly when needed.
369        self.local_tables_references.clear();
370
371        // Load vanilla loose files (from /data).
372        self.load_loose_files(schema, game_info, game_path)?;
373
374        // Load parent mods of the currently loaded Pack.
375        self.load_parent_files(schema, parent_pack_names, game_info, game_path, secondary_path)?;
376
377        // Populate the localisation data.
378        let loc_files = self.loc_data(true, true).unwrap_or_default();
379        let loc_decoded = loc_files.iter()
380            .filter_map(|file| if let Ok(RFileDecoded::Loc(loc)) = file.decoded() { Some(loc) } else { None })
381            .map(|file| file.data())
382            .collect::<Vec<_>>();
383
384        self.localisation_data = loc_decoded.par_iter()
385            .flat_map(|data| data.par_iter()
386                .map(|entry| (entry[0].data_to_string().to_string(), entry[1].data_to_string().to_string()))
387                .collect::<Vec<(_,_)>>()
388            ).collect::<HashMap<_,_>>();
389
390        Ok(())
391    }
392
393    /// This function generates the dependencies cache for the game provided and returns it.
394    pub fn generate_dependencies_cache(schema: &Option<Schema>, game_info: &GameInfo, game_path: &Path, asskit_path: &Option<PathBuf>, ignore_game_files_in_ak: bool) -> Result<Self> {
395        let mut cache = Self {
396            build_date: current_time()?,
397            version: VERSION.to_owned(),
398            vanilla_files: Pack::read_and_merge_ca_packs(game_info, game_path)?.files().clone(),
399            ..Default::default()
400        };
401
402        let cacheable = cache.vanilla_files.par_iter_mut()
403            .filter_map(|(_, file)| {
404                let _ = file.guess_file_type();
405
406                match file.file_type() {
407                    FileType::DB |
408                    FileType::Loc => Some(file),
409                    _ => None,
410                }
411            })
412            .collect::<Vec<&mut RFile>>();
413
414        cacheable.iter()
415            .for_each(|file| {
416                match file.file_type() {
417                    FileType::DB => {
418                        if let Some(table_name) = file.db_table_name_from_path() {
419                            match cache.vanilla_tables.get_mut(table_name) {
420                                Some(table_paths) => table_paths.push(file.path_in_container_raw().to_owned()),
421                                None => { cache.vanilla_tables.insert(table_name.to_owned(), vec![file.path_in_container_raw().to_owned()]); },
422                            }
423                        }
424                    }
425                    FileType::Loc => {
426                        cache.vanilla_locs.insert(file.path_in_container_raw().to_owned());
427                    }
428                    _ => {}
429                }
430            }
431        );
432
433        cache.vanilla_folders = cache.vanilla_files.par_iter().filter_map(|(path, _)| {
434            let file_path_split = path.split('/').collect::<Vec<&str>>();
435            let folder_path_len = file_path_split.len() - 1;
436            if folder_path_len == 0 {
437                None
438            } else {
439
440                let mut paths = Vec::with_capacity(folder_path_len);
441
442                for (index, folder) in file_path_split.iter().enumerate() {
443                    if index < path.len() - 1 && !folder.is_empty() {
444                        paths.push(file_path_split[0..=index].join("/"))
445                    }
446                }
447
448                Some(paths)
449            }
450        }).flatten().collect::<HashSet<String>>();
451
452        cache.vanilla_files.keys().for_each(|path| {
453            let lower = path.to_lowercase();
454            match cache.vanilla_paths.get_mut(&lower) {
455                Some(paths) => paths.push(path.to_owned()),
456                None => { cache.vanilla_paths.insert(lower, vec![path.to_owned()]); },
457            }
458        });
459
460        // Load vanilla loose files before processing the AK files, so the AK process has these files available.
461        //
462        // It's mainly so any loose loc is used in the bruteforcing process.
463        cache.load_loose_files(&None, game_info, game_path)?;
464
465        // This one can fail, leaving the dependencies with only game data.
466        if let Some(path) = asskit_path {
467            let _ = cache.generate_asskit_only_db_tables(schema, path, *game_info.raw_db_version(), ignore_game_files_in_ak);
468        }
469
470        Ok(cache)
471    }
472
473    /// This function generates a "fake" table list with tables only present in the Assembly Kit.
474    ///
475    /// This works by processing all the tables from the game's raw table folder and turning them into fake decoded tables,
476    /// with version -1. That will allow us to use them for dependency checking and for populating combos.
477    ///
478    /// To keep things fast, only undecoded or missing (from the game files) tables will be included into the PAK2 file.
479    fn generate_asskit_only_db_tables(&mut self, schema: &Option<Schema>, raw_db_path: &Path, version: i16, ignore_game_files: bool) -> Result<()> {
480        let files_to_ignore = if ignore_game_files {
481            self.vanilla_tables.keys().map(|table_name| &table_name[..table_name.len() - 7]).collect::<Vec<_>>()
482        } else {
483            vec![]
484        };
485        let raw_tables = RawTable::read_all(raw_db_path, version, &files_to_ignore)?;
486        let asskit_only_db_tables = raw_tables.par_iter()
487            .map(|x| match schema {
488                Some(schema) => {
489                    let mut table_name = x.definition.clone().unwrap().name.unwrap().to_owned();
490                    table_name.pop();
491                    table_name.pop();
492                    table_name.pop();
493                    table_name.pop();
494
495                    table_name = format!("{table_name}_tables");
496
497                    let definition = schema.definitions().get(&table_name).and_then(|x| x.first());
498
499                    x.to_db(definition)
500                }
501                None => x.to_db(None),
502            })
503            .collect::<Result<Vec<DB>>>()?;
504
505        // We need to bruteforce loc keys for ak tables here, so locs relations are setup correctly for ak tables.
506        let mut asskit_only_db_tables = asskit_only_db_tables.par_iter().map(|table| (table.table_name().to_owned(), table.clone())).collect::<HashMap<String, DB>>();
507
508        let decode_extra_data = DecodeableExtraData::default();
509        let extra_data = Some(decode_extra_data);
510
511        // Vanilla loose files.
512        let mut files = self.vanilla_loose_locs.iter().filter_map(|path| {
513            self.vanilla_loose_files.remove(path).map(|file| (path.to_owned(), file))
514        }).collect::<Vec<_>>();
515
516        files.par_iter_mut().for_each(|(_, file)| {
517            let _ = file.decode(&extra_data, true, false);
518        });
519
520        self.vanilla_loose_files.par_extend(files);
521
522        // Vanilla files.
523        let mut files = self.vanilla_locs.iter().filter_map(|path| {
524            self.vanilla_files.remove(path).map(|file| (path.to_owned(), file))
525        }).collect::<Vec<_>>();
526
527        files.par_iter_mut().for_each(|(_, file)| {
528            let _ = file.decode(&extra_data, true, false);
529        });
530
531        self.vanilla_files.par_extend(files);
532
533        self.bruteforce_loc_key_order(&mut Schema::default(), None, None, Some(&mut asskit_only_db_tables))?;
534        self.asskit_only_db_tables = asskit_only_db_tables;
535
536        Ok(())
537    }
538
539    /// This function builds the local db references data for the tables you pass to it from the Packs provided.
540    ///
541    /// Table names must be provided as full names (with *_tables* at the end).
542    ///
543    /// NOTE: This function, like many others, assumes the tables are already decoded in the Packs. If they're not, they'll be ignored.
544    pub fn generate_local_db_references(&mut self, schema: &Schema, packs: &BTreeMap<String, Pack>, table_names: &[String]) {
545
546        let local_tables_references = packs.values()
547            .flat_map(|pack| pack.files_by_type(&[FileType::DB]))
548            .par_bridge()
549            .filter_map(|file| {
550                if let Ok(RFileDecoded::DB(db)) = file.decoded() {
551
552                    // Only generate references for the tables you pass it, or for all if we pass the list of tables empty.
553                    if table_names.is_empty() || table_names.iter().any(|x| x == db.table_name()) {
554                        Some((db.table_name().to_owned(), self.generate_references(schema, db.table_name(), db.definition())))
555                    } else { None }
556                } else { None }
557            }).collect::<HashMap<_, _>>();
558
559        self.local_tables_references.extend(local_tables_references);
560    }
561
562    /// This function builds the local db references data for the table with the definition you pass to and stores it in the cache.
563    pub fn generate_local_definition_references(&mut self, schema: &Schema, table_name: &str, definition: &Definition) {
564        self.local_tables_references.insert(table_name.to_owned(), self.generate_references(schema, table_name, definition));
565    }
566
567    /// This function builds the local db references data for the table with the definition you pass to, and returns it.
568    pub fn generate_references(&self, schema: &Schema, local_table_name: &str, definition: &Definition) -> HashMap<i32, TableReferences> {
569
570        // Trick: before doing this, we modify the definition to include any lookup from any reference,
571        // so we are actually able to catch recursive-like lookups without reading multiple tables.
572        let mut definition = definition.clone();
573
574        // `Definition::patches` is `#[serde(skip)]`, so a definition that came across the IPC
575        // boundary arrives with empty patches. Repopulate them from the schema's patch set or
576        // every patch-defined lookup (incl. `lookup_hardcoded`, e.g. the names tables) is lost.
577        if let Some(table_patches) = schema.patches_for_table(local_table_name) {
578            definition.set_patches(table_patches.clone());
579        }
580
581        self.add_recursive_lookups_to_definition(schema, &mut definition, local_table_name);
582
583        let patches = Some(definition.patches());
584        let fields_processed = definition.fields_processed();
585
586        // Key deletes works in a different way. For it we have to get the names of all the tables,
587        // then we retrieve the keys data dinamically when selecting in the ui.
588        if local_table_name == KEY_DELETES_TABLE_NAME {
589            let mut hashmap = HashMap::new();
590            let mut references = TableReferences::default();
591            *references.field_name_mut() = "table_name".to_owned();
592
593            for key in schema.definitions().keys() {
594                if key.len() > 7 {
595                    let table_name = key.to_owned().drain(..key.len() - 7).collect::<String>();
596                    references.data.insert(table_name, String::new());
597                }
598            }
599
600            hashmap.insert(1, references);
601            return hashmap;
602        }
603
604        fields_processed.par_iter().enumerate().filter_map(|(column, field)| {
605            match field.is_reference(patches) {
606                Some((ref ref_table, ref ref_column)) => {
607                    if !ref_table.is_empty() && !ref_column.is_empty() {
608                        let ref_table = format!("{ref_table}_tables");
609
610                        // Get his lookup data if it has it.
611                        let lookup_data = if let Some(ref data) = field.lookup_no_patch() { data.to_vec() } else { Vec::with_capacity(0) };
612                        let mut references = TableReferences::default();
613                        *references.field_name_mut() = field.name().to_owned();
614
615                        let fake_found = self.db_reference_data_from_asskit_tables(&mut references, (&ref_table, ref_column, &lookup_data));
616                        let real_found = self.db_reference_data_from_vanilla_and_modded_tables(&mut references, (&ref_table, ref_column, &lookup_data));
617
618                        if fake_found && real_found.is_none() {
619                            references.referenced_table_is_ak_only = true;
620                        }
621
622                        if let Some(ref_definition) = real_found {
623                            if ref_definition.localised_fields().iter().any(|x| x.name() == ref_column) {
624                                references.referenced_column_is_localised = true;
625                            }
626                        }
627
628                        Some((column as i32, references))
629                    } else { None }
630                },
631
632                // In the fallback case (no references) we still need to check for lookup data within our table and the locs.
633                None => {
634                    if let Some(ref lookup_data) = field.lookup_no_patch() {
635
636                        // Only single-keyed tables can have lookups.
637                        if field.is_key(patches) && fields_processed.iter().filter(|x| x.is_key(patches)).count() == 1 {
638                            let ref_table = local_table_name;
639                            let ref_column = field.name();
640
641                            // Get his lookup data if it has it.
642                            let mut references = TableReferences::default();
643                            *references.field_name_mut() = field.name().to_owned();
644
645                            let fake_found = self.db_reference_data_from_asskit_tables(&mut references, (ref_table, ref_column, lookup_data));
646                            let real_found = self.db_reference_data_from_vanilla_and_modded_tables(&mut references, (ref_table, ref_column, lookup_data));
647
648                            if fake_found && real_found.is_none() {
649                                references.referenced_table_is_ak_only = true;
650                            }
651
652                            if let Some(ref_definition) = real_found {
653                                if ref_definition.localised_fields().iter().any(|x| x.name() == ref_column) {
654                                    references.referenced_column_is_localised = true;
655                                }
656                            }
657
658                            Some((column as i32, references))
659                        } else { None }
660                    } else { None }
661                },
662            }
663        }).collect::<HashMap<_, _>>()
664    }
665
666    /// This function tries to load dependencies from the path provided.
667    pub fn load(file_path: &Path, schema: &Option<Schema>) -> Result<Self> {
668
669        // Optimization: Instead of a big file, we split the dependencies in 3 files. Why?
670        // Because bitcode is not multithreaded and, while reading 3 medium files is slower than a big one,
671        // deserializing 3 medium files in 3 separate threads is way faster than 1 big file in 1 thread.
672        let mut file_path_1 = file_path.to_path_buf();
673        let handle_1: JoinHandle<Result<(u64, String, Vec<RFile>)>> = spawn(move || {
674            file_path_1.set_extension("pak1");
675            let mut file = BufReader::new(File::open(&file_path_1)?);
676            let mut data = Vec::with_capacity(file.get_ref().metadata()?.len() as usize);
677            file.read_to_end(&mut data)?;
678
679            // Never deserialize directly from the file. It's bloody slow!!!
680            bitcode::deserialize(&data).map_err(From::from)
681        });
682
683        let mut file_path_2 = file_path.to_path_buf();
684        let handle_2: JoinHandle<Result<Vec<RFile>>> = spawn(move || {
685            file_path_2.set_extension("pak2");
686            let mut file = BufReader::new(File::open(&file_path_2)?);
687            let mut data = Vec::with_capacity(file.get_ref().metadata()?.len() as usize);
688            file.read_to_end(&mut data)?;
689
690            // Never deserialize directly from the file. It's bloody slow!!!
691            bitcode::deserialize(&data).map_err(From::from)
692        });
693
694        let mut file_path_3 = file_path.to_path_buf();
695        let handle_3: JoinHandle<Result<Pak3Cache>> = spawn(move || {
696            file_path_3.set_extension("pak3");
697            let mut file = BufReader::new(File::open(&file_path_3)?);
698            let mut data = Vec::with_capacity(file.get_ref().metadata()?.len() as usize);
699            file.read_to_end(&mut data)?;
700
701            // Never deserialize directly from the file. It's bloody slow!!!
702            bitcode::deserialize(&data).map_err(From::from)
703        });
704
705        // Get the thread's data in reverse, as 1 and 2 are actually the slower to process.
706        let mut dependencies = Self::default();
707        let data_3 = handle_3.join().unwrap()?;
708        let data_2 = handle_2.join().unwrap()?;
709        let data_1 = handle_1.join().unwrap()?;
710
711        // The vanilla file list is stored in a Vec format instead of a hashmap, because a vec can be splited,
712        // and that list is more than 100mb long in some games. Here we turn it back to HashMap and merge it.
713        let mut vanilla_files: HashMap<_,_> = data_1.2.into_par_iter().map(|file| (file.path_in_container_raw().to_owned(), file)).collect();
714        vanilla_files.par_extend(data_2.into_par_iter().map(|file| (file.path_in_container_raw().to_owned(), file)));
715
716        dependencies.build_date = data_1.0;
717        dependencies.version = data_1.1;
718        dependencies.vanilla_files = vanilla_files;
719        dependencies.vanilla_tables = data_3.0;
720        dependencies.vanilla_locs = data_3.1;
721        dependencies.vanilla_folders = data_3.2;
722        dependencies.vanilla_paths = data_3.3;
723        dependencies.asskit_only_db_tables = data_3.4;
724
725        // Only decode the tables if we passed a schema. If not, it's responsability of the user to decode them later.
726        if let Some(schema) = schema {
727            let mut decode_extra_data = DecodeableExtraData::default();
728            decode_extra_data.set_schema(Some(schema));
729            let extra_data = Some(decode_extra_data);
730
731            let mut files = dependencies.vanilla_locs.iter().chain(dependencies.vanilla_tables.values().flatten()).filter_map(|path| {
732                dependencies.vanilla_files.remove(path).map(|file| (path.to_owned(), file))
733            }).collect::<Vec<_>>();
734
735            files.par_iter_mut().for_each(|(_, file)| {
736                let _ = file.decode(&extra_data, true, false);
737            });
738
739            dependencies.vanilla_files.par_extend(files);
740        }
741
742        Ok(dependencies)
743    }
744
745    /// This function saves a dependencies cache to the provided path.
746    pub fn save(&mut self, file_path: &Path) -> Result<()> {
747        let mut folder_path = file_path.to_owned();
748        folder_path.pop();
749        DirBuilder::new().recursive(true).create(&folder_path)?;
750
751        let mut file_path_1 = file_path.to_path_buf();
752        let mut file_path_2 = file_path.to_path_buf();
753        let mut file_path_3 = file_path.to_path_buf();
754
755        file_path_1.set_extension("pak1");
756        file_path_2.set_extension("pak2");
757        file_path_3.set_extension("pak3");
758
759        let mut file_1 = File::create(&file_path_1)?;
760        let mut file_2 = File::create(&file_path_2)?;
761        let mut file_3 = File::create(&file_path_3)?;
762
763        // Split the vanilla file's list in half and turn it into a vec, so it's faster when loading.
764        // NOTE: While the HashMap -> Vec conversion only keeping values thing is slower to read
765        // than serializing/loading the keys directly, it saves about 40mb of data on disk.
766        let mut vanilla_files_1 = self.vanilla_files.par_iter().map(|(_, b)| b.clone()).collect::<Vec<RFile>>();
767        let vanilla_files_2 = vanilla_files_1.split_off(self.vanilla_files.len() / 2);
768
769        // Never serialize directly into the file. It's bloody slow!!!
770        let serialized_1: Vec<u8> = bitcode::serialize(&(&self.build_date, &self.version, &vanilla_files_1))?;
771        let serialized_2: Vec<u8> = bitcode::serialize(&vanilla_files_2)?;
772        let serialized_3: Vec<u8> = bitcode::serialize(&(&self.vanilla_tables, &self.vanilla_locs, &self.vanilla_folders, &self.vanilla_paths, &self.asskit_only_db_tables))?;
773
774        file_1.write_all(&serialized_1).map_err(RLibError::from)?;
775        file_2.write_all(&serialized_2).map_err(RLibError::from)?;
776        file_3.write_all(&serialized_3).map_err(From::from)
777    }
778
779    /// This function is used to check if the game files used to generate the dependencies cache have changed, requiring an update.
780    pub fn needs_updating(&self, game_info: &GameInfo, game_path: &Path) -> Result<bool> {
781        let ca_paths = game_info.ca_packs_paths(game_path)?;
782        let last_date = last_modified_time_from_files(&ca_paths)?;
783        Ok(last_date > self.build_date || self.version != VERSION)
784    }
785
786    /// This function loads all the loose files within the game's /data folder.
787    fn load_loose_files(&mut self, schema: &Option<Schema>, game_info: &GameInfo, game_path: &Path) -> Result<()> {
788        self.vanilla_loose_files.clear();
789        self.vanilla_loose_tables.clear();
790        self.vanilla_loose_locs.clear();
791        self.vanilla_loose_folders.clear();
792        self.vanilla_loose_paths.clear();
793
794        let game_data_path = game_info.data_path(game_path)?;
795        let game_data_path_str = game_data_path.to_string_lossy().replace('\\', "/");
796
797        self.vanilla_loose_files = files_from_subdir(&game_data_path, true)?
798            .into_par_iter()
799            .filter_map(|path| {
800                let mut path = path.to_string_lossy().replace('\\', "/");
801                if !path.ends_with(".pack") {
802                    if let Ok(mut rfile) = RFile::new_from_file(&path) {
803                        let subpath = path.split_off(game_data_path_str.len() + 1);
804                        rfile.set_path_in_container_raw(&subpath);
805                        let _ = rfile.guess_file_type();
806                        Some((subpath, rfile))
807                    } else {
808                        None
809                    }
810                } else {
811                    None
812                }
813            })
814            .collect::<HashMap<String, RFile>>();
815
816        let cacheable = self.vanilla_loose_files.par_iter_mut()
817            .filter_map(|(_, file)| {
818                let _ = file.guess_file_type();
819
820                match file.file_type() {
821                    FileType::DB |
822                    FileType::Loc => Some(file),
823                    _ => None,
824                }
825            })
826            .collect::<Vec<&mut RFile>>();
827
828        cacheable.iter()
829            .for_each(|file| {
830                match file.file_type() {
831                    FileType::DB => {
832                        if let Some(table_name) = file.db_table_name_from_path() {
833                            match self.vanilla_loose_tables.get_mut(table_name) {
834                                Some(table_paths) => table_paths.push(file.path_in_container_raw().to_owned()),
835                                None => { self.vanilla_loose_tables.insert(table_name.to_owned(), vec![file.path_in_container_raw().to_owned()]); },
836                            }
837                        }
838                    }
839                    FileType::Loc => {
840                        self.vanilla_loose_locs.insert(file.path_in_container_raw().to_owned());
841                    }
842                    _ => {}
843                }
844            }
845        );
846
847        self.vanilla_loose_folders = self.vanilla_loose_files.par_iter().filter_map(|(path, _)| {
848            let file_path_split = path.split('/').collect::<Vec<&str>>();
849            let folder_path_len = file_path_split.len() - 1;
850            if folder_path_len == 0 {
851                None
852            } else {
853
854                let mut paths = Vec::with_capacity(folder_path_len);
855
856                for (index, folder) in file_path_split.iter().enumerate() {
857                    if index < path.len() - 1 && !folder.is_empty() {
858                        paths.push(file_path_split[0..=index].join("/"))
859                    }
860                }
861
862                Some(paths)
863            }
864        }).flatten().collect::<HashSet<String>>();
865
866        self.vanilla_loose_files.keys().for_each(|path| {
867            let lower = path.to_lowercase();
868            match self.vanilla_loose_paths.get_mut(&lower) {
869                Some(paths) => paths.push(path.to_owned()),
870                None => { self.vanilla_loose_paths.insert(lower, vec![path.to_owned()]); },
871            }
872        });
873
874        // Only decode the tables if we passed a schema. If not, it's responsability of the user to decode them later.
875        if let Some(schema) = schema {
876            let mut decode_extra_data = DecodeableExtraData::default();
877            decode_extra_data.set_schema(Some(schema));
878            let extra_data = Some(decode_extra_data);
879
880            let mut files = self.vanilla_loose_locs.iter().chain(self.vanilla_loose_tables.values().flatten()).filter_map(|path| {
881                self.vanilla_loose_files.remove(path).map(|file| (path.to_owned(), file))
882            }).collect::<Vec<_>>();
883
884            files.par_iter_mut().for_each(|(_, file)| {
885                let _ = file.decode(&extra_data, true, false);
886            });
887
888            self.vanilla_loose_files.par_extend(files);
889        }
890
891        Ok(())
892    }
893
894
895    /// This function loads all the loose files within the game's /data folder.
896    fn load_parent_files(&mut self, schema: &Option<Schema>, parent_pack_names: &[String], game_info: &GameInfo, game_path: &Path, secondary_path: &Path) -> Result<()> {
897        self.parent_files.clear();
898        self.parent_tables.clear();
899        self.parent_locs.clear();
900        self.parent_folders.clear();
901        self.parent_paths.clear();
902
903        // Preload parent mods of the currently loaded Pack.
904        self.load_parent_packs(parent_pack_names, game_info, game_path, secondary_path)?;
905        self.parent_files.par_iter_mut().map(|(_, file)| file.guess_file_type()).collect::<Result<()>>()?;
906
907        // Then build the table/loc lists, for easy access.
908        self.parent_files.iter()
909            .for_each(|(path, file)| {
910                match file.file_type() {
911                    FileType::DB => {
912                        if let Some(table_name) = file.db_table_name_from_path() {
913                            match self.parent_tables.get_mut(table_name) {
914                                Some(table_paths) => table_paths.push(path.to_owned()),
915                                None => { self.parent_tables.insert(table_name.to_owned(), vec![path.to_owned()]); },
916                            }
917                        }
918                    }
919                    FileType::Loc => {
920                        self.parent_locs.insert(path.to_owned());
921                    }
922                    _ => {}
923                }
924            }
925        );
926
927        // Build the folder list.
928        self.parent_folders = self.parent_files.par_iter().filter_map(|(path, _)| {
929            let file_path_split = path.split('/').collect::<Vec<&str>>();
930            let folder_path_len = file_path_split.len() - 1;
931            if folder_path_len == 0 {
932                None
933            } else {
934
935                let mut paths = Vec::with_capacity(folder_path_len);
936
937                for (index, folder) in file_path_split.iter().enumerate() {
938                    if index < path.len() - 1 && !folder.is_empty() {
939                        paths.push(file_path_split[0..=index].join("/"))
940                    }
941                }
942
943                Some(paths)
944            }
945        }).flatten().collect::<HashSet<String>>();
946
947        self.parent_files.keys().for_each(|path| {
948            let lower = path.to_lowercase();
949            match self.parent_paths.get_mut(&lower) {
950                Some(paths) => paths.push(path.to_owned()),
951                None => { self.parent_paths.insert(lower, vec![path.to_owned()]); },
952            }
953        });
954
955        // Only decode the tables if we passed a schema. If not, it's responsability of the user to decode them later.
956        if let Some(schema) = schema {
957            let mut decode_extra_data = DecodeableExtraData::default();
958            decode_extra_data.set_schema(Some(schema));
959            let extra_data = Some(decode_extra_data);
960
961            let mut files = self.parent_tables.values().flatten().filter_map(|path| {
962                self.parent_files.remove(path).map(|file| (path.to_owned(), file))
963            }).collect::<Vec<_>>();
964
965            files.par_iter_mut().for_each(|(_, file)| {
966                let _ = file.decode(&extra_data, true, false);
967            });
968
969            self.parent_files.par_extend(files);
970        }
971
972        // Also decode the locs. They don't need an schema.
973        let mut files = self.parent_locs.iter().filter_map(|path| {
974            self.parent_files.remove(path).map(|file| (path.to_owned(), file))
975        }).collect::<Vec<_>>();
976
977        files.par_iter_mut().for_each(|(_, file)| {
978            let _ = file.decode(&None, true, false);
979        });
980
981        self.parent_files.par_extend(files);
982
983        Ok(())
984    }
985
986    /// This function loads all the parent [Packs](rpfm_lib::files::pack::Pack) provided as `parent_pack_names` as dependencies,
987    /// taking care of also loading all dependencies of all of them, if they're not already loaded.
988    fn load_parent_packs(&mut self, parent_pack_names: &[String], game_info: &GameInfo, game_path: &Path, secondary_path: &Path) -> Result<()> {
989        let data_packs_paths = game_info.data_packs_paths(game_path).unwrap_or_default();
990        let secondary_packs_paths = game_info.secondary_packs_paths(secondary_path);
991        let content_packs_paths = game_info.content_packs_paths(game_path);
992        let mut loaded_packfiles = vec![];
993
994        parent_pack_names.iter().for_each(|pack_name| self.load_parent_pack(pack_name, &mut loaded_packfiles, &data_packs_paths, &secondary_packs_paths, &content_packs_paths, game_info));
995
996        Ok(())
997    }
998
999    /// This function loads a parent [Pack](rpfm_lib::files::pack::Pack) as a dependency,
1000    /// taking care of also loading all dependencies of it, if they're not already loaded.
1001    fn load_parent_pack(
1002        &mut self,
1003        pack_name: &str,
1004        already_loaded: &mut Vec<String>,
1005        data_paths: &[PathBuf],
1006        secondary_paths: &Option<Vec<PathBuf>>,
1007        content_paths: &Option<Vec<PathBuf>>,
1008        game_info: &GameInfo
1009    ) {
1010        // Do not process Packs twice.
1011        if !already_loaded.contains(&pack_name.to_owned()) {
1012
1013            // First check in /data. If we have packs there, do not bother checking for external Packs.
1014            if let Some(path) = data_paths.iter().find(|x| x.file_name().unwrap().to_string_lossy() == pack_name) {
1015                if let Ok(pack) = Pack::read_and_merge(&[path.to_path_buf()], game_info, true, false, false) {
1016                    already_loaded.push(pack_name.to_owned());
1017                    pack.dependencies().iter().for_each(|(_, pack_name)| self.load_parent_pack(pack_name, already_loaded, data_paths, secondary_paths, content_paths, game_info));
1018                    self.parent_files.extend(pack.files().clone());
1019
1020                    return;
1021                }
1022            }
1023
1024            // Then check in /secondary. If we have packs there, do not bother checking for content Packs.
1025            if let Some(ref paths) = secondary_paths {
1026                if let Some(path) = paths.iter().find(|x| x.file_name().unwrap().to_string_lossy() == pack_name) {
1027                    if let Ok(pack) = Pack::read_and_merge(&[path.to_path_buf()], game_info, true, false, false) {
1028                        already_loaded.push(pack_name.to_owned());
1029                        pack.dependencies().iter().for_each(|(_, pack_name)| self.load_parent_pack(pack_name, already_loaded, data_paths, secondary_paths, content_paths, game_info));
1030                        self.parent_files.extend(pack.files().clone());
1031
1032                        return;
1033                    }
1034                }
1035            }
1036
1037            // If nothing else works, check in content.
1038            if let Some(ref paths) = content_paths {
1039                if let Some(path) = paths.iter().find(|x| x.file_name().unwrap().to_string_lossy() == pack_name) {
1040                    if let Ok(pack) = Pack::read_and_merge(&[path.to_path_buf()], game_info, true, false, false) {
1041                        already_loaded.push(pack_name.to_owned());
1042                        pack.dependencies().iter().for_each(|(_, pack_name)| self.load_parent_pack(pack_name, already_loaded, data_paths, secondary_paths, content_paths, game_info));
1043                        self.parent_files.extend(pack.files().clone());
1044                    }
1045                }
1046            }
1047        }
1048    }
1049
1050    /// Function to force-decode all tables/locs in the dependencies.
1051    ///
1052    /// Many operations require them to be decoded, so if you did not decoded them on load, make sure to call this to decode them after load.
1053    pub fn decode_tables(&mut self, schema: &Option<Schema>) {
1054        if let Some(schema) = schema {
1055
1056            let mut decode_extra_data = DecodeableExtraData::default();
1057            decode_extra_data.set_schema(Some(schema));
1058            let extra_data = Some(decode_extra_data);
1059
1060            // Vanilla loose files.
1061            let mut files = self.vanilla_loose_locs.iter().chain(self.vanilla_loose_tables.values().flatten()).filter_map(|path| {
1062                self.vanilla_loose_files.remove(path).map(|file| (path.to_owned(), file))
1063            }).collect::<Vec<_>>();
1064
1065            files.par_iter_mut().for_each(|(_, file)| {
1066                let _ = file.decode(&extra_data, true, false);
1067            });
1068
1069            self.vanilla_loose_files.par_extend(files);
1070
1071            // Vanilla files.
1072            let mut files = self.vanilla_locs.iter().chain(self.vanilla_tables.values().flatten()).filter_map(|path| {
1073                self.vanilla_files.remove(path).map(|file| (path.to_owned(), file))
1074            }).collect::<Vec<_>>();
1075
1076            files.par_iter_mut().for_each(|(_, file)| {
1077                let _ = file.decode(&extra_data, true, false);
1078            });
1079
1080            self.vanilla_files.par_extend(files);
1081
1082            // Parent files.
1083            let mut files = self.parent_locs.iter().chain(self.parent_tables.values().flatten()).filter_map(|path| {
1084                self.parent_files.remove(path).map(|file| (path.to_owned(), file))
1085            }).collect::<Vec<_>>();
1086
1087            files.par_iter_mut().for_each(|(_, file)| {
1088                let _ = file.decode(&extra_data, true, false);
1089            });
1090
1091            self.parent_files.par_extend(files);
1092        }
1093    }
1094
1095    //-----------------------------------//
1096    // Getters
1097    //-----------------------------------//
1098
1099    /// This function returns a reference to a specific file from the cache, if exists.
1100    pub fn file(&self, file_path: &str, include_vanilla: bool, include_parent: bool, case_insensitive: bool) -> Result<&RFile> {
1101        let file_path = if let Some(file_path) = file_path.strip_prefix('/') {
1102            file_path
1103        } else {
1104            file_path
1105        };
1106
1107        if include_parent {
1108
1109            // Even on case-insensitive searches, try to use get first. We may get lucky.
1110            if let Some(file) = self.parent_files.get(file_path) {
1111                return Ok(file);
1112            }
1113
1114            if case_insensitive {
1115                let lower = file_path.to_lowercase();
1116                if let Some(file) = self.parent_paths.get(&lower).and_then(|paths| self.parent_files.get(&paths[0])) {
1117                    return Ok(file);
1118                }
1119            }
1120        }
1121
1122        if include_vanilla {
1123
1124            // Even on case-insensitive searches, try to use get first. We may get lucky.
1125            if let Some(file) = self.vanilla_files.get(file_path) {
1126                return Ok(file);
1127            }
1128
1129            if case_insensitive {
1130                let lower = file_path.to_lowercase();
1131                if let Some(file) = self.vanilla_paths.get(&lower).and_then(|paths| self.vanilla_files.get(&paths[0])) {
1132                    return Ok(file);
1133                }
1134
1135            }
1136
1137            // Same check for loose paths.
1138            if let Some(file) = self.vanilla_loose_files.get(file_path) {
1139                return Ok(file);
1140            }
1141
1142            if case_insensitive {
1143                let lower = file_path.to_lowercase();
1144                if let Some(file) = self.vanilla_loose_paths.get(&lower).and_then(|paths| self.vanilla_loose_files.get(&paths[0])) {
1145                    return Ok(file);
1146                }
1147            }
1148        }
1149
1150        Err(RLibError::DependenciesCacheFileNotFound(file_path.to_owned()))
1151    }
1152
1153    /// This function returns a mutable reference to a specific file from the cache, if exists.
1154    pub fn file_mut(&mut self, file_path: &str, include_vanilla: bool, include_parent: bool) -> Result<&mut RFile> {
1155        if include_parent {
1156            if let Some(file) = self.parent_files.get_mut(file_path) {
1157                return Ok(file);
1158            }
1159        }
1160
1161        if include_vanilla {
1162            if let Some(file) = self.vanilla_files.get_mut(file_path) {
1163                return Ok(file);
1164            }
1165
1166            if let Some(file) = self.vanilla_loose_files.get_mut(file_path) {
1167                return Ok(file);
1168            }
1169        }
1170
1171        Err(RLibError::DependenciesCacheFileNotFound(file_path.to_owned()))
1172    }
1173
1174    /// Batch variant of [`Dependencies::file_mut`] that returns one `&mut RFile` per matching path.
1175    pub fn files_mut_by_paths(
1176        &mut self,
1177        paths: &HashSet<String>,
1178        include_vanilla: bool,
1179        include_parent: bool,
1180    ) -> HashMap<String, &mut RFile> {
1181        let mut result: HashMap<String, &mut RFile> = HashMap::with_capacity(paths.len());
1182
1183        if include_parent {
1184            for (k, v) in self.parent_files.iter_mut() {
1185                if paths.contains(k) {
1186                    result.insert(k.clone(), v);
1187                }
1188            }
1189        }
1190
1191        if include_vanilla {
1192            for (k, v) in self.vanilla_files.iter_mut() {
1193                if paths.contains(k) && !result.contains_key(k) {
1194                    result.insert(k.clone(), v);
1195                }
1196            }
1197
1198            for (k, v) in self.vanilla_loose_files.iter_mut() {
1199                if paths.contains(k) && !result.contains_key(k) {
1200                    result.insert(k.clone(), v);
1201                }
1202            }
1203        }
1204
1205        result
1206    }
1207
1208    /// This function returns a reference to all files corresponding to the provided paths.
1209    pub fn files_by_path(&self, file_paths: &[ContainerPath], include_vanilla: bool, include_parent: bool, case_insensitive: bool) -> HashMap<String, &RFile> {
1210        let (file_paths, folder_paths): (Vec<_>, Vec<_>) = file_paths.iter().partition_map(|file_path| match file_path {
1211            ContainerPath::File(file_path) => Either::Left(file_path.to_owned()),
1212            ContainerPath::Folder(file_path) => Either::Right(file_path.to_owned()),
1213        });
1214
1215        let mut hashmap = HashMap::new();
1216
1217        // File check.
1218        if !file_paths.is_empty() {
1219            hashmap.extend(file_paths.par_iter()
1220                .filter_map(|file_path| self.file(file_path, include_vanilla, include_parent, case_insensitive)
1221                    .ok()
1222                    .map(|file| (file_path.to_owned(), file)))
1223                .collect::<Vec<(_,_)>>()
1224            );
1225        }
1226
1227        // Folder check.
1228        if !folder_paths.is_empty() {
1229            hashmap.extend(folder_paths.into_par_iter().flat_map(|folder_path| {
1230                let mut folder = vec![];
1231                let folder_path = folder_path.to_owned() + "/";
1232                if include_vanilla {
1233
1234                    if folder_path == "/" {
1235                        folder.extend(self.vanilla_loose_files.par_iter()
1236                            .map(|(path, file)| (path.to_owned(), file))
1237                            .collect::<Vec<(_,_)>>());
1238
1239                        folder.extend(self.vanilla_files.par_iter()
1240                            .map(|(path, file)| (path.to_owned(), file))
1241                            .collect::<Vec<(_,_)>>());
1242
1243                    } else {
1244                        folder.extend(self.vanilla_loose_files.par_iter()
1245                            .filter(|(path, _)| {
1246                                if case_insensitive {
1247                                    starts_with_case_insensitive(path, &folder_path)
1248                                } else {
1249                                    path.starts_with(&folder_path)
1250                                }
1251                            })
1252                            .map(|(path, file)| (path.to_owned(), file))
1253                            .collect::<Vec<(_,_)>>());
1254
1255                        folder.extend(self.vanilla_files.par_iter()
1256                            .filter(|(path, _)| {
1257                                if case_insensitive {
1258                                    starts_with_case_insensitive(path, &folder_path)
1259                                } else {
1260                                    path.starts_with(&folder_path)
1261                                }
1262                            })
1263                            .map(|(path, file)| (path.to_owned(), file))
1264                            .collect::<Vec<(_,_)>>());
1265                    }
1266                }
1267
1268                if include_parent {
1269                    if folder_path == "/" {
1270                        folder.extend(self.parent_files.par_iter()
1271                            .map(|(path, file)| (path.to_owned(), file))
1272                            .collect::<Vec<(_,_)>>());
1273
1274                    } else {
1275                        folder.extend(self.parent_files.par_iter()
1276                            .filter(|(path, _)| {
1277                                if case_insensitive {
1278                                    starts_with_case_insensitive(path, &folder_path)
1279                                } else {
1280                                    path.starts_with(&folder_path)
1281                                }
1282                            })
1283                            .map(|(path, file)| (path.to_owned(), file))
1284                            .collect::<Vec<(_,_)>>());
1285                    }
1286                }
1287                folder
1288            }).collect::<Vec<(_,_)>>());
1289        }
1290
1291        hashmap
1292    }
1293
1294    /// This function returns a reference to all files of the specified FileTypes from the cache, if any, along with their path.
1295    pub fn files_by_types(&self, file_types: &[FileType], include_vanilla: bool, include_parent: bool) -> HashMap<String, &RFile> {
1296        let mut files = HashMap::new();
1297
1298        // Vanilla first, so if parent files are found, they overwrite vanilla files.
1299        if include_vanilla {
1300            files.extend(self.vanilla_loose_files.par_iter().chain(self.vanilla_files.par_iter())
1301                .filter(|(_, file)| file_types.contains(&file.file_type()))
1302                .map(|(path, file)| (path.to_owned(), file))
1303                .collect::<HashMap<_,_>>());
1304        }
1305
1306        if include_parent {
1307            files.extend(self.parent_files.par_iter()
1308                .filter(|(_, file)| file_types.contains(&file.file_type()))
1309                .map(|(path, file)| (path.to_owned(), file))
1310                .collect::<HashMap<_,_>>());
1311        }
1312
1313        files
1314    }
1315
1316    /// This function returns a mutable reference to all files of the specified FileTypes from the cache, if any, along with their path.
1317    pub fn files_by_types_mut(&mut self, file_types: &[FileType], include_vanilla: bool, include_parent: bool) -> HashMap<String, &mut RFile> {
1318        let mut files = HashMap::new();
1319
1320        // Vanilla first, so if parent files are found, they overwrite vanilla files.
1321        if include_vanilla {
1322            files.extend(self.vanilla_loose_files.par_iter_mut().chain(self.vanilla_files.par_iter_mut())
1323                .filter(|(_, file)| file_types.contains(&file.file_type()))
1324                .map(|(path, file)| (path.to_owned(), file))
1325                .collect::<HashMap<_,_>>());
1326        }
1327
1328        if include_parent {
1329            files.extend(self.parent_files.par_iter_mut()
1330                .filter(|(_, file)| file_types.contains(&file.file_type()))
1331                .map(|(path, file)| (path.to_owned(), file))
1332                .collect::<HashMap<_,_>>());
1333        }
1334
1335        files
1336    }
1337
1338    /// This function returns the vanilla/parent locs from the cache, according to the params you pass it.
1339    ///
1340    /// It returns them in the order the game will load them.
1341    pub fn loc_data(&self, include_vanilla: bool, include_parent: bool) -> Result<Vec<&RFile>> {
1342        let mut cache = vec![];
1343
1344        if include_vanilla {
1345            let mut vanilla_loose_locs = self.vanilla_loose_locs.iter().collect::<Vec<_>>();
1346            vanilla_loose_locs.sort();
1347
1348            for path in &vanilla_loose_locs {
1349                if let Some(file) = self.vanilla_loose_files.get(*path) {
1350                    cache.push(file);
1351                }
1352            }
1353
1354            let mut vanilla_locs = self.vanilla_locs.iter().collect::<Vec<_>>();
1355            vanilla_locs.sort();
1356
1357            for path in &vanilla_locs {
1358                if let Some(file) = self.vanilla_files.get(*path) {
1359                    cache.push(file);
1360                }
1361            }
1362        }
1363
1364        if include_parent {
1365            let mut parent_locs = self.parent_locs.iter().collect::<Vec<_>>();
1366            parent_locs.sort();
1367
1368            for path in &parent_locs {
1369                if let Some(file) = self.parent_files.get(*path) {
1370                    cache.push(file);
1371                }
1372            }
1373        }
1374
1375        Ok(cache)
1376    }
1377
1378    /// This function returns the vanilla/parent db tables from the cache, according to the params you pass it.
1379    ///
1380    /// It returns them in the order the game will load them.
1381    ///
1382    /// NOTE: table_name is expected to be the table's folder name, with "_tables" at the end.
1383    pub fn db_data(&self, table_name: &str, include_vanilla: bool, include_parent: bool) -> Result<Vec<&RFile>> {
1384        let mut cache = vec![];
1385
1386        if include_vanilla {
1387            if let Some(vanilla_loose_tables) = self.vanilla_loose_tables.get(table_name) {
1388                let mut vanilla_loose_tables = vanilla_loose_tables.to_vec();
1389                vanilla_loose_tables.sort();
1390
1391                for path in &vanilla_loose_tables {
1392                    if let Some(file) = self.vanilla_loose_files.get(path) {
1393                        cache.push(file);
1394                    }
1395                }
1396            }
1397
1398            if let Some(vanilla_tables) = self.vanilla_tables.get(table_name) {
1399                let mut vanilla_tables = vanilla_tables.to_vec();
1400                vanilla_tables.sort();
1401
1402                for path in &vanilla_tables {
1403                    if let Some(file) = self.vanilla_files.get(path) {
1404                        cache.push(file);
1405                    }
1406                }
1407            }
1408        }
1409
1410        if include_parent {
1411            if let Some(parent_tables) = self.parent_tables.get(table_name) {
1412                let mut parent_tables = parent_tables.to_vec();
1413                parent_tables.sort();
1414
1415                for path in &parent_tables {
1416                    if let Some(file) = self.parent_files.get(path) {
1417                        cache.push(file);
1418                    }
1419                }
1420            }
1421        }
1422
1423        Ok(cache)
1424    }
1425
1426    /// This function returns the vanilla/parent db tables from the cache, according to the params you pass it,
1427    /// applying to them any datacore from the provided Pack.
1428    ///
1429    /// It returns them in the order the game will load them.
1430    ///
1431    /// NOTE: table_name is expected to be the table's folder name, with "_tables" at the end.
1432    pub fn db_data_datacored<'a>(&'a self, table_name: &str, packs: &'a BTreeMap<String, Pack>, include_vanilla: bool, include_parent: bool) -> Result<Vec<&'a RFile>> {
1433        let mut cache = vec![];
1434
1435        if include_vanilla {
1436            if let Some(vanilla_loose_tables) = self.vanilla_loose_tables.get(table_name) {
1437                let mut vanilla_loose_tables = vanilla_loose_tables.to_vec();
1438                vanilla_loose_tables.sort();
1439
1440                for path in &vanilla_loose_tables {
1441                    if let Some(file) = self.vanilla_loose_files.get(path) {
1442                        cache.push(file);
1443                    }
1444                }
1445            }
1446
1447            if let Some(vanilla_tables) = self.vanilla_tables.get(table_name) {
1448                let mut vanilla_tables = vanilla_tables.to_vec();
1449                vanilla_tables.sort();
1450
1451                for path in &vanilla_tables {
1452                    if let Some(file) = self.vanilla_files.get(path) {
1453                        cache.push(file);
1454                    }
1455                }
1456            }
1457        }
1458
1459        if include_parent {
1460            if let Some(parent_tables) = self.parent_tables.get(table_name) {
1461                let mut parent_tables = parent_tables.to_vec();
1462                parent_tables.sort();
1463
1464                for path in &parent_tables {
1465                    if let Some(file) = self.parent_files.get(path) {
1466                        cache.push(file);
1467                    }
1468                }
1469            }
1470        }
1471
1472        let paths = cache.iter()
1473            .map(|x| x.path_in_container())
1474            .collect::<Vec<_>>();
1475
1476        for pack in packs.values() {
1477            for pack_file in pack.files_by_paths(&paths, true) {
1478                for cache_file in &mut cache {
1479                    if cache_file.path_in_container() == pack_file.path_in_container() {
1480                        *cache_file = pack_file;
1481                        break;
1482                    }
1483                }
1484            }
1485        }
1486
1487        Ok(cache)
1488    }
1489
1490    /// This function returns the vanilla/parent DB and Loc tables from the cache, according to the params you pass it.
1491    ///
1492    /// It returns them in the order the game will load them.
1493    pub fn db_and_loc_data(&self, include_db: bool, include_loc: bool, include_vanilla: bool, include_parent: bool) -> Result<Vec<&RFile>> {
1494        let mut cache = vec![];
1495
1496        if include_vanilla {
1497            if include_db {
1498                let mut vanilla_loose_tables = self.vanilla_loose_tables.values().flatten().collect::<Vec<_>>();
1499                vanilla_loose_tables.sort();
1500
1501                for path in &vanilla_loose_tables {
1502                    if let Some(file) = self.vanilla_loose_files.get(*path) {
1503                        cache.push(file);
1504                    }
1505                }
1506
1507                let mut vanilla_tables = self.vanilla_tables.values().flatten().collect::<Vec<_>>();
1508                vanilla_tables.sort();
1509
1510                for path in &vanilla_tables {
1511                    if let Some(file) = self.vanilla_files.get(*path) {
1512                        cache.push(file);
1513                    }
1514                }
1515            }
1516
1517            if include_loc {
1518                let mut vanilla_loose_locs = self.vanilla_loose_locs.iter().collect::<Vec<_>>();
1519                vanilla_loose_locs.sort();
1520
1521                for path in &vanilla_loose_locs {
1522                    if let Some(file) = self.vanilla_loose_files.get(*path) {
1523                        cache.push(file);
1524                    }
1525                }
1526
1527                let mut vanilla_locs = self.vanilla_locs.iter().collect::<Vec<_>>();
1528                vanilla_locs.sort();
1529
1530                for path in &vanilla_locs {
1531                    if let Some(file) = self.vanilla_files.get(*path) {
1532                        cache.push(file);
1533                    }
1534                }
1535            }
1536        }
1537
1538        if include_parent {
1539            if include_db {
1540                let mut parent_tables = self.parent_tables.values().flatten().collect::<Vec<_>>();
1541                parent_tables.sort();
1542
1543                for path in &parent_tables {
1544                    if let Some(file) = self.parent_files.get(*path) {
1545                        cache.push(file);
1546                    }
1547                }
1548            }
1549
1550            if include_loc {
1551                let mut parent_locs = self.parent_locs.iter().collect::<Vec<_>>();
1552                parent_locs.sort();
1553
1554                for path in &parent_locs {
1555                    if let Some(file) = self.parent_files.get(*path) {
1556                        cache.push(file);
1557                    }
1558                }
1559            }
1560        }
1561
1562        Ok(cache)
1563    }
1564
1565    //-----------------------------------//
1566    // Advanced Getters.
1567    //-----------------------------------//
1568
1569    /// This function returns the reference/lookup data of all relevant columns of a DB Table.
1570    ///
1571    /// NOTE: This assumes you've populated the runtime references before this. If not, it'll fail.
1572    pub fn db_reference_data(&self, schema: &Schema, packs: &BTreeMap<String, Pack>, table_name: &str, definition: &Definition, loc_data: &Option<HashMap<Cow<str>, Cow<str>>>) -> HashMap<i32, TableReferences> {
1573
1574        // First check if the data is already cached, to speed up things.
1575        //
1576        // NOTE: The None branch should only trigger in cases were there's a bug. We just let it pass without reference instead of crashing.
1577        let mut vanilla_references = match self.local_tables_references.get(table_name) {
1578            Some(cached_data) => cached_data.clone(),
1579            None => HashMap::new(),
1580        };
1581
1582        // If we receive premade loc data (because this may trigger on many files at the same time), don't calculate it here.
1583        let (_loc_files, loc_decoded) = if loc_data.is_some() {
1584            (vec![], vec![])
1585        } else {
1586            let loc_files: Vec<_> = packs.values().flat_map(|pack| pack.files_by_type(&[FileType::Loc])).collect();
1587            let loc_decoded = loc_files.iter()
1588                .filter_map(|file| if let Ok(RFileDecoded::Loc(loc)) = file.decoded() { Some(loc) } else { None })
1589                .map(|file| file.data())
1590                .collect::<Vec<_>>();
1591            (loc_files, loc_decoded)
1592        };
1593
1594        let mut _loc_data_dummy = HashMap::new();
1595        let loc_data = if let Some(ref loc_data) = loc_data {
1596            loc_data
1597        } else {
1598            _loc_data_dummy = loc_decoded.par_iter()
1599                .flat_map(|data| data.par_iter()
1600                    .map(|entry| (entry[0].data_to_string(), entry[1].data_to_string()))
1601                    .collect::<Vec<(_,_)>>()
1602                ).collect::<HashMap<_,_>>();
1603            &_loc_data_dummy
1604        };
1605
1606        // Trick: before doing this, we modify the definition to include any lookup from any reference,
1607        // so we are actually able to catch recursive-like lookups without reading multiple tables.
1608        let mut definition = definition.clone();
1609
1610        // `Definition::patches` is `#[serde(skip)]`, so a definition that came across the IPC
1611        // boundary arrives with empty patches. Repopulate them from the schema's patch set or
1612        // every patch-defined lookup (incl. `lookup_hardcoded`, e.g. the names tables) is lost.
1613        if let Some(table_patches) = schema.patches_for_table(table_name) {
1614            definition.set_patches(table_patches.clone());
1615        }
1616
1617        self.add_recursive_lookups_to_definition(schema, &mut definition, table_name);
1618
1619        let patches = Some(definition.patches());
1620        let fields_processed = definition.fields_processed();
1621        let local_references = fields_processed.par_iter().enumerate().filter_map(|(column, field)| {
1622            match field.is_reference(patches) {
1623                Some((ref ref_table, ref ref_column)) => {
1624                    if !ref_table.is_empty() && !ref_column.is_empty() {
1625
1626                        // Get his lookup data if it has it.
1627                        let lookup_data = if let Some(ref data) = field.lookup_no_patch() { data.to_vec() } else { Vec::with_capacity(0) };
1628                        let mut references = TableReferences::default();
1629                        *references.field_name_mut() = field.name().to_owned();
1630
1631                        let _local_found = self.db_reference_data_from_local_pack(&mut references, (ref_table, ref_column, &lookup_data), packs, loc_data);
1632
1633                        Some((column as i32, references))
1634                    } else { None }
1635                }
1636
1637                // In the fallback case (no references) we still need to check for lookup data within our table and the locs.
1638                None => {
1639                    if let Some(ref lookup_data) = field.lookup_no_patch() {
1640
1641                        // Only single-keyed tables can have lookups.
1642                        if field.is_key(patches) && fields_processed.iter().filter(|x| x.is_key(patches)).count() == 1 {
1643
1644                            // The fallback here is to avoid crashes on packs that have renamed folders.
1645                            let ref_table = if table_name.ends_with("_tables") && table_name.len() > 7 {
1646                                table_name.to_owned().drain(..table_name.len() - 7).collect()
1647                            } else {
1648                                table_name.to_owned()
1649                            };
1650
1651                            let ref_column = field.name();
1652
1653                            // Get his lookup data if it has it.
1654                            let mut references = TableReferences::default();
1655                            *references.field_name_mut() = field.name().to_owned();
1656
1657                            let _local_found = self.db_reference_data_from_local_pack(&mut references, (&ref_table, ref_column, lookup_data), packs, loc_data);
1658
1659                            Some((column as i32, references))
1660                        } else { None }
1661                    } else { None }
1662                }
1663            }
1664        }).collect::<HashMap<_, _>>();
1665
1666        vanilla_references.par_iter_mut().for_each(|(key, value)|
1667            if let Some(local_value) = local_references.get(key) {
1668                value.data.extend(local_value.data.iter().map(|(k, v)| (k.clone(), v.clone())));
1669            }
1670        );
1671
1672        for (index, field) in fields_processed.iter().enumerate() {
1673            match vanilla_references.get_mut(&(index as i32)) {
1674                Some(references) => {
1675                    let hardcoded_lookup = field.lookup_hardcoded(patches);
1676                    if !hardcoded_lookup.is_empty() {
1677                        references.data.extend(hardcoded_lookup);
1678                    }
1679                },
1680                None => {
1681                    let mut references = TableReferences::default();
1682                    *references.field_name_mut() = field.name().to_owned();
1683                    let hardcoded_lookup = field.lookup_hardcoded(patches);
1684                    if !hardcoded_lookup.is_empty() {
1685                        references.data.extend(hardcoded_lookup);
1686                        vanilla_references.insert(index as i32, references);
1687                    }
1688                },
1689            }
1690        }
1691
1692        vanilla_references
1693    }
1694
1695    /// This function returns the reference/lookup data of all relevant columns of a DB Table from the vanilla/parent data.
1696    ///
1697    /// If reference data was found, the most recent definition of said data is returned.
1698    fn db_reference_data_from_vanilla_and_modded_tables(&self, references: &mut TableReferences, reference_info: (&str, &str, &[String])) -> Option<Definition> {
1699        self.db_reference_data_generic(references, reference_info, None, &HashMap::new())
1700    }
1701
1702    /// This function returns the reference/lookup data of all relevant columns of a DB Table from the assembly kit data.
1703    ///
1704    /// It returns true if data is found, otherwise it returns false.
1705    fn db_reference_data_from_asskit_tables(&self, references: &mut TableReferences, reference_info: (&str, &str, &[String])) -> bool {
1706        let ref_table = reference_info.0;
1707        let ref_column = reference_info.1;
1708        let ref_lookup_columns = reference_info.2;
1709
1710        match self.asskit_only_db_tables.get(ref_table) {
1711            Some(table) => {
1712                let fields_processed = table.definition().fields_processed();
1713                let ref_column_index = fields_processed.iter().position(|x| x.name() == ref_column);
1714                let ref_lookup_columns_index = ref_lookup_columns.iter().map(|column| fields_processed.iter().position(|x| x.name() == column)).collect::<Vec<_>>();
1715
1716                for row in &*table.data() {
1717                    let mut reference_data = String::new();
1718                    let mut lookup_data = vec![];
1719
1720                    // First, we get the reference data.
1721                    if let Some(index) = ref_column_index {
1722                        reference_data = row[index].data_to_string().to_string();
1723                    }
1724
1725                    // Then, we get the lookup data.
1726                    for column in ref_lookup_columns_index.iter().flatten() {
1727                        lookup_data.push(row[*column].data_to_string());
1728                    }
1729
1730                    references.data.insert(reference_data, lookup_data.join(" "));
1731                }
1732                true
1733            },
1734            None => false,
1735        }
1736    }
1737
1738    /// This function returns the reference/lookup data of all relevant columns of a DB Table from the provided Pack.
1739    fn db_reference_data_from_local_pack(&self, references: &mut TableReferences, reference_info: (&str, &str, &[String]), packs: &BTreeMap<String, Pack>, loc_data: &HashMap<Cow<str>, Cow<str>>) -> Option<Definition> {
1740        self.db_reference_data_generic(references, reference_info, Some(packs), loc_data)
1741    }
1742
1743    fn db_reference_data_generic(&self, references: &mut TableReferences, reference_info: (&str, &str, &[String]), packs: Option<&BTreeMap<String, Pack>>, loc_data: &HashMap<Cow<str>, Cow<str>>) -> Option<Definition> {
1744        let mut data_found: Option<Definition> = None;
1745
1746        let ref_table = reference_info.0;
1747        let ref_column = reference_info.1;
1748        let ref_lookup_columns = reference_info.2;
1749
1750        let mut cache = HashMap::new();
1751
1752        // Input is not guaranteed to be in one or another format, so sanitize them here.
1753        let ref_table_full = if ref_table.ends_with("_tables") {
1754            ref_table.to_owned()
1755        } else {
1756            ref_table.to_owned() + "_tables"
1757        };
1758
1759        let files = match packs {
1760            Some(packs) => {
1761                let mut files: Vec<&RFile> = packs.values().flat_map(|pack| pack.files_by_path(&ContainerPath::Folder(format!("db/{ref_table_full}")), true)).collect();
1762                files.append(&mut self.db_data(&ref_table_full, true, true).unwrap_or_else(|_| vec![]));
1763                files
1764            },
1765            None => self.db_data(&ref_table_full, true, true).unwrap_or_else(|_| vec![]),
1766        };
1767
1768        let mut table_data_cache: HashMap<String, HashMap<String, String>> = HashMap::new();
1769
1770        files.iter().for_each(|file| {
1771            if let Ok(RFileDecoded::DB(db)) = file.decoded() {
1772                let definition = db.definition();
1773                let fields_processed = definition.fields_processed();
1774
1775                // Only continue if the column we're referencing actually exists.
1776                if let Some(ref_column_index) = fields_processed.iter().position(|x| x.name() == ref_column) {
1777
1778                    // Here we analyze the lookups to build their table cache.
1779                    let lookups_analyzed = ref_lookup_columns.iter().map(|ref_lookup_path| {
1780                        let ref_lookup_steps = ref_lookup_path.split(':').map(|x| x.split('#').collect::<Vec<_>>()).collect::<Vec<_>>();
1781                        let mut is_loc = false;
1782                        let mut col_pos = 0;
1783
1784                        for (index, ref_lookup_step) in ref_lookup_steps.iter().enumerate() {
1785                            if ref_lookup_step.len() == 3 {
1786                                let lookup_ref_table = ref_lookup_step[0];
1787                                let lookup_ref_key = ref_lookup_step[1];
1788                                let lookup_ref_lookup = ref_lookup_step[2];
1789                                let lookup_ref_table_long = lookup_ref_table.to_owned() + "_tables";
1790
1791                                // Build the cache for the tables we need to check.
1792                                if !cache.contains_key(lookup_ref_table) {
1793                                    let mut files = vec![];
1794
1795                                    if let Some(packs) = packs {
1796                                        for pack in packs.values() {
1797                                            files.append(&mut pack.files_by_path(&ContainerPath::Folder(format!("db/{lookup_ref_table_long}")), true));
1798                                        }
1799                                    }
1800
1801                                    // Only add to the cache the files not already there due to being in the pack.
1802                                    for file in self.db_data(&lookup_ref_table_long, true, true).unwrap_or_else(|_| vec![]) {
1803                                        if files.iter().all(|x| x.path_in_container_raw() != file.path_in_container_raw()) {
1804                                            files.push(file);
1805                                        }
1806                                    }
1807
1808                                    if !files.is_empty() {
1809
1810                                        // Make sure they're in order so if the lookup is in a mod, we have to do less iterations to find it.
1811                                        files.sort_by(|a, b| a.path_in_container_raw().cmp(b.path_in_container_raw()));
1812                                        cache.insert(lookup_ref_table.to_owned(), files);
1813                                    }
1814                                }
1815
1816                                // If it's the last step, check if it's a loc, or a table column.
1817                                if index == ref_lookup_steps.len() - 1 {
1818                                    if let Some(file) = cache.get(lookup_ref_table) {
1819                                        if let Some(file) = file.first() {
1820                                            if let Ok(RFileDecoded::DB(db)) = file.decoded() {
1821                                                let definition = db.definition();
1822                                                let fields_processed = definition.fields_processed();
1823                                                let localised_fields = definition.localised_fields();
1824
1825                                                match localised_fields.iter().position(|x| x.name() == lookup_ref_lookup) {
1826                                                    Some(loc_pos) => {
1827                                                        is_loc = true;
1828                                                        col_pos = loc_pos;
1829                                                    },
1830                                                    None => match fields_processed.iter().position(|x| x.name() == lookup_ref_lookup) {
1831                                                        Some(pos) => {
1832                                                            is_loc = false;
1833                                                            col_pos = pos;
1834                                                        },
1835                                                        None => {
1836                                                            //error!("Missing column for lookup. This is a bug.");
1837                                                        },
1838                                                    }
1839                                                }
1840                                            }
1841                                        }
1842                                    }
1843                                }
1844
1845                                // Build the hashed cache for lookups, so we don't need to iterate again and again for each row.
1846                                if let Some(files) = cache.get(lookup_ref_table) {
1847                                    for file in files {
1848                                        let table_data_column_cache_key = file.path_in_container_raw().to_owned() + &ref_lookup_step.join("++");
1849                                        if !table_data_cache.contains_key(&table_data_column_cache_key) {
1850                                            if let Ok(RFileDecoded::DB(db)) = file.decoded() {
1851                                                let definition = db.definition();
1852                                                let fields_processed = definition.fields_processed();
1853                                                let localised_fields = definition.localised_fields();
1854                                                let localised_order = definition.localised_key_order();
1855
1856                                                let loc_key = if is_loc {
1857                                                    if let Some(loc_field) = localised_fields.get(col_pos) {
1858                                                        let mut loc_key = String::with_capacity(2 + lookup_ref_table.len() + loc_field.name().len());
1859                                                        loc_key.push_str(lookup_ref_table);
1860                                                        loc_key.push('_');
1861                                                        loc_key.push_str(loc_field.name());
1862                                                        loc_key.push('_');
1863                                                        loc_key
1864                                                    } else {
1865                                                        String::new()
1866                                                    }
1867                                                } else {
1868                                                    String::new()
1869                                                };
1870
1871                                                if let Some(source_key_column) = fields_processed.iter().position(|x| x.name() == lookup_ref_key) {
1872
1873                                                    // Intermediate step cache.
1874                                                    if index < ref_lookup_steps.len() - 1 {
1875                                                        if let Some(source_lookup_column) = fields_processed.iter().position(|x| x.name() == lookup_ref_lookup) {
1876                                                            let cache = db.data().iter()
1877                                                                .map(|row| (row[source_key_column].data_to_string().to_string(), row[source_lookup_column].data_to_string().to_string()))
1878                                                                .collect::<HashMap<_,_>>();
1879
1880                                                            table_data_cache.insert(table_data_column_cache_key.clone(), cache);
1881                                                        }
1882                                                    }
1883
1884                                                    // Locs are already pre-cached. We only need the final part of their key.
1885                                                    else if is_loc {
1886                                                        let cache = db.data().iter()
1887                                                            .map(|row| {
1888                                                                let mut loc_key = loc_key.to_owned();
1889                                                                loc_key.push_str(&localised_order.iter().map(|pos| row[*pos as usize].data_to_string()).join(""));
1890                                                                (row[source_key_column].data_to_string().to_string(), loc_key)
1891                                                            })
1892                                                            .collect::<HashMap<_,_>>();
1893                                                        table_data_cache.insert(table_data_column_cache_key.clone(), cache);
1894                                                    }
1895
1896                                                    else {
1897                                                        let cache = db.data().iter()
1898                                                            .map(|row| (row[source_key_column].data_to_string().to_string(), row[col_pos].data_to_string().to_string()))
1899                                                            .collect::<HashMap<_,_>>();
1900
1901                                                        table_data_cache.insert(table_data_column_cache_key.clone(), cache);
1902                                                    }
1903                                                }
1904                                            }
1905                                        }
1906                                    }
1907                                }
1908                            } else {
1909                                error!("Badly built lookup. This is a bug.");
1910                            }
1911                        }
1912
1913                        (ref_lookup_steps, is_loc)
1914
1915                    }).collect::<Vec<_>>();
1916
1917                    let data = db.data();
1918                    for row in &*data {
1919                        let mut lookup_data = Vec::with_capacity(lookups_analyzed.len());
1920
1921                        // First, we get the reference data.
1922                        let reference_data = row[ref_column_index].data_to_string();
1923
1924                        // Then, we get the lookup data. Only calculate it for non-empty keys.
1925                        for (lookup_steps, is_loc) in lookups_analyzed.iter() {
1926                            if !reference_data.is_empty() {
1927
1928                                if let Some(lookup) = self.db_reference_data_generic_lookup(&cache, loc_data, &reference_data, lookup_steps, *is_loc, &table_data_cache) {
1929                                    lookup_data.push(lookup);
1930                                }
1931                            }
1932                        }
1933
1934                        references.data.insert(reference_data.to_string(), lookup_data.into_iter().join(":"));
1935                    }
1936
1937                    // Once done with the table, check if we should return its definition.
1938                    match data_found {
1939                        Some(ref definition) => {
1940                            if db.definition().version() > definition.version() {
1941                                data_found = Some(db.definition().clone());
1942                            }
1943                        }
1944
1945                        None => data_found = Some(db.definition().clone()),
1946                    }
1947                }
1948            }
1949        });
1950
1951        data_found
1952    }
1953
1954    fn db_reference_data_generic_lookup(
1955        &self,
1956        cache: &HashMap<String, Vec<&RFile>>,
1957        loc_data: &HashMap<Cow<str>, Cow<str>>,
1958        lookup_key: &str,
1959        lookup_steps: &[Vec<&str>],
1960        is_loc: bool,
1961        table_data_cache: &HashMap<String, HashMap<String, String>>
1962    ) -> Option<String> {
1963        let mut data_found: Option<String> = None;
1964
1965        if lookup_steps.is_empty() {
1966            return None;
1967        }
1968
1969        let current_step = &lookup_steps[0];
1970        let source_table = current_step[0];
1971
1972        if let Some(files) = cache.get(source_table) {
1973            for file in files {
1974                let table_data_column_cache_key = file.path_in_container_raw().to_owned() + &current_step.join("++");
1975                if let Some(table_data_column_cache) = table_data_cache.get(&table_data_column_cache_key) {
1976
1977                    if let Some(lookup_value) = table_data_column_cache.get(lookup_key) {
1978
1979                        // If we're not yet in the last step, reduce the steps and repeat.
1980                        if lookup_steps.len() > 1 {
1981                            if !lookup_value.is_empty() {
1982                                data_found = self.db_reference_data_generic_lookup(cache, loc_data, lookup_value, &lookup_steps[1..], is_loc, table_data_cache);
1983                            }
1984                        }
1985
1986                        // If we're on the last step, properly get the lookup data. Locs first.
1987                        else if is_loc {
1988
1989                            if let Some(data) = loc_data.get(&**lookup_value) {
1990                                data_found = Some(data.to_string());
1991                            } else if let Some(data) = self.localisation_data.get(&**lookup_value) {
1992                                data_found = Some(data.to_string());
1993                            } else {
1994                                data_found = Some(lookup_value.to_string())
1995                            }
1996                        }
1997
1998                        // Then table columns.
1999                        else {
2000                            data_found = Some(lookup_value.to_owned());
2001                        }
2002
2003                        // If we find a match, don't bother with the rest of the files.
2004                        break;
2005                    }
2006                }
2007            }
2008        }
2009
2010        data_found
2011    }
2012
2013    /// This function returns the table/column/key from the provided loc key.
2014    ///
2015    /// We return the table without "_tables". Keep that in mind if you use this.
2016    pub fn loc_key_source(&self, key: &str) -> Option<(String, String, Vec<String>)> {
2017        let key_split = key.split('_').collect::<Vec<_>>();
2018
2019        // We don't know how much of the string the key the table is, so we try removing parts until we find a table that matches.
2020        // in reverse so longer table names have priority in case of collision.
2021        for (index, _) in key_split.iter().enumerate().rev() {
2022
2023            // Index 0 would mean empty table name.
2024            if index >= 1 {
2025
2026                let mut table_name = key_split[..index].join("_");
2027                let full_table_name = format!("{table_name}_tables");
2028
2029                if let Ok(rfiles) = self.db_data(&full_table_name, true, false) {
2030                    let mut decoded = rfiles.iter()
2031                        .filter_map(|x| if let Ok(RFileDecoded::DB(table)) = x.decoded() {
2032                            Some(table)
2033                        } else {
2034                            None
2035                        }).collect::<Vec<_>>();
2036
2037                    // Also add the ak files if present.
2038                    if let Some(ak_file) = self.asskit_only_db_tables().get(&full_table_name) {
2039                        decoded.push(ak_file);
2040                    }
2041
2042                    for table in decoded {
2043                        let definition = table.definition();
2044                        let localised_fields = definition.localised_fields();
2045                        let localised_key_order = definition.localised_key_order();
2046                        if !localised_fields.is_empty() {
2047                            let mut field = String::new();
2048
2049                            // Loop to get the column.
2050                            for (second_index, value) in key_split[index..].iter().enumerate() {
2051                                field.push_str(value);
2052
2053                                if localised_fields.iter().any(|x| x.name() == field) {
2054
2055                                    // If we reached this, the rest is the value.
2056                                    let key_data = &key_split[index + second_index + 1..].join("_");
2057
2058                                    // Once we get the key, we need to use the stored loc order to find out to what specific line it belongs.
2059                                    // And yes, this means checking every single fucking line in every single table.
2060                                    let data = table.data();
2061                                    for row in data.iter() {
2062                                        let generated_key_split = localised_key_order.iter().map(|col| row[*col as usize].data_to_string()).collect::<Vec<_>>();
2063                                        let generated_key = generated_key_split.join("");
2064                                        if &generated_key == key_data {
2065                                            return Some((table_name, field, generated_key_split.iter().map(|x| x.to_string()).collect()));
2066                                        }
2067                                    }
2068                                }
2069
2070                                field.push('_');
2071                            }
2072                        }
2073                    }
2074                }
2075
2076                // Add an underscore before adding the next part of the table name in the next loop.
2077                table_name.push('_');
2078            }
2079        }
2080
2081        None
2082    }
2083
2084    //-----------------------------------//
2085    // Utility functions.
2086    //-----------------------------------//
2087
2088    /// This function returns if a specific file exists in the dependencies cache.
2089    pub fn file_exists(&self, file_path: &str, include_vanilla: bool, include_parent: bool, case_insensitive: bool) -> bool {
2090        if include_parent {
2091            if self.parent_files.contains_key(file_path) {
2092                return true
2093            } else if case_insensitive {
2094                let lower = file_path.to_lowercase();
2095                if self.parent_paths.contains_key(&lower) {
2096                    return true
2097                }
2098            }
2099        }
2100
2101        if include_vanilla {
2102
2103            if self.vanilla_files.contains_key(file_path) || self.vanilla_loose_files.contains_key(file_path) {
2104                return true
2105            } else if case_insensitive {
2106                let lower = file_path.to_lowercase();
2107                if self.vanilla_paths.contains_key(&lower) || self.vanilla_loose_paths.contains_key(&lower) {
2108                    return true
2109                }
2110            }
2111        }
2112
2113        false
2114    }
2115
2116    /// This function returns if a specific folder exists in the dependencies cache.
2117    pub fn folder_exists(&self, folder_path: &str, include_vanilla: bool, include_parent: bool, case_insensitive: bool) -> bool {
2118        if include_parent && (
2119            self.parent_folders.contains(folder_path) ||
2120            (case_insensitive && self.parent_folders.par_iter().any(|path| caseless::canonical_caseless_match_str(path, folder_path)))
2121        ) {
2122            return true
2123        }
2124
2125        if include_vanilla && (
2126            (self.vanilla_folders.contains(folder_path) || self.vanilla_loose_folders.contains(folder_path)) ||
2127            (case_insensitive && self.vanilla_folders.par_iter().chain(self.vanilla_loose_folders.par_iter()).any(|path| caseless::canonical_caseless_match_str(path, folder_path)))
2128        ) {
2129            return true
2130        }
2131
2132        false
2133    }
2134
2135    /// This function checks if the dependencies cache file exists on disk.
2136    pub fn are_dependencies_generated(file_path: &Path) -> bool {
2137        file_path.is_file()
2138    }
2139
2140    /// This function checks if there is vanilla data loaded in the provided cache.
2141    pub fn is_vanilla_data_loaded(&self, include_asskit: bool) -> bool {
2142        if include_asskit {
2143            !self.vanilla_files.is_empty() && self.is_asskit_data_loaded()
2144        } else {
2145            !self.vanilla_files.is_empty()
2146        }
2147    }
2148
2149    /// This function checks if there is assembly kit data loaded in the provided cache.
2150    pub fn is_asskit_data_loaded(&self) -> bool {
2151        !self.asskit_only_db_tables.is_empty()
2152    }
2153
2154    /// This function is used to check if a table is outdated or not.
2155    pub fn is_db_outdated(&self, rfile: &RFileDecoded) -> bool {
2156        if let RFileDecoded::DB(data) = rfile {
2157            let dep_db_undecoded = if let Ok(undecoded) = self.db_data(data.table_name(), true, false) { undecoded } else { return false };
2158            let dep_db_decoded = dep_db_undecoded.iter().filter_map(|x| if let Ok(RFileDecoded::DB(decoded)) = x.decoded() { Some(decoded) } else { None }).collect::<Vec<_>>();
2159
2160            if let Some(vanilla_db) = dep_db_decoded.iter().max_by(|x, y| x.definition().version().cmp(y.definition().version())) {
2161                if vanilla_db.definition().version() > data.definition().version() {
2162                    return true;
2163                }
2164            }
2165        }
2166
2167        false
2168    }
2169
2170    /// This function is used to get the version of a table in the game files, if said table is in the game files.
2171    pub fn db_version(&self, table_name: &str) -> Option<i32> {
2172        let tables = self.vanilla_tables.get(table_name)?;
2173        for table_path in tables {
2174
2175            let table = self.vanilla_files.get(table_path)?;
2176            if let RFileDecoded::DB(table) = table.decoded().ok()? {
2177                return Some(*table.definition().version());
2178            }
2179
2180            let table = self.vanilla_loose_files.get(table_path)?;
2181            if let RFileDecoded::DB(table) = table.decoded().ok()? {
2182                return Some(*table.definition().version());
2183            }
2184        }
2185
2186        None
2187    }
2188
2189    /// This function returns the list of values a column of a table has, across all instances of said table in the dependencies and the provided Packs.
2190    pub fn db_values_from_table_name_and_column_name(&self, packs: Option<&BTreeMap<String, Pack>>, table_name: &str, column_name: &str, include_vanilla: bool, include_parent: bool) -> HashSet<String> {
2191        let mut values = HashSet::new();
2192
2193        if let Ok(files) = self.db_data(table_name, include_vanilla, include_parent) {
2194            values.extend(files.par_iter().filter_map(|file| {
2195                if let Ok(RFileDecoded::DB(table)) = file.decoded() {
2196                    table.definition().column_position_by_name(column_name).map(|column| table.data().par_iter().map(|row| row[column].data_to_string().to_string()).collect::<Vec<_>>())
2197                } else { None }
2198            }).flatten().collect::<Vec<_>>());
2199        }
2200
2201        if let Some(packs) = packs {
2202            for pack in packs.values() {
2203                let files = pack.files_by_path(&ContainerPath::Folder(format!("db/{table_name}")), true);
2204                values.extend(files.par_iter().filter_map(|file| {
2205                    if let Ok(RFileDecoded::DB(table)) = file.decoded() {
2206                        table.definition().column_position_by_name(column_name).map(|column| table.data().par_iter().map(|row| row[column].data_to_string().to_string()).collect::<Vec<_>>())
2207                    } else { None }
2208                }).flatten().collect::<Vec<_>>());
2209            }
2210        }
2211
2212        values
2213    }
2214
2215    /// This function returns the value a table has in the row it has a specific value in a specific column.
2216    pub fn db_values_from_table_name_and_column_name_for_value(&self, packs: Option<&BTreeMap<String, Pack>>, table_name: &str, key_column_name: &str, desired_column_name: &str, include_vanilla: bool, include_parent: bool) -> HashMap<String, String> {
2217        let mut values = HashMap::new();
2218
2219        if let Ok(files) = self.db_data(table_name, include_vanilla, include_parent) {
2220            values.extend(files.par_iter().filter_map(|file| {
2221                if let Ok(RFileDecoded::DB(table)) = file.decoded() {
2222                    if let Some(column) = table.definition().column_position_by_name(key_column_name) {
2223                        table.definition().column_position_by_name(desired_column_name).map(|desired_column| table.data().par_iter().map(|row| (row[column].data_to_string().to_string(), row[desired_column].data_to_string().to_string())).collect::<Vec<_>>())
2224                    } else { None }
2225                } else { None }
2226            }).flatten().collect::<Vec<_>>());
2227        }
2228
2229        if let Some(packs) = packs {
2230            for pack in packs.values() {
2231                let files = pack.files_by_path(&ContainerPath::Folder(format!("db/{table_name}")), true);
2232                values.extend(files.par_iter().filter_map(|file| {
2233                    if let Ok(RFileDecoded::DB(table)) = file.decoded() {
2234                        if let Some(column) = table.definition().column_position_by_name(key_column_name) {
2235                            table.definition().column_position_by_name(desired_column_name).map(|desired_column| table.data().par_iter().map(|row| (row[column].data_to_string().to_string(), row[desired_column].data_to_string().to_string())).collect::<Vec<_>>())
2236                        } else { None }
2237                    } else { None }
2238                }).flatten().collect::<Vec<_>>());
2239            }
2240        }
2241
2242        values
2243    }
2244
2245    /// This function updates a DB Table to its latest valid version, being the latest valid version the one in the vanilla files.
2246    ///
2247    /// It returns both, old and new versions, or an error.
2248    pub fn update_db(&mut self, rfile: &mut RFileDecoded) -> Result<(i32, i32, Vec<String>, Vec<String>)> {
2249        match rfile {
2250            RFileDecoded::DB(data) => {
2251                let dep_db_undecoded = self.db_data(data.table_name(), true, false)?;
2252                let dep_db_decoded = dep_db_undecoded.iter().filter_map(|x| if let Ok(RFileDecoded::DB(decoded)) = x.decoded() { Some(decoded) } else { None }).collect::<Vec<_>>();
2253
2254                if let Some(vanilla_db) = dep_db_decoded.iter().max_by(|x, y| x.definition().version().cmp(y.definition().version())) {
2255
2256                    let definition_new = vanilla_db.definition();
2257                    let definition_old = data.definition().clone();
2258                    if definition_old != *definition_new {
2259                        data.set_definition(definition_new);
2260
2261                        // Get the info about the definition differences.
2262                        let fields_old = definition_old.fields_processed();
2263                        let fields_new = definition_new.fields_processed();
2264                        let fields_deleted = fields_old.iter()
2265                            .filter(|x| fields_new.iter().all(|y| y.name() != x.name()))
2266                            .map(|x| x.name().to_owned())
2267                            .collect::<Vec<_>>();
2268                        let fields_added = fields_new.iter()
2269                            .filter(|x| fields_old.iter().all(|y| y.name() != x.name()))
2270                            .map(|x| x.name().to_owned())
2271                            .collect::<Vec<_>>();
2272
2273                        Ok((*definition_old.version(), *definition_new.version(), fields_deleted, fields_added))
2274                    }
2275                    else {
2276                        Err(RLibError::NoDefinitionUpdateAvailable)
2277                    }
2278                }
2279                else { Err(RLibError::NoTableInGameFilesToCompare) }
2280            }
2281            _ => Err(RLibError::DecodingDBNotADBTable),
2282        }
2283    }
2284
2285    /// Function to generate the missing loc entries in a pack.
2286    pub fn generate_missing_loc_data(&self, packs: &mut BTreeMap<String, Pack>) -> Result<Vec<ContainerPath>> {
2287        let loc_data = self.loc_data(true, true)?;
2288        let mut existing_locs = HashMap::new();
2289
2290        for loc in &loc_data {
2291            if let Ok(RFileDecoded::Loc(ref data)) = loc.decoded() {
2292                existing_locs.extend(data.table().data().iter().map(|x| (x[0].data_to_string().to_string(), x[1].data_to_string().to_string())));
2293            }
2294        }
2295
2296        let mut all_paths = vec![];
2297        for pack in packs.values_mut() {
2298            all_paths.extend(pack.generate_missing_loc_data(&existing_locs)?);
2299        }
2300        Ok(all_paths)
2301    }
2302
2303    /// This function bruteforces the order in which multikeyed tables get their keys together for loc entries.
2304    pub fn bruteforce_loc_key_order(&self, schema: &mut Schema, locs: Option<HashMap<String, Vec<String>>>, local_packs: Option<&BTreeMap<String, Pack>>, mut ak_files: Option<&mut HashMap<String, DB>>) -> Result<()> {
2305        let mut fields_still_not_found = vec![];
2306
2307        // Get all vanilla loc keys into a big hashmap so we can check them fast.
2308        let loc_files = self.loc_data(true, false)?;
2309        let loc_table = loc_files.iter()
2310            .filter_map(|file| if let Ok(RFileDecoded::Loc(loc)) = file.decoded() { Some(loc) } else { None })
2311            .flat_map(|file| file.data().to_vec())
2312            .map(|entry| (entry[0].data_to_string().to_string(), entry[1].data_to_string().to_string()))
2313            .collect::<HashMap<_,_>>();
2314
2315        let ak_tables = match ak_files {
2316            Some(ref tables) => (**tables).clone(),
2317            None => HashMap::new(),
2318        };
2319
2320        // This is to fix bruteforcing not working on tables like campaigns.
2321        let local_files: Vec<_> = match local_packs {
2322            Some(packs) => packs.values()
2323                .flat_map(|pack| pack.files_by_type(&[FileType::DB]))
2324                .filter_map(|x| match x.decoded() {
2325                    Ok(RFileDecoded::DB(db)) => Some(db),
2326                    _ => None,
2327                })
2328                .collect(),
2329            None => Vec::new(),
2330        };
2331
2332        // Get all the tables so we don't need to re-fetch each table individually.
2333        let mut db_tables = if ak_files.is_some() {
2334            ak_tables.values().collect::<Vec<_>>()
2335        } else {
2336            self.db_and_loc_data(true, false, true, false)?
2337                .iter()
2338                .filter_map(|file| if let Ok(RFileDecoded::DB(table)) = file.decoded() { Some(table) } else { None })
2339                .collect::<Vec<_>>()
2340        };
2341
2342        db_tables.extend_from_slice(&local_files);
2343
2344        // Merge tables of the same name and version, so we got more chances of loc data being found.
2345        let mut db_tables_dedup: Vec<DB> = vec![];
2346        for table in &db_tables {
2347            match db_tables_dedup.iter_mut().find(|x| x.table_name() == table.table_name() && x.definition().version() == table.definition().version()) {
2348                Some(db_source) => *db_source = DB::merge(&[db_source, table])?,
2349                None => db_tables_dedup.push((*table).clone()),
2350            }
2351        }
2352
2353        for table in &db_tables_dedup {
2354            let definition = table.definition();
2355            let mut loc_fields = definition.localised_fields().to_vec();
2356
2357            // We assume the fields that came with the table are correct, as they probably come from the normal procedure to get these.
2358            //let mut loc_fields_final: Vec<Field> = vec![];
2359            let mut loc_fields_final = loc_fields.to_vec();
2360
2361            // If we received possible loc info, add the one we received.
2362            if let Some(ref loc_fields_info) = locs {
2363                loc_fields.clear();
2364
2365                if let Some(loc_names) = loc_fields_info.get(&table.table_name_without_tables()) {
2366                    for name in loc_names {
2367                        if loc_fields.iter().all(|x| x.name() != name) {
2368
2369                            let mut field = Field::default();
2370                            field.set_name(name.to_string());
2371                            field.set_field_type(FieldType::StringU8);
2372
2373                            loc_fields.push(field);
2374                        }
2375                    }
2376                }
2377            }
2378
2379            let fields = definition.fields_processed();
2380            let key_fields = fields.iter()
2381                .enumerate()
2382                .filter(|(_, field)| field.is_key(None))
2383                .collect::<Vec<_>>();
2384
2385            // Check which fields from the missing field list are actually loc fields.
2386            let short_table_name = table.table_name_without_tables();
2387            for localised_field in &loc_fields {
2388                let localised_key = format!("{}_{}_", short_table_name, localised_field.name());
2389
2390                // Note: the second check is to avoid a weird bug I'm still not sure why it happens where loc fields get duplicated.
2391                if loc_table.keys().any(|x| x.starts_with(&localised_key)) && loc_fields_final.iter().all(|x| x.name() != localised_field.name()) {
2392                    loc_fields_final.push(localised_field.clone());
2393                }
2394            }
2395
2396            // Some fields fail the previous check because the table contains a field with the same name. So we must repeat it with the table fields.
2397            // There is a weird corner case here where a localised field may start like the name of another table field. We need to avoid that.
2398            for table_field in &fields {
2399                if loc_fields_final.iter().all(|x| !x.name().starts_with(table_field.name())) {
2400                    let localised_key = format!("{}_{}_", short_table_name, table_field.name());
2401                    if loc_table.keys().any(|x| x.starts_with(&localised_key)) && loc_fields_final.iter().all(|x| x.name() != table_field.name()) {
2402                        loc_fields_final.push(table_field.clone());
2403                    }
2404                }
2405            }
2406
2407            for loc_field in &loc_fields {
2408                if loc_fields_final.iter().all(|x| x.name() != loc_field.name()) {
2409                    fields_still_not_found.push(format!("{}/{}", table.table_name_without_tables(), loc_field.name()));
2410                }
2411            }
2412
2413            // Save the loc fields.
2414            if let Some(ak_files) = &mut ak_files {
2415                let ak_table = ak_files.get_mut(table.table_name()).unwrap();
2416                let mut definition = ak_table.definition().clone();
2417                definition.set_localised_fields(loc_fields_final.to_vec());
2418                ak_table.set_definition(&definition);
2419
2420            } else if let Some(schema_definition) = schema.definition_by_name_and_version_mut(table.table_name(), *definition.version()) {
2421                schema_definition.set_localised_fields(loc_fields_final.to_vec());
2422            }
2423
2424            // If after updating the loc data we have loc fields, try to find the key order for them.
2425            if !loc_fields_final.is_empty() {
2426
2427                // If we only have one key field, don't bother searching.
2428                let order = if key_fields.len() == 1 {
2429                    vec![key_fields[0].0 as u32]
2430                }
2431
2432                // If we have multiple key fields, we need to test for combinations.
2433                else {
2434                    let mut order = Vec::with_capacity(key_fields.len());
2435                    let combos = key_fields.iter().permutations(key_fields.len());
2436                    let table_data = table.data();
2437                    for combo in combos {
2438
2439                        // Many multikeyed tables admit empty values as part of the key. We need rows with no empty values.
2440                        // NOTE: While we just need one line to get the order, we check every line to avoid wrong orders due to first line sharing fields.
2441                        let mut combo_is_valid = true;
2442                        for row in table_data.iter() {
2443                            //for (index, _) in &combo {
2444                            //    if row[*index].data_to_string().is_empty() {
2445                            //        fail_due_to_empty_keys_in_combos = true;
2446                            //        //break;
2447                            //    }
2448                            //}
2449
2450                            let mut combined_key = String::new();
2451                            for (index, _) in &combo {
2452                                combined_key.push_str(&row[*index].data_to_string());
2453                            }
2454
2455                            for localised_field in &loc_fields_final {
2456                                let localised_key = format!("{}_{}_{}", short_table_name, localised_field.name(), combined_key);
2457                                match loc_table.get(&localised_key) {
2458                                    Some(_) => {
2459                                        if order.is_empty() {
2460                                            order = combo.iter().map(|(index, _)| *index as u32).collect();
2461                                        }
2462                                    }
2463                                    None => {
2464                                        combo_is_valid = false;
2465                                        break;
2466                                    }
2467                                }
2468                            }
2469
2470                            // If the combo was not valid for a loc field on a line, stop.
2471                            if !combo_is_valid {
2472                                break;
2473                            }
2474                        }
2475
2476                        // If the combo is not valid, reset the order and try the next one.
2477                        if !combo_is_valid {
2478                            order = vec![];
2479                            continue;
2480                        }
2481
2482                        if !order.is_empty() {
2483                            break;
2484                        }
2485                    }
2486
2487                    order
2488                };
2489
2490                if !order.is_empty() && !loc_fields_final.is_empty() {
2491                    info!("Bruteforce: loc key order found for table {}, version {}.", table.table_name(), definition.version());
2492                    if let Some(ak_files) = &mut ak_files {
2493                        let ak_table = ak_files.get_mut(table.table_name()).unwrap();
2494                        let mut definition = ak_table.definition().clone();
2495                        definition.set_localised_key_order(order);
2496                        ak_table.set_definition(&definition);
2497                    } else if let Some(schema_definition) = schema.definition_by_name_and_version_mut(table.table_name(), *definition.version()) {
2498                        schema_definition.set_localised_key_order(order);
2499                    }
2500                } else {
2501                    info!("Bruteforce: loc key order found (but may be incorrect) for table {}, version {}.", table.table_name(), definition.version());
2502
2503                    // If we don't have locs, make sure to delete any order we had.
2504                    if loc_fields_final.is_empty() {
2505                        if let Some(ak_files) = &mut ak_files {
2506                            let ak_table = ak_files.get_mut(table.table_name()).unwrap();
2507                            let mut definition = ak_table.definition().clone();
2508                            definition.set_localised_key_order(vec![]);
2509                            ak_table.set_definition(&definition);
2510                        } else if let Some(schema_definition) = schema.definition_by_name_and_version_mut(table.table_name(), *definition.version()) {
2511                            schema_definition.set_localised_key_order(vec![]);
2512                        }
2513                    }
2514                }
2515            }
2516
2517            // Make sure to cleanup any past mess here.
2518            else if let Some(ak_files) = &mut ak_files {
2519                let ak_table = ak_files.get_mut(table.table_name()).unwrap();
2520                let mut definition = ak_table.definition().clone();
2521                definition.set_localised_key_order(vec![]);
2522                ak_table.set_definition(&definition);
2523            } else if let Some(schema_definition) = schema.definition_by_name_and_version_mut(table.table_name(), *definition.version()) {
2524                schema_definition.set_localised_key_order(vec![]);
2525            }
2526        }
2527
2528        // Dedup this list, because if the game had multiple table files, we'll get duplicated fields.
2529        fields_still_not_found.sort();
2530        fields_still_not_found.dedup();
2531        info!("Bruteforce: fields still not found :{fields_still_not_found:#?}");
2532
2533        // Once everything is done, run a check on the loc keys to see if any of them still doesn't match any table/field combo.
2534        // This will fail if called on cache generation. Only execute it when updating the schema.
2535        if ak_files.is_none() {
2536            for key in loc_table.keys().sorted() {
2537                if self.loc_key_source(key).is_none() {
2538                    info!("-- Bruteforce: cannot find source for loc key {key}.");
2539                }
2540            }
2541        }
2542
2543        Ok(())
2544    }
2545
2546    /// This function generates automatic schema patches based mainly on bruteforcing and some clever logic.
2547    #[allow(clippy::if_same_then_else)]
2548    pub fn generate_automatic_patches(&self, schema: &mut Schema, packs: &BTreeMap<String, Pack>) -> Result<()> {
2549        let mut db_tables = self.db_and_loc_data(true, false, true, false)?
2550            .iter()
2551            .filter_map(|file| if let Ok(RFileDecoded::DB(table)) = file.decoded() { Some(table) } else { None })
2552            .collect::<Vec<_>>();
2553
2554        for pack in packs.values() {
2555            db_tables.extend_from_slice(&pack.files_by_type(&[FileType::DB])
2556                .iter()
2557                .filter_map(|x| if let Ok(RFileDecoded::DB(db)) = x.decoded() {
2558                    Some(db)
2559                } else {
2560                    None
2561                })
2562                .collect::<Vec<_>>()
2563            );
2564        }
2565
2566        let current_patches = schema.patches_mut();
2567        let mut new_patches: HashMap<String, DefinitionPatch> = HashMap::new();
2568
2569        // Cache all image and video paths.
2570        let image_paths = self.vanilla_files()
2571            .keys()
2572            .filter(|x| x.ends_with(".png") || x.ends_with(".tga"))
2573            .collect::<Vec<_>>();
2574
2575        let video_paths = self.vanilla_files()
2576            .keys()
2577            .filter(|x| x.ends_with(".ca_vp8"))
2578            .collect::<Vec<_>>();
2579
2580        for table in &db_tables {
2581            let definition = table.definition();
2582            let fields = definition.fields_processed();
2583            for (column, field) in fields.iter().enumerate() {
2584                match field.field_type() {
2585                    FieldType::StringU8 |
2586                    FieldType::StringU16 |
2587                    FieldType::OptionalStringU8 |
2588                    FieldType::OptionalStringU16 => {
2589
2590                        // Icons can be found by:
2591                        // - Checking if the data contains ".png" or ".tga".
2592                        // - Checking if the data contains "Icon" or "Image" in the name.
2593                        //
2594                        // Note that if the field contains incomplete/relative paths, this will guess and try to find unique files that match the path.
2595                        let mut possible_icon = false;
2596                        let low_name = field.name().to_lowercase();
2597                        if (low_name.contains("icon") || low_name.contains("image")) &&
2598
2599                            // Attila. This doesn't match with anything that makes sense.
2600                            !(table.table_name() == "building_sets_tables" && field.name() == "icon") &&
2601
2602                            // This really should be called category. It's wrong in the ak.
2603                            !(table.table_name() == "character_traits_tables" && field.name() == "icon") {
2604                            possible_icon = true;
2605                        }
2606
2607                        // Use hashset for uniqueness and ram usage.
2608                        let mut possible_relative_paths = table.data().par_iter()
2609                            .filter_map(|row| {
2610
2611                                // Only check fields that are not already marked, or are marked but without path (like override_icon in incidents).
2612                                if !field.is_filename(None) || (
2613                                        field.is_filename(None) && (
2614                                            field.filename_relative_path(None).is_none() ||
2615                                            field.filename_relative_path(None).unwrap().is_empty()
2616                                        )
2617                                    ) || (
2618
2619                                        // This table has an incorrect path by default.
2620                                        (table.table_name() == "advisors_tables" && field.name() == "advisor_icon_path") ||
2621
2622                                        // This one is missing subpaths.
2623                                        (table.table_name() == "campaign_post_battle_captive_options_tables" && field.name() == "icon_path") ||
2624
2625                                        // This one for some reason points to "working_data" and has no replacement bit.
2626                                        (table.table_name() == "narrative_viewer_tabs_tables" && field.name() == "image_path") ||
2627
2628                                        // This one has a path missing the replacement bits.
2629                                        (table.table_name() == "technology_ui_groups_tables" && field.name() == "optional_background_image")
2630                                    ) {
2631
2632                                    // These checks filter out certain problematic cell values:
2633                                    // - .: means empty in some image fields.
2634                                    // - x: means empty in some image fields.
2635                                    // - placeholder: because it's in multiple places and generates false positives.
2636                                    let mut data = row[column].data_to_string().to_lowercase().replace("\\", "/");
2637
2638                                    // Fix formatting for cells which start with / or \\.
2639                                    if data.starts_with("/") {
2640                                        if data.len() > 1 {
2641                                            data = data[1..].to_owned();
2642                                        } else {
2643                                            data = String::new();
2644                                        }
2645                                    }
2646
2647                                    if !data.is_empty() && !data.ends_with("/") &&
2648                                        data != "." &&
2649                                        data != "x" &&
2650                                        data != "false" &&
2651                                        data != "building_placeholder" &&
2652                                        data != "placehoder.png" &&
2653                                        data != "placeholder" &&
2654                                        data != "placeholder.tga" &&
2655                                        data != "placeholder.png" && (
2656                                            possible_icon ||
2657                                            data.ends_with(".png") || data.ends_with(".tga")
2658                                        ) {
2659
2660                                        let possible_paths = image_paths.iter()
2661
2662                                            // Manual filters for some fields that are known to trigger hard-to-fix false positives.
2663                                            .filter(|x| {
2664                                                if table.table_name() == "aide_de_camp_speeches_tables" && field.name() == "icon_name" {
2665                                                    x.starts_with("ui/battle ui/adc_icons/")
2666                                                } else if table.table_name() == "agent_string_subculture_overrides_tables" && field.name() == "icon_path" {
2667                                                    x.starts_with("ui/campaign ui/agents/icons/")
2668                                                } else if table.table_name() == "ancillary_types_tables" && field.name() == "ui_icon" {
2669                                                    x.starts_with("ui/portraits/ancillaries/")
2670                                                } else if table.table_name() == "battlefield_building_categories_tables" && field.name() == "icon_path" {
2671                                                    x.starts_with("ui/battle ui/building icons/")
2672                                                } else if table.table_name() == "bonus_value_uis_tables" && field.name() == "icon" {
2673                                                    x.starts_with("ui/campaign ui/effect_bundles/")
2674                                                } else if table.table_name() == "building_culture_variants_tables" && field.name() == "icon" {
2675                                                    x.starts_with("ui/buildings/icons/")
2676                                                } else if table.table_name() == "campaign_payload_ui_details_tables" && field.name() == "icon" {
2677                                                    x.starts_with("ui/campaign ui/effect_bundles/")
2678                                                } else if table.table_name() == "campaign_post_battle_captive_options_tables" && field.name() == "icon_path" {
2679                                                    x.starts_with("ui/campaign ui/captive_option_icons/")
2680                                                } else if table.table_name() == "capture_point_types_tables" && field.name() == "icon_name" {
2681                                                    x.starts_with("ui/battle ui/capture_point_icons/")
2682                                                } else if table.table_name() == "character_skills_tables" && field.name() == "image_path" {
2683                                                    x.starts_with("ui/campaign ui/skills/")
2684                                                } else if table.table_name() == "character_traits_tables" && field.name() == "icon_custom" {
2685                                                    x.starts_with("ui/campaign ui/effect_bundles/")
2686
2687                                                // This is to fix issues with incomplete cursor paths.
2688                                                } else if table.table_name() == "cursors_tables" && field.name() == "image" {
2689                                                    !x.starts_with(&(data.to_owned() + "_"))
2690                                                } else if table.table_name() == "dilemmas_tables" && field.name() == "ui_image" {
2691                                                    x.starts_with("ui/eventpics/")
2692                                                } else if table.table_name() == "effect_bundles_tables" && field.name() == "ui_icon" {
2693                                                    x.starts_with("ui/campaign ui/effect_bundles/")
2694                                                } else if table.table_name() == "effects_tables" && (field.name() == "icon" || field.name() == "icon_negative") {
2695                                                    x.starts_with("ui/campaign ui/effect_bundles/")
2696                                                } else if table.table_name() == "faction_groups_tables" && field.name() == "ui_icon" {
2697                                                    x.starts_with("ui/campaign ui/effect_bundles/")
2698                                                } else if table.table_name() == "incidents_tables" && field.name() == "ui_image" {
2699                                                    x.starts_with("ui/eventpics/")
2700                                                } else if table.table_name() == "message_event_strings_tables" && field.name() == "image" {
2701                                                    x.starts_with("ui/eventpics/")
2702                                                } else if table.table_name() == "missions_tables" && field.name() == "ui_icon" {
2703                                                    x.starts_with("ui/campaign ui/message_icons/")
2704
2705                                                // This is to fix false positives in sequencial missions in Pharaoh.
2706                                                } else if table.table_name() == "missions_tables" && field.name() == "ui_image" {
2707                                                    x.starts_with("ui/eventpics/") && x.ends_with(&(data.to_owned() + ".png"))
2708                                                } else if table.table_name() == "pooled_resources_tables" && field.name() == "optional_icon_path" {
2709                                                    x.starts_with("ui/skins/")
2710                                                } else if table.table_name() == "projectile_shot_type_enum_tables" && field.name() == "icon_name" {
2711                                                    x.starts_with("ui/battle ui/ability_icons/")
2712                                                } else if table.table_name() == "religions_tables" && field.name() == "ui_icon_path" {
2713                                                    x.starts_with("ui/campaign ui/religion_icons/")
2714                                                } else if table.table_name() == "special_ability_phases_tables" && field.name() == "ticker_icon" {
2715                                                    x.starts_with("ui/battle ui/ability_icons/")
2716                                                } else if table.table_name() == "technologies_tables" && field.name() == "icon_name" {
2717                                                    x.starts_with("ui/campaign ui/technologies/")
2718                                                } else if table.table_name() == "technologies_tables" && field.name() == "info_pic" {
2719                                                    x.starts_with("ui/eventpics/")
2720                                                } else if table.table_name() == "trait_categories_tables" && field.name() == "icon_path" {
2721                                                    x.starts_with("ui/campaign ui/effect_bundles/")
2722                                                } else if table.table_name() == "ui_unit_groupings_tables" && field.name() == "icon" {
2723                                                    x.starts_with("ui/common ui/unit_category_icons/")
2724                                                } else if table.table_name() == "victory_types_tables" && field.name() == "icon" {
2725                                                    x.starts_with("ui/campaign ui/victory_type_icons/")
2726
2727                                                // For some reason, some brilliant mind at CA decided to end a video name with ".png". So we need to filter this here.
2728                                                } else if table.table_name() == "videos_tables" && field.name() == "video_name" {
2729                                                    x.starts_with("movies/")
2730                                                } else {
2731                                                    true
2732                                                }
2733                                            })
2734
2735                                            // This filter is for reducing false positives in these cases:
2736                                            // - "default" or generic data.
2737                                            // - "x" value for invalid paths
2738                                            // - Entries that end in "_", which is used for some button path entries.
2739                                            .filter(|x| if !data.ends_with('_') {
2740                                                if !data.contains("/") {
2741                                                    if !data.contains('.') {
2742                                                        x.contains(&("/".to_owned() + &data + "."))
2743                                                    } else {
2744                                                        x.contains(&("/".to_owned() + &data))
2745                                                    }
2746                                                } else {
2747                                                    x.contains(&data)
2748                                                }
2749                                            } else {
2750                                                false
2751                                            })
2752
2753                                            // Replace only the last instance, to avoid weird folder-replacing bugs.
2754                                            .filter_map(|x| x.rfind(&data).map(|pos| (x, pos)))
2755                                            .map(|(x, pos)| x[..pos].to_owned() + &x[pos..].replacen(&data, "%", 1))
2756                                            .collect::<Vec<_>>();
2757
2758
2759                                        if !possible_paths.is_empty() {
2760                                            return Some(possible_paths)
2761                                        }
2762                                    }
2763                                }
2764
2765                                None
2766                            })
2767                            .flatten()
2768                            .collect::<HashSet<String>>();
2769
2770                        // Video files can be found by:
2771                        // - Checking if the data contains ".ca_vp8".
2772                        // - Checking if the data contains "video" in the name.
2773                        //
2774                        // Note that if the field contains incomplete/relative paths, this will guess and try to find unique files that match the path.
2775                        let mut possible_video = false;
2776                        if low_name.contains("video") {
2777                            possible_video = true;
2778                        }
2779
2780                        possible_relative_paths.extend(
2781                            table.data().par_iter().filter_map(|row| {
2782
2783                                // Only check fields that are not already marked, or are marked but without path (like override_icon in incidents).
2784                                if !field.is_filename(None) || (
2785                                        field.is_filename(None) && (
2786                                            field.filename_relative_path(None).is_none() ||
2787                                            field.filename_relative_path(None).unwrap().is_empty()
2788                                        )
2789                                    ) || (
2790
2791                                        // This table is missing the subpaths (which are valid) by default.
2792                                        table.table_name() == "videos_tables" && field.name() == "video_name"
2793                                    ) {
2794
2795                                    let mut data = row[column].data_to_string().to_lowercase().replace("\\", "/");
2796
2797                                    // Fix formatting for cells which start with / or \\.
2798                                    if data.starts_with("/") {
2799                                        if data.len() > 1 {
2800                                            data = data[1..].to_owned();
2801                                        } else {
2802                                            data = String::new();
2803                                        }
2804                                    }
2805
2806                                    if !data.is_empty() && (
2807                                            possible_video ||
2808                                            data.ends_with(".ca_vp8")
2809                                        ) {
2810
2811                                        let possible_paths = video_paths.iter()
2812                                            .filter(|x| {
2813                                                if table.table_name() == "videos_tables" && field.name() == "video_name" {
2814                                                    x.starts_with("movies/")
2815                                                } else {
2816                                                    true
2817                                                }
2818                                            })
2819                                            // This filter is for reducing false positives in these cases:
2820                                            // - "%_something", which is used for sequential videos.
2821                                            // - Faction-specific videos.
2822                                            .filter(|x| if !data.contains('.') {
2823                                                    x.contains(&("/".to_owned() + &data + "."))
2824                                                } else {
2825                                                    x.contains(&("/".to_owned() + &data))
2826                                                })
2827
2828                                            // Replace only the last instance, to avoid weird folder-replacing bugs.
2829                                            .filter_map(|x| x.rfind(&data).map(|pos| (x, pos)))
2830                                            .map(|(x, pos)| x[..pos].to_owned() + &x[pos..].replacen(&data, "%", 1))
2831                                            .collect::<Vec<_>>();
2832
2833
2834                                        if !possible_paths.is_empty() {
2835                                            return Some(possible_paths)
2836                                        }
2837                                    }
2838                                }
2839
2840                                None
2841                            })
2842                            .flatten()
2843                            .collect::<HashSet<String>>()
2844                        );
2845
2846                        // Debug message.
2847                        if !possible_relative_paths.is_empty() && (possible_relative_paths.len() > 1 || (possible_relative_paths.len() == 1 && possible_relative_paths.iter().collect::<Vec<_>>()[0] != "%")) {
2848                            info!("Checking table {}, field {} ...", table.table_name(), field.name());
2849                            dbg!(&possible_relative_paths);
2850                        }
2851
2852                        // This one has an incorrect relative path value that needs to be patched out.
2853                        //
2854                        // This is due to we assigning a name to this column which matches a different column in the AK.
2855                        if (table.table_name() == "models_building_tables" && field.name() == "logic_file") ||
2856                            (table.table_name() == "models_sieges_tables" && (field.name() == "model_file" || field.name() == "logic_file" || field.name() == "collision_file")) ||
2857                            (table.table_name() == "models_deployables_tables" && (field.name() == "model_file" || field.name() == "logic_file" || field.name() == "collision_file")) {
2858                            possible_relative_paths.clear();
2859                            possible_relative_paths.insert("%".to_owned());
2860                        }
2861
2862                        // These columns have incomplete paths or are incorrectly marked as files. Do not treat them as file paths.
2863                        if (table.table_name() == "ui_mercenary_recruitment_infos_tables" && field.name() == "hire_button_icon_path") ||
2864                            (table.table_name() == "battles_tables" && (field.name() == "specification" || field.name() == "battle_environment_audio")) ||
2865                            (table.table_name() == "factions_tables" && field.name() == "key") ||
2866                            (table.table_name() == "frontend_faction_leaders_tables" && field.name() == "key") {
2867                            let mut patch = HashMap::new();
2868                            patch.insert("is_filename".to_owned(), "false".to_owned());
2869
2870                            match new_patches.get_mut(table.table_name()) {
2871                                Some(patches) => match patches.get_mut(field.name()) {
2872                                    Some(patches) => patches.extend(patch),
2873                                    None => { patches.insert(field.name().to_owned(), patch); }
2874                                },
2875                                None => {
2876                                    let mut table_patch = HashMap::new();
2877                                    table_patch.insert(field.name().to_owned(), patch);
2878                                    new_patches.insert(table.table_name().to_string(), table_patch);
2879                                }
2880                            }
2881                        }
2882
2883                        // Only make patches for fields we manage to pinpoint to a file.
2884                        if !possible_relative_paths.is_empty() {
2885                            let mut possible_relative_paths = possible_relative_paths.iter().collect::<Vec<_>>();
2886                            possible_relative_paths.sort();
2887
2888                            let mut patch = HashMap::new();
2889                            if !field.is_filename(None) {
2890                                patch.insert("is_filename".to_owned(), "true".to_owned());
2891                            }
2892
2893                            // Only add paths if we're not dealing with single paths with full replacement, or we're force-replacing a path (advisors table).
2894                            if possible_relative_paths.len() > 1 || (
2895                                (
2896                                    possible_relative_paths.len() == 1 &&
2897                                    possible_relative_paths[0].contains('%') &&
2898                                    possible_relative_paths[0] != "%"
2899                                ) || (
2900                                    possible_relative_paths[0] == "%" &&
2901                                    field.filename_relative_path(None).is_some() &&
2902                                    !field.filename_relative_path(None).unwrap().is_empty()
2903                                )
2904                            ) {
2905                                patch.insert("filename_relative_path".to_owned(), possible_relative_paths.into_iter().join(";"));
2906                            }
2907
2908                            // Do not bother with empty patches.
2909                            if !patch.is_empty() {
2910                                match new_patches.get_mut(table.table_name()) {
2911                                    Some(patches) => match patches.get_mut(field.name()) {
2912                                        Some(patches) => patches.extend(patch),
2913                                        None => { patches.insert(field.name().to_owned(), patch); }
2914                                    },
2915                                    None => {
2916                                        let mut table_patch = HashMap::new();
2917                                        table_patch.insert(field.name().to_owned(), patch);
2918                                        new_patches.insert(table.table_name().to_string(), table_patch);
2919                                    }
2920                                }
2921                            }
2922                        }
2923                        /*
2924                        if (low_name == "key" || low_name == "id") && table.data().par_iter().all(|x| x[column].data_to_string().parse::<i32>().is_ok()) {
2925                            let mut patch = HashMap::new();
2926                            patch.insert("is_numeric".to_owned(), "true".to_owned());
2927
2928                            match new_patches.get_mut(table.table_name()) {
2929                                Some(patches) => match patches.get_mut(field.name()) {
2930                                    Some(patches) => patches.extend(patch),
2931                                    None => { patches.insert(field.name().to_owned(), patch); }
2932                                },
2933                                None => {
2934                                    let mut table_patch = HashMap::new();
2935                                    table_patch.insert(field.name().to_owned(), patch);
2936                                    new_patches.insert(table.table_name().to_string(), table_patch);
2937                                }
2938                            }
2939                        }*/
2940                    }
2941                    FieldType::I64 |
2942                    FieldType::OptionalI64 => {
2943                        /*let low_name = field.name().to_lowercase();
2944                        if (low_name == "key" || low_name == "id") && table.data().par_iter().all(|x| x[column].data_to_string().parse::<i32>().is_ok()) {
2945                            let mut patch = HashMap::new();
2946                            patch.insert("is_numeric".to_owned(), "true".to_owned());
2947
2948                            match new_patches.get_mut(table.table_name()) {
2949                                Some(patches) => match patches.get_mut(field.name()) {
2950                                    Some(patches) => patches.extend(patch),
2951                                    None => { patches.insert(field.name().to_owned(), patch); }
2952                                },
2953                                None => {
2954                                    let mut table_patch = HashMap::new();
2955                                    table_patch.insert(field.name().to_owned(), patch);
2956                                    new_patches.insert(table.table_name().to_string(), table_patch);
2957                                }
2958                            }
2959                        }*/
2960                    }
2961                    _ => continue
2962                }
2963            }
2964        }
2965
2966        Schema::add_patches_to_patch_set(current_patches, &new_patches);
2967
2968        Ok(())
2969    }
2970
2971    /// Function to add tiles and tile maps to the provided pack.
2972    ///
2973    /// Only for Warhammer 3.
2974    #[allow(clippy::too_many_arguments)]
2975    pub fn add_tile_maps_and_tiles(&mut self, packs: &mut BTreeMap<String, Pack>, pack_key: Option<&str>, game: &GameInfo, schema: &Schema, options: OptimizerOptions, tile_maps: Vec<PathBuf>, tiles: Vec<(PathBuf, String)>) -> Result<(Vec<ContainerPath>, Vec<ContainerPath>)> {
2976        let mut added_paths = vec![];
2977
2978        // Use the provided key, or fall back to the first pack in the map.
2979        let pack = match pack_key {
2980            Some(key) => packs.get_mut(key).ok_or_else(|| RLibError::NoPacksProvided)?,
2981            None => packs.values_mut().next().ok_or_else(|| RLibError::NoPacksProvided)?,
2982        };
2983
2984        // Tile Maps are from assembly_kit/working_data/terrain/battles/.
2985        for tile_map in &tile_maps {
2986            added_paths.append(&mut pack.insert_folder(tile_map, "terrain/battles", &None, &None, true)?);
2987        }
2988
2989        // Tiles are from assembly_kit/working_data/terrain/tiles/battle/, and can be in a subfolder if they're part of a tileset.
2990        for (tile, subpath) in &tiles {
2991
2992            let (internal_path, needs_tile_database) = if subpath.is_empty() {
2993                ("terrain/tiles/battle".to_owned(), false)
2994            } else {
2995                (format!("terrain/tiles/battle/{}", subpath.replace('\\', "/")), true)
2996            };
2997            added_paths.append(&mut pack.insert_folder(tile, &internal_path, &None, &None, true)?);
2998
2999            // If it's part of a tile set, we need to add the relevant tile database file for the tileset or the map will load as blank ingame.
3000            if needs_tile_database {
3001
3002                // We only need the database for out map, not the full database folder.
3003                let subpath_len = subpath.replace('\\', "/").split('/').count();
3004                let mut tile_database = tile.to_path_buf();
3005
3006                (0..=subpath_len).for_each(|_| {
3007                    tile_database.pop();
3008                });
3009
3010                let file_name = format!("{}_{}.bin", subpath.replace('/', "_"), tile.file_name().unwrap().to_string_lossy());
3011                tile_database.push(format!("_tile_database/TILES/{file_name}"));
3012                let tile_database_path = format!("terrain/tiles/battle/_tile_database/TILES/{file_name}");
3013
3014                added_paths.push(pack.insert_file(&tile_database, &tile_database_path, &None)?.unwrap());
3015            }
3016        }
3017
3018        let (paths_to_delete, paths_to_add) = pack.optimize(Some(added_paths.clone()), self, schema, game, &options)?;
3019
3020        let paths_to_delete = paths_to_delete.iter()
3021            .map(|path| ContainerPath::File(path.to_string()))
3022            .collect::<Vec<_>>();
3023
3024        added_paths.extend(paths_to_add.into_iter()
3025            .map(|path| ContainerPath::File(path.to_string()))
3026            .collect::<Vec<_>>());
3027
3028        Ok((added_paths, paths_to_delete))
3029    }
3030
3031    /// Function to trigger an startpos build.
3032    ///
3033    /// After this ends, remember to call the post one!
3034    #[allow(clippy::too_many_arguments)]
3035    pub fn build_starpos_pre(&self, packs: &mut BTreeMap<String, Pack>, pack_key: Option<&str>, game: &GameInfo, game_path: &Path, campaign_id: &str, process_hlp_spd_data: bool, sub_start_pos: &str) -> Result<()> {
3036
3037        // Pre-fetch data we need before taking mutable borrows.
3038        let map_names = if process_hlp_spd_data {
3039            self.db_values_from_table_name_and_column_name_for_value(Some(packs), "campaigns_tables", "campaign_name", "map_name", true, true)
3040        } else {
3041            HashMap::new()
3042        };
3043
3044        // Use the provided key, or fall back to the first pack in the map.
3045        let pack_file = match pack_key {
3046            Some(key) => packs.get_mut(key).ok_or_else(|| RLibError::NoPacksProvided)?,
3047            None => packs.values_mut().next().ok_or_else(|| RLibError::NoPacksProvided)?,
3048        };
3049        let pack_name = pack_file.disk_file_name();
3050        if pack_name.is_empty() {
3051            return Err(RLibError::BuildStartposError("The Pack needs to be saved to disk in order to build a startpos. Save it and try again.".to_owned()));
3052        }
3053
3054        if campaign_id.is_empty() {
3055            return Err(RLibError::BuildStartposError("campaign_id not provided.".to_owned()));
3056        }
3057
3058        let process_hlp_spd_data_string = if process_hlp_spd_data {
3059            String::from("process_campaign_ai_map_data;")
3060        } else {
3061            String::new()
3062        };
3063
3064        // Note: 3K uses 2 passes per campaign, each one with a different startpos, but both share the hlp/spd process, so that only needs to be generated once.
3065        // Also, extra folders is to fix a bug in Rome 2, Attila and possibly Thrones where objectives are not processed if certain folders are missing.
3066        let extra_folders = "add_working_directory assembly_kit\\working_data;";
3067        let mut user_script_contents = if game.key() == KEY_ATTILA || game.key() == KEY_THRONES_OF_BRITANNIA { extra_folders.to_owned() } else { String::new() };
3068
3069        user_script_contents.push_str(&format!("
3070    mod {pack_name};
3071    process_campaign_startpos {campaign_id} {sub_start_pos};
3072    {process_hlp_spd_data_string}
3073    quit_after_campaign_processing;"
3074        ));
3075
3076        // Games may fail to launch if we don't have this path created, which is done the first time we start the game.
3077        let game_data_path = game.data_path(game_path)?;
3078        if !game_path.is_dir() {
3079            return Err(RLibError::BuildStartposError("Game path incorrect. Fix it in the settings and try again.".to_owned()));
3080        }
3081
3082        if !PathBuf::from(pack_file.disk_file_path()).starts_with(&game_data_path) {
3083            return Err(RLibError::BuildStartposError("The Pack needs to be in /data. Install it there and try again.".to_owned()));
3084        }
3085
3086        // We need to extract the victory_objectives.txt file to "data/campaign_id/". Warhammer 3 doesn't use this file.
3087        if GAMES_NEEDING_VICTORY_OBJECTIVES.contains(&game.key()) {
3088            let mut game_campaign_path = game_data_path.to_path_buf();
3089            game_campaign_path.push(campaign_id);
3090            DirBuilder::new().recursive(true).create(&game_campaign_path)?;
3091
3092            game_campaign_path.push(VICTORY_OBJECTIVES_EXTRACTED_FILE_NAME);
3093            pack_file.extract(ContainerPath::File(VICTORY_OBJECTIVES_FILE_NAME.to_owned()), &game_campaign_path, false, &None, true, false, &None)?;
3094        }
3095
3096        let config_path = game.config_path(game_path).ok_or(RLibError::BuildStartposError("Error getting the game's config path.".to_owned()))?;
3097        let scripts_path = config_path.join("scripts");
3098        DirBuilder::new().recursive(true).create(&scripts_path)?;
3099
3100        // Rome 2 is bugged when generating startpos using the userscript. We need to pass it to the game through args in a cmd terminal instead of by file.
3101        //
3102        // So don't do any userscript change for Rome 2.
3103        if game.key() != KEY_ROME_2 {
3104
3105            // Make a backup before editing the script, so we can restore it later.
3106            let uspa = scripts_path.join(USER_SCRIPT_FILE_NAME);
3107            let uspb = scripts_path.join(USER_SCRIPT_FILE_NAME.to_owned() + ".bak");
3108
3109            if uspa.is_file() {
3110                std::fs::copy(&uspa, uspb)?;
3111            }
3112
3113            let mut file = BufWriter::new(File::create(uspa)?);
3114
3115            // Napoleon, Empire and Shogun 2 require the user.script.txt or mod list file (for Shogun's latest update) to be in UTF-16 LE. What the actual fuck.
3116            if *game.raw_db_version() < 2 {
3117                file.write_string_u16(&user_script_contents)?;
3118            } else {
3119                file.write_all(user_script_contents.as_bytes())?;
3120            }
3121
3122            file.flush()?;
3123        }
3124
3125        // Due to how the starpos is generated, if we generate it on vanilla campaigns it'll overwrite existing files if it's generated on /data.
3126        // So we must backup the vanilla files, then restore them after.
3127        //
3128        // Only needed from Warhammer 1 onwards, and in Rome 2 due to how is generated there.
3129        if game.key() != KEY_THRONES_OF_BRITANNIA &&
3130            game.key() != KEY_ATTILA &&
3131            game.key() != KEY_SHOGUN_2 {
3132
3133            let sub_start_pos_suffix = if sub_start_pos.is_empty() {
3134                String::new()
3135            } else {
3136                format!("_{sub_start_pos}")
3137            };
3138
3139            let starpos_path = game_data_path.join(format!("campaigns/{campaign_id}/startpos{sub_start_pos_suffix}.esf"));
3140            if starpos_path.is_file() {
3141                let starpos_path_bak = game_data_path.join(format!("campaigns/{campaign_id}/startpos{sub_start_pos_suffix}.esf.bak"));
3142                std::fs::copy(&starpos_path, starpos_path_bak)?;
3143                std::fs::remove_file(starpos_path)?;
3144            }
3145        }
3146
3147        // Same for the other two files, if we're generating them. We need to get the campaign name from the campaigns table first, then get the files generated.
3148        if process_hlp_spd_data {
3149            if let Some(map_name) = map_names.get(campaign_id) {
3150                match game.key() {
3151
3152                    // For generating the hlp data, from Warhammer 1 onwards the game outputs it to /data, which may not exists and may conflict with existing files.
3153                    //
3154                    // Create the folder just in case, and back any file found.
3155                    KEY_PHARAOH_DYNASTIES |
3156                    KEY_PHARAOH |
3157                    KEY_WARHAMMER_3 |
3158                    KEY_TROY |
3159                    KEY_THREE_KINGDOMS |
3160                    KEY_WARHAMMER_2 |
3161                    KEY_WARHAMMER => {
3162                        let hlp_folder_path = game_data_path.join(format!("campaign_maps/{map_name}"));
3163                        if !hlp_folder_path.is_dir() {
3164                            DirBuilder::new().recursive(true).create(&hlp_folder_path)?;
3165                        }
3166
3167                        let hlp_path = game_data_path.join(format!("campaign_maps/{map_name}/hlp_data.esf"));
3168                        if hlp_path.is_file() {
3169                            let hlp_path_bak = game_data_path.join(format!("campaign_maps/{map_name}/hlp_data.esf.bak"));
3170                            std::fs::copy(&hlp_path, hlp_path_bak)?;
3171                            std::fs::remove_file(hlp_path)?;
3172                        }
3173                    },
3174
3175                    // For Thrones and Attila is more tricky, because the game itself is bugged when processing this file.
3176                    //
3177                    // It's generated in the game's config folder, but we need to manually keep recreating the folder for a while because the game deletes it
3178                    // in the middle of the process and causes an error when trying to write the file. The way we do it is with a background thread
3179                    // that keeps recreating it every 100ms if it ever detects it's gone.
3180                    //
3181                    // Keep in mind this thread is kept alive for as long as the program runs unless it's intentionally stopped. So remember to stop it.
3182                    KEY_THRONES_OF_BRITANNIA |
3183                    KEY_ATTILA => {
3184                        let folder_path = config_path.join(format!("maps/campaign_maps/{map_name}"));
3185
3186                        let (sender, receiver) = channel::<bool>();
3187                        let join = thread::spawn(move || {
3188                            loop {
3189                                match receiver.try_recv() {
3190                                    Ok(stop) => if stop {
3191                                        break;
3192                                    }
3193                                    Err(_) => {
3194                                        if !folder_path.is_dir() {
3195                                            let _ = DirBuilder::new().recursive(true).create(&folder_path);
3196                                        }
3197
3198                                        thread::sleep(Duration::from_millis(100));
3199                                    }
3200                                }
3201                            }
3202                        });
3203
3204                         *START_POS_WORKAROUND_THREAD.write().unwrap() = Some(vec![(sender, join)]);
3205                    },
3206
3207                    // For rome 2 is a weird one. It generates the file in config (like Attila), but them moves it to /data (like Warhammer).
3208                    //
3209                    // So we need to first, ensure the config folder is created (it may not exists, but it's not deleted mid-process like in Attile)
3210                    // and it's empty, and then backup the hlp file, if exists, from /data.
3211                    KEY_ROME_2 => {
3212                        let hlp_folder = game_data_path.join(format!("campaign_maps/{map_name}/"));
3213                        if hlp_folder.is_dir() {
3214                            let _ = DirBuilder::new().recursive(true).create(&hlp_folder);
3215                        }
3216
3217                        let hlp_path = hlp_folder.join("hlp_data.esf");
3218                        if hlp_path.is_file() {
3219                            let hlp_path_bak = game_data_path.join(format!("campaign_maps/{map_name}/hlp_data.esf.bak"));
3220                            std::fs::copy(&hlp_path, hlp_path_bak)?;
3221                            std::fs::remove_file(hlp_path)?;
3222                        }
3223
3224                    }
3225                    KEY_SHOGUN_2 => return Err(RLibError::BuildStartposError("Unsupported... yet. If you want to test support for this game, let me know.".to_owned())),
3226                    KEY_NAPOLEON => return Err(RLibError::BuildStartposError("Unsupported... yet. If you want to test support for this game, let me know.".to_owned())),
3227                    KEY_EMPIRE => return Err(RLibError::BuildStartposError("Unsupported... yet. If you want to test support for this game, let me know.".to_owned())),
3228                    _ => return Err(RLibError::BuildStartposError("How the fuck did you trigger this?".to_owned())),
3229                }
3230
3231                // This file is only from Warhammer 1 onwards. No need to check if the path exists because the hlp process should have created the folder.
3232                if game.key() != KEY_THRONES_OF_BRITANNIA &&
3233                    game.key() != KEY_ATTILA &&
3234                    game.key() != KEY_ROME_2 &&
3235                    game.key() != KEY_SHOGUN_2 &&
3236                    game.key() != KEY_NAPOLEON &&
3237                    game.key() != KEY_EMPIRE {
3238
3239                    let spd_path = game_data_path.join(format!("campaign_maps/{map_name}/spd_data.esf"));
3240                    if spd_path.is_file() {
3241                        let spd_path_bak = game_data_path.join(format!("campaign_maps/{map_name}/spd_data.esf.bak"));
3242                        std::fs::copy(&spd_path, spd_path_bak)?;
3243                        std::fs::remove_file(spd_path)?;
3244                    }
3245                }
3246            }
3247        }
3248
3249        // Then launch the game. 3K needs to be launched manually and in a blocking manner to make sure it does each pass it has to do correctly.
3250        if game.key() == KEY_THREE_KINGDOMS {
3251            let exe_path = game.executable_path(game_path).ok_or_else(|| RLibError::BuildStartposError("Game exe path not found.".to_owned()))?;
3252            let exe_name = exe_path.file_name().ok_or_else(|| RLibError::BuildStartposError("Game exe name not found.".to_owned()))?.to_string_lossy();
3253
3254            // NOTE: This uses a non-existant load order file on purpouse, so no mod in the load order interferes with generating the startpos.
3255            let mut command = Command::new("cmd");
3256            command.arg("/C");
3257            command.arg("start");
3258            command.arg("/wait");
3259            command.arg("/d");
3260            command.arg(game_path.to_string_lossy().replace('\\', "/"));
3261            command.arg(exe_name.to_string());
3262            command.arg("temp_file.txt;");
3263
3264            let _ = command.output()?;
3265
3266            // In multipass, we need to clean the user script after each pass.
3267            let uspa = scripts_path.join(USER_SCRIPT_FILE_NAME);
3268            let uspb = scripts_path.join(USER_SCRIPT_FILE_NAME.to_owned() + ".bak");
3269            if uspb.is_file() {
3270                std::fs::copy(uspb, uspa)?;
3271            }
3272
3273            // If there's no backup, means there was no file to begin with, so we delete the custom file.
3274            else if uspa.is_file() {
3275                std::fs::remove_file(uspa)?;
3276            }
3277
3278        // Rome 2 needs to be launched manually through the cmd with params. The rest can be launched through their regular launcher.
3279        } else if game.key() == KEY_ROME_2 {
3280            let exe_path = game.executable_path(game_path).ok_or_else(|| RLibError::BuildStartposError("Game exe path not found.".to_owned()))?;
3281            let exe_name = exe_path.file_name().ok_or_else(|| RLibError::BuildStartposError("Game exe name not found.".to_owned()))?.to_string_lossy();
3282
3283            // NOTE: This uses a non-existant load order file on purpouse, so no mod in the load order interferes with generating the startpos.
3284            let mut command = Command::new("cmd");
3285            command.arg("/C");
3286            command.arg("start");
3287            command.arg("/d");
3288            command.arg(game_path.to_string_lossy().replace('\\', "/"));
3289            command.arg(exe_name.to_string());
3290            command.arg("temp_file.txt;");
3291
3292            // We need to turn the user script contents into a oneliner or the command will ignore it.
3293            #[cfg(target_os = "windows")] {
3294                use std::os::windows::process::CommandExt;
3295
3296                // Rome 2 needs the working_data folder in order to throw the startpos file there.
3297                command.raw_arg(extra_folders);
3298                command.raw_arg(user_script_contents.replace("\n", " "));
3299            }
3300
3301            command.spawn()?;
3302        } else {
3303            match game.game_launch_command(game_path) {
3304                Ok(command) => { let _ = open::that(command); },
3305                _ => return Err(RLibError::BuildStartposError("The currently selected game cannot be launched from Steam.".to_owned())),
3306            }
3307        }
3308
3309        Ok(())
3310    }
3311
3312    /// Function to trigger the second part of the startpos build process, which involves importing the startpos file
3313    /// into the provided pack.
3314    ///
3315    /// Call this when the game closes after the pre function launched it.
3316    ///
3317    /// NOTE: The assembly kit path is only needed for Rome 2.
3318    #[allow(clippy::too_many_arguments)]
3319    pub fn build_starpos_post(&self, packs: &mut BTreeMap<String, Pack>, pack_key: Option<&str>, game: &GameInfo, game_path: &Path, asskit_path: Option<PathBuf>,campaign_id: &str, process_hlp_spd_data: bool, cleanup_mode: bool, sub_start_pos: &[String]) -> Result<Vec<ContainerPath>> {
3320
3321        // Pre-fetch data we need before taking mutable borrows.
3322        let map_names = if process_hlp_spd_data {
3323            self.db_values_from_table_name_and_column_name_for_value(Some(packs), "campaigns_tables", "campaign_name", "map_name", true, true)
3324        } else {
3325            HashMap::new()
3326        };
3327
3328        // Use the provided key, or fall back to the first pack in the map.
3329        let pack_file = match pack_key {
3330            Some(key) => packs.get_mut(key).ok_or_else(|| RLibError::NoPacksProvided)?,
3331            None => packs.values_mut().next().ok_or_else(|| RLibError::NoPacksProvided)?,
3332        };
3333
3334        let mut startpos_failed = false;
3335        let mut sub_startpos_failed = vec![];
3336        let mut hlp_failed = false;
3337        let mut spd_failed = false;
3338
3339        // Before anything else, close the workaround thread.
3340        if let Some(data) = START_POS_WORKAROUND_THREAD.write().unwrap().as_mut() {
3341            let (sender, handle) = data.remove(0);
3342            let _ = sender.send(true);
3343            let _ = handle.join();
3344        }
3345
3346        *START_POS_WORKAROUND_THREAD.write().unwrap() = None;
3347
3348        if !game_path.is_dir() {
3349            return Err(RLibError::BuildStartposError("Game path incorrect. Fix it in the settings and try again.".to_owned()));
3350        }
3351
3352        let game_data_path = game.data_path(game_path)?;
3353
3354        // Warhammer 3 doesn't use this folder.
3355        if GAMES_NEEDING_VICTORY_OBJECTIVES.contains(&game.key()) {
3356
3357            // We need to delete the "data/campaign_id/" folder.
3358            let mut game_campaign_path = game_data_path.to_path_buf();
3359            game_campaign_path.push(campaign_id);
3360            if game_campaign_path.is_dir() {
3361                let _ = std::fs::remove_dir_all(game_campaign_path);
3362            }
3363        }
3364
3365        let config_path = game.config_path(game_path).ok_or(RLibError::BuildStartposError("Error getting the game's config path.".to_owned()))?;
3366        let scripts_path = config_path.join("scripts");
3367        if !scripts_path.is_dir() {
3368            DirBuilder::new().recursive(true).create(&scripts_path)?;
3369        }
3370
3371        // Restore the userscript backup, if any.
3372        let uspa = scripts_path.join(USER_SCRIPT_FILE_NAME);
3373        let uspb = scripts_path.join(USER_SCRIPT_FILE_NAME.to_owned() + ".bak");
3374        if uspb.is_file() {
3375            std::fs::copy(uspb, uspa)?;
3376        }
3377
3378        // If there's no backup, means there was no file to begin with, so we delete the custom file.
3379        else if uspa.is_file() {
3380            std::fs::remove_file(uspa)?;
3381        }
3382
3383        let mut added_paths = vec![];
3384
3385        // Add the starpos file. As some games have multiple startpos per campaign (3K) we return a vector with all the paths we have to generate.
3386        let starpos_paths = match game.key() {
3387            KEY_PHARAOH_DYNASTIES |
3388            KEY_PHARAOH |
3389            KEY_WARHAMMER_3 |
3390            KEY_TROY |
3391            KEY_THREE_KINGDOMS |
3392            KEY_WARHAMMER_2 |
3393            KEY_WARHAMMER => {
3394                if sub_start_pos.is_empty() {
3395                    vec![game_data_path.join(format!("campaigns/{campaign_id}/startpos.esf"))]
3396                } else {
3397                    let mut paths = vec![];
3398                    for sub in sub_start_pos {
3399                        paths.push(game_data_path.join(format!("campaigns/{campaign_id}/startpos_{sub}.esf")));
3400
3401                    }
3402                    paths
3403                }
3404            }
3405            KEY_THRONES_OF_BRITANNIA |
3406            KEY_ATTILA => vec![config_path.join(format!("maps/campaigns/{campaign_id}/startpos.esf"))],
3407
3408            // Rome 2 outputs the startpos in the assembly kit folder.
3409            KEY_ROME_2 => {
3410                match asskit_path {
3411                    Some(asskit_path) => {
3412                        if !asskit_path.is_dir() {
3413                            return Err(RLibError::BuildStartposError("Assembly Kit path is not a valid folder.".to_owned()));
3414                        }
3415
3416                        vec![asskit_path.join(format!("working_data/campaigns/{campaign_id}/startpos.esf"))]
3417                    },
3418                    None => return Err(RLibError::BuildStartposError("Assembly Kit path not provided.".to_owned())),
3419                }
3420            },
3421
3422            // Shogun 2 outputs to data, but unlike modern names, vanilla startpos are packed, so there's no rist of overwrite.
3423            // We still need to clean it up later though. Napoleon and Empire override vanilla files, so those are backed.
3424            KEY_SHOGUN_2 |
3425            KEY_NAPOLEON |
3426            KEY_EMPIRE => vec![game_data_path.join(format!("campaigns/{campaign_id}/startpos.esf"))],
3427            _ => return Err(RLibError::BuildStartposError("How the fuck did you trigger this?".to_owned())),
3428        };
3429
3430        let starpos_paths_pack = if sub_start_pos.is_empty() {
3431            vec![format!("campaigns/{}/startpos.esf", campaign_id)]
3432        } else {
3433            let mut paths = vec![];
3434            for sub in sub_start_pos {
3435                paths.push(format!("campaigns/{campaign_id}/startpos_{sub}.esf"));
3436            }
3437            paths
3438        };
3439
3440        if !cleanup_mode {
3441            for (index, starpos_path) in starpos_paths.iter().enumerate() {
3442                if !starpos_path.is_file() {
3443                    if sub_start_pos.is_empty() {
3444                        startpos_failed = true;
3445                    } else {
3446                        sub_startpos_failed.push(sub_start_pos[index].to_owned());
3447                    }
3448                } else {
3449
3450                    let mut rfile = RFile::new_from_file_path(starpos_path)?;
3451                    rfile.set_path_in_container_raw(&starpos_paths_pack[index]);
3452                    rfile.load()?;
3453                    rfile.guess_file_type()?;
3454
3455                    added_paths.push(pack_file.insert(rfile).map(|x| x.unwrap())?);
3456                }
3457            }
3458        }
3459
3460        // Restore the old starpos if there was one, and delete the new one if it has already been added.
3461        //
3462        // Only needed from Warhammer 1 onwards, and for Rome 2, Napoleon and Empire. Other games generate the startpos outside that folder.
3463        //
3464        // 3K uses 2 startpos, so we need to restore them both.
3465        if game.key() != KEY_THRONES_OF_BRITANNIA &&
3466            game.key() != KEY_ATTILA &&
3467            game.key() != KEY_SHOGUN_2 {
3468
3469            for starpos_path in &starpos_paths {
3470                let file_name = starpos_path.file_name().unwrap().to_string_lossy().to_string();
3471                let file_name_bak = file_name + ".bak";
3472
3473                let mut starpos_path_bak = starpos_path.to_path_buf();
3474                starpos_path_bak.set_file_name(file_name_bak);
3475
3476                if starpos_path_bak.is_file() {
3477                    std::fs::copy(&starpos_path_bak, starpos_path)?;
3478                    std::fs::remove_file(starpos_path_bak)?;
3479                }
3480            }
3481        }
3482
3483        // In Shogun 2, we need to cleanup the generated file as to not interfere with the packed one.
3484        if game.key() == KEY_SHOGUN_2 {
3485            for starpos_path in &starpos_paths {
3486                if starpos_path.is_file() {
3487                    std::fs::remove_file(starpos_path)?;
3488                }
3489            }
3490        }
3491
3492        // Same with the other two files.
3493        if process_hlp_spd_data {
3494            if let Some(map_name) = map_names.get(campaign_id) {
3495
3496                // Same as with startpos. It's different depending on the game.
3497                let hlp_path = match game.key() {
3498                    KEY_PHARAOH_DYNASTIES |
3499                    KEY_PHARAOH |
3500                    KEY_WARHAMMER_3 |
3501                    KEY_TROY |
3502                    KEY_THREE_KINGDOMS |
3503                    KEY_WARHAMMER_2 |
3504                    KEY_WARHAMMER => game_data_path.join(format!("campaign_maps/{map_name}/hlp_data.esf")),
3505                    KEY_THRONES_OF_BRITANNIA |
3506                    KEY_ATTILA => config_path.join(format!("maps/campaign_maps/{map_name}/hlp_data.esf")),
3507                    KEY_ROME_2 => game_data_path.join(format!("campaign_maps/{map_name}/hlp_data.esf")),
3508                    _ => return Err(RLibError::BuildStartposError("How the fuck did you trigger this?".to_owned())),
3509                };
3510
3511                let hlp_path_pack = format!("campaign_maps/{map_name}/hlp_data.esf");
3512
3513                if !cleanup_mode {
3514
3515                    if !hlp_path.is_file() {
3516                        hlp_failed = true;
3517                    } else {
3518
3519                        let mut rfile_hlp = RFile::new_from_file_path(&hlp_path)?;
3520                        rfile_hlp.set_path_in_container_raw(&hlp_path_pack);
3521                        rfile_hlp.load()?;
3522                        rfile_hlp.guess_file_type()?;
3523
3524                        added_paths.push(pack_file.insert(rfile_hlp).map(|x| x.unwrap())?);
3525                    }
3526                }
3527
3528                // Only needed from Warhammer 1 onwards, and in Rome 2. Other games generate the hlp file outside that folder.
3529                if game.key() != KEY_THRONES_OF_BRITANNIA &&
3530                    game.key() != KEY_ATTILA {
3531
3532                    let hlp_path_bak = game_data_path.join(format!("campaign_maps/{map_name}/hlp_data.esf.bak"));
3533
3534                    if hlp_path_bak.is_file() {
3535                        std::fs::copy(&hlp_path_bak, hlp_path)?;
3536                        std::fs::remove_file(hlp_path_bak)?;
3537                    }
3538                }
3539
3540                // The spd file was introduced in Warhammer 1. Don't expect it on older games.
3541                if game.key() != KEY_THRONES_OF_BRITANNIA &&
3542                    game.key() != KEY_ATTILA &&
3543                    game.key() != KEY_ROME_2 {
3544
3545                    let spd_path = game_data_path.join(format!("campaign_maps/{map_name}/spd_data.esf"));
3546                    let spd_path_pack = format!("campaign_maps/{map_name}/spd_data.esf");
3547
3548                    if !cleanup_mode {
3549
3550                        if !spd_path.is_file() {
3551                            spd_failed = true;
3552                        } else {
3553
3554                            let mut rfile_spd = RFile::new_from_file_path(&spd_path)?;
3555                            rfile_spd.set_path_in_container_raw(&spd_path_pack);
3556                            rfile_spd.load()?;
3557                            rfile_spd.guess_file_type()?;
3558
3559                            added_paths.push(pack_file.insert(rfile_spd).map(|x| x.unwrap())?);
3560                        }
3561                    }
3562
3563                    let spd_path_bak = game_data_path.join(format!("campaign_maps/{map_name}/spd_data.esf.bak"));
3564                    if spd_path_bak.is_file() {
3565                        std::fs::copy(&spd_path_bak, spd_path)?;
3566                        std::fs::remove_file(spd_path_bak)?;
3567                    }
3568                }
3569            }
3570        }
3571
3572        let mut error = String::new();
3573        if startpos_failed || (!sub_start_pos.is_empty() && !sub_startpos_failed.is_empty()) || hlp_failed || spd_failed {
3574            error.push_str("<p>One or more files failed to generate:</p><ul>")
3575        }
3576        if startpos_failed {
3577            error.push_str("<li>Startpos file failed to generate.</li>");
3578        }
3579
3580        for sub_failed in &sub_startpos_failed {
3581            error.push_str(&format!("<li>\"{sub_failed}\" Startpos file failed to generate.</li>"));
3582        }
3583
3584        if hlp_failed {
3585            error.push_str("<li>HLP file failed to generate.</li>");
3586        }
3587
3588        if spd_failed {
3589            error.push_str("<li>SPD file failed to generate.</li>");
3590        }
3591
3592        if startpos_failed || hlp_failed || spd_failed {
3593            error.push_str("</ul><p>No files were added and the related files were restored to their pre-build state. Check your tables are correct before trying to generate them again.</p>")
3594        }
3595
3596        if error.is_empty() {
3597            Ok(added_paths)
3598        } else {
3599            Err(RLibError::BuildStartposError(error))
3600        }
3601    }
3602
3603    /// This function imports a specific table from the data it has in the AK.
3604    ///
3605    /// Tables generated with this are VALID.
3606    pub fn import_from_ak(&self, table_name: &str, schema: &Schema) -> Result<DB> {
3607        let definition = if let Some(definitions) = schema.definitions_by_table_name_cloned(table_name) {
3608            if !definitions.is_empty() {
3609                definitions[0].clone()
3610            } else {
3611                return Err(RLibError::DecodingDBNoDefinitionsFound)
3612            }
3613        } else {
3614            return Err(RLibError::DecodingDBNoDefinitionsFound)
3615        };
3616
3617        // Create the new table according to the schema, and import its data from the AK.
3618        if let Some(ak_file) = self.asskit_only_db_tables().get(table_name) {
3619            let mut real_table = ak_file.clone();
3620            real_table.set_definition(&definition);
3621            Ok(real_table)
3622        } else {
3623            Err(RLibError::AssemblyKitTableNotFound(table_name.to_owned()))
3624        }
3625    }
3626
3627    //-----------------------------------//
3628    // Dangerous functions.
3629    //-----------------------------------//
3630
3631    /// This function manually inserts a loc file from this into the dependencies as a vanilla loc.
3632    ///
3633    /// THIS IS DANGEROUS. DO NOT USE IT UNLESS YOU KNOW WHAT YOU'RE DOING.
3634    pub fn insert_loc_as_vanilla_loc(&mut self, rfile: RFile) {
3635        let path = rfile.path_in_container_raw().to_owned();
3636        self.vanilla_files.insert(path.to_owned(), rfile);
3637        self.vanilla_locs.insert(path);
3638    }
3639
3640    /// This function manipulates a definition to recursively add reference lookups if found.
3641    ///
3642    /// THIS IS DANGEROUS IF WE FIND A CYCLIC DEPENDENCY.
3643    pub fn add_recursive_lookups_to_definition(&self, schema: &Schema, definition: &mut Definition, table_name: &str) {
3644        let schema_patches = definition.patches().clone();
3645
3646        for field in definition.fields_mut().iter_mut() {
3647
3648            // First check lookups on the local table.
3649            if let Some(lookup_data_old) = field.lookup(Some(&schema_patches)) {
3650                let mut lookup_data = vec![];
3651
3652                // Check first for local lookups.
3653                if !lookup_data_old.is_empty() {
3654
3655                    let table_name = if let Some(table_name) = table_name.strip_suffix("_tables") {
3656                        table_name.to_owned()
3657                    } else {
3658                        table_name.to_owned()
3659                    };
3660
3661                    for lookup_data_old in &lookup_data_old {
3662                        let lookup_string = format!("{}#{}#{}", table_name, field.name(), lookup_data_old);
3663                        self.add_recursive_lookups(schema, &schema_patches, lookup_data_old, &mut lookup_data, &lookup_string, &table_name);
3664                    }
3665
3666                }
3667
3668                // If our field is a reference, do recursive checks to find out all the lookup data of a specific field.
3669                if let Some((ref_table_name, ref_column)) = field.is_reference(Some(&schema_patches)) {
3670                    for lookup_data_old in &lookup_data_old {
3671                        let lookup_string = format!("{ref_table_name}#{ref_column}#{lookup_data_old}");
3672                        self.add_recursive_lookups(schema, &schema_patches, lookup_data_old, &mut lookup_data, &lookup_string, &ref_table_name);
3673                    }
3674                }
3675
3676                if !lookup_data.is_empty() {
3677                    field.set_lookup(Some(lookup_data));
3678                } else {
3679                    field.set_lookup(None);
3680                }
3681            }
3682        }
3683    }
3684
3685    fn add_recursive_lookups(&self,
3686        schema: &Schema,
3687        schema_patches: &HashMap<String, HashMap<String, String>>,
3688        lookup: &str,
3689        lookup_data: &mut Vec<String>,
3690        lookup_string: &str,
3691        table_name: &str
3692    ) {
3693        let mut finish_lookup = false;
3694        let table_name = table_name.to_string() + "_tables";
3695        if let Ok(ref_tables) = self.db_data(&table_name, true, true) {
3696            let candidates = ref_tables.iter()
3697                .filter_map(|rfile| rfile.decoded().ok())
3698                .filter_map(|decoded| if let RFileDecoded::DB(db) = decoded {
3699                    Some(db.definition().clone())
3700                } else {
3701                    None
3702                })
3703                .collect::<Vec<_>>();
3704
3705            if let Some(definition) = schema.definition_newer(&table_name, &candidates) {
3706
3707                // If this fails, it may be a loc.
3708                if let Some(pos) = definition.column_position_by_name(lookup) {
3709                    if let Some(field) = definition.fields_processed().get(pos) {
3710
3711                        // If our field is a reference, we need to go one level deeper to find the lookup.
3712                        if let Some((ref_table_name, ref_column)) = field.is_reference(Some(schema_patches)) {
3713                            if let Some(lookups) = field.lookup(Some(schema_patches)) {
3714                                for lookup in &lookups {
3715                                    let lookup_string = format!("{lookup_string}:{ref_table_name}#{ref_column}#{lookup}");
3716
3717                                    self.add_recursive_lookups(schema, schema_patches, lookup, lookup_data, &lookup_string, &ref_table_name);
3718                                }
3719                            } else {
3720                                finish_lookup = true;
3721                            }
3722                        } else {
3723                            finish_lookup = true;
3724                        }
3725                    } else {
3726                        finish_lookup = true;
3727                    }
3728                }
3729
3730                else if definition.localised_fields().iter().any(|x| x.name() == lookup) {
3731                    finish_lookup = true;
3732                }
3733            } else {
3734                finish_lookup = true;
3735            }
3736        }
3737
3738        if finish_lookup && !lookup_data.iter().any(|x| x == lookup_string) {
3739            lookup_data.push(lookup_string.to_owned());
3740        }
3741    }
3742}