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