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