Skip to main content

rpfm_extensions/dependencies/
mod.rs

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