Skip to main content

rpfm_lib/files/pack/
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//! PackFile (`.pack`) container format for Total War games.
12//!
13//! This module handles reading and writing PackFiles, the primary container format used by
14//! Total War games to store game assets. PackFiles bundle multiple files into a single archive
15//! with optional compression and encryption support.
16//!
17//! # Overview
18//!
19//! PackFiles have evolved through multiple versions since Empire: Total War, with each version
20//! adding features like timestamps, compression, and encryption. This module supports all
21//! known PackFile versions (PFH0 through PFH6).
22//!
23//! # Pack Types
24//!
25//! Packs have different types that determine load order and behavior:
26//! - **Boot**: Core game files loaded first
27//! - **Release**: Official game content
28//! - **Patch**: Official patches
29//! - **Mod**: User-created modifications
30//! - **Movie**: Video content packs
31//!
32//! # Features
33//!
34//! - **Lazy Loading**: Files can be loaded on-demand to reduce memory usage
35//! - **Compression**: PFH5+ supports LZ4, Zlib, and LZMA compression (game-dependent)
36//! - **Encryption**: Packs support index and data encryption
37//! - **Timestamps**: Track when files were last modified
38//! - **Dependencies**: Packs can declare dependencies on other packs
39//! - **Metadata**: Store notes and settings within the pack itself
40//!
41//! # Example
42//!
43//! ```no_run
44//! use rpfm_lib::files::pack::Pack;
45//! use rpfm_lib::files::{Container, Decodeable, DecodeableExtraData};
46//! use std::fs::File;
47//! use std::io::BufReader;
48//!
49//! // Open and read a pack file
50//! let file = File::open("my_mod.pack").unwrap();
51//! let mut reader = BufReader::new(file);
52//! let mut extra_data = DecodeableExtraData::default();
53//! extra_data.set_lazy_load(true);
54//!
55//! let pack = Pack::decode(&mut reader, &Some(extra_data)).unwrap();
56//! println!("Pack contains {} files", pack.files().len());
57//! ```
58
59use bitflags::bitflags;
60use getset::*;
61use rayon::prelude::*;
62use serde_derive::{Serialize, Deserialize};
63use serde_json::{from_slice, to_string_pretty};
64use itertools::Itertools;
65
66use std::cmp::Ordering;
67use std::collections::{BTreeMap, HashMap, HashSet};
68use std::fs::File;
69use std::hash::{DefaultHasher, Hash, Hasher};
70use std::io::{BufReader, BufWriter, Cursor, SeekFrom, Write};
71use std::path::{Path, PathBuf};
72use std::str::FromStr;
73
74use crate::binary::{ReadBytes, WriteBytes};
75use crate::compression::{Compressible, CompressionFormat};
76use crate::error::{RLibError, Result};
77use crate::files::{Container, ContainerPath, Decodeable, DecodeableExtraData, Encodeable, EncodeableExtraData, FileType, Loc, RFile, RFileDecoded, table::DecodedData};
78use crate::games::{GameInfo, pfh_file_type::PFHFileType, pfh_version::PFHVersion};
79use crate::notes::Note;
80use crate::utils::{current_time, last_modified_time_from_file};
81
82#[cfg(test)]
83mod pack_test;
84mod pack_versions;
85
86/// File extension used by PackFiles.
87pub const EXTENSION: &str = ".pack";
88
89/// Special preamble found in some Steam Workshop downloaded packs.
90const MFH_PREAMBLE: &str = "MFH";
91
92/// Path where Terry (map editor) exports map files within a pack.
93const TERRY_MAP_PATH: &str = "terrain/tiles/battle/_assembly_kit";
94
95/// Default filename for Battle Map Data files exported from Terry.
96const DEFAULT_BMD_DATA: &str = "bmd_data.bin";
97
98/// Path prefix for missing loc entries that override existing translations.
99const MISSING_LOCS_PATH_START_EXISTING: &str = "text/aaa_missing_locs_";
100/// Path prefix for missing loc entries that are new translations.
101const MISSING_LOCS_PATH_START_NEW: &str = "text/zzz_missing_locs_";
102
103// Binary markers used by the Siege AI patching function.
104const FORT_PERIMETER_HINT: &[u8; 18] = b"AIH_FORT_PERIMETER";
105const DEFENSIVE_HILL_HINT: &[u8; 18] = b"AIH_DEFENSIVE_HILL";
106const SIEGE_AREA_NODE_HINT: &[u8; 19] = b"AIH_SIEGE_AREA_NODE";
107
108/// Reserved filename for legacy dependency manager data.
109pub const RESERVED_NAME_DEPENDENCIES_MANAGER: &str = "dependencies_manager.rpfm_reserved";
110/// Reserved filename for dependency manager data (current version).
111pub const RESERVED_NAME_DEPENDENCIES_MANAGER_V2: &str = "dependencies_manager_v2.rpfm_reserved";
112/// Reserved filename for extra packfile references.
113pub const RESERVED_NAME_EXTRA_PACKFILE: &str = "extra_packfile.rpfm_reserved";
114/// Reserved filename for pack settings data.
115pub const RESERVED_NAME_SETTINGS: &str = "settings.rpfm_reserved";
116/// Reserved filename for extracted pack settings (JSON format).
117pub const RESERVED_NAME_SETTINGS_EXTRACTED: &str = "settings.rpfm_reserved.json";
118/// Reserved filename for pack notes data.
119pub const RESERVED_NAME_NOTES: &str = "notes.rpfm_reserved";
120/// Reserved filename for extracted pack notes (Markdown format).
121pub const RESERVED_NAME_NOTES_EXTRACTED: &str = "notes.rpfm_reserved.md";
122
123/// List of reserved filenames used by RPFM for internal purposes.
124///
125/// These files are automatically handled during pack read/write operations
126/// and should not be manually manipulated.
127pub const RESERVED_RFILE_NAMES: [&str; 3] = [RESERVED_NAME_EXTRA_PACKFILE, RESERVED_NAME_SETTINGS, RESERVED_NAME_NOTES];
128
129/// Authoring tool identifier for Creative Assembly tools.
130const AUTHORING_TOOL_CA: &str = "CA_TOOL";
131/// Authoring tool identifier for RPFM.
132const AUTHORING_TOOL_RPFM: &str = "RPFM";
133/// Maximum size in bytes for the authoring tool string.
134const AUTHORING_TOOL_SIZE: u32 = 8;
135
136/// Settings key for the compression format used by this pack.
137pub const SETTING_KEY_CF: &str = "compression_format";
138
139bitflags! {
140
141    /// This represents the bitmasks a Pack can have applied to his type.
142    ///
143    /// Keep in mind that this lib supports decoding Packs with any of these flags enabled,
144    /// but it only supports enconding for the `HAS_INDEX_WITH_TIMESTAMPS` flag.
145    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
146    pub struct PFHFlags: u32 {
147
148        /// Used to specify that the header of the Pack is extended by 20 bytes. Used in Arena.
149        const HAS_EXTENDED_HEADER       = 0b0000_0001_0000_0000;
150
151        /// Used to specify that the File Index is encrypted. Used in Arena.
152        const HAS_ENCRYPTED_INDEX       = 0b0000_0000_1000_0000;
153
154        /// Used to specify that the File Index contains a timestamp of every Pack.
155        const HAS_INDEX_WITH_TIMESTAMPS = 0b0000_0000_0100_0000;
156
157        /// Used to specify that the File Data is encrypted. Seen in `music.pack` Packs and in Arena.
158        const HAS_ENCRYPTED_DATA        = 0b0000_0000_0001_0000;
159    }
160}
161
162//---------------------------------------------------------------------------//
163//                              Enum & Structs
164//---------------------------------------------------------------------------//
165
166/// Packs are a container-type file, used for "packing" all game assets into single files, to speed up disk reads.
167///
168/// Their format has passed through multiple iterations since empire, getting changes on almost all iterations,
169/// like timestamps, encryption, compression,...
170///
171/// # Pack Structure
172///
173/// | Bytes  | Type                        | Data                                                                       |
174/// | ------ | --------------------------- | -------------------------------------------------------------------------- |
175/// | 20-384 | [PackHeader]                | Header of the Pack. Lenght depends on Pack version and flags enabled.      |
176/// | *      | [Pack Index](#pack-index)   | Index containing the list of Packs this Pack depends on.                   |
177/// | *      | [File Index](#file-index)   | Index containing the list of Files this Pack depends on                    |
178/// | *      | [File Data](#file-data)     | Data of the files contained in this Pack.                                  |
179/// | 256    | Appendix                    | Unknown data at the end of the Pack. Only seen in Arena's encrypted Packs. |
180///
181/// ## Pack Index
182///
183/// The Pack Index contains a list of Packs that will be force-loaded before this mod.
184///
185/// | Bytes | Type                     | Data            |
186/// | ----- | ------------------------ | --------------- |
187/// | *     | Null-terminated StringU8 | Pack file name. |
188///
189/// ## File Index
190///
191/// The File Index contains the metadata of the Files this Pack contains, in the same order their data is, further in the Pack.
192///
193/// | Bytes | Type                     | Data                                                                                                            |
194/// | ----- | ------------------------ | --------------------------------------------------------------------------------------------------------------- |
195/// | 4     | u32                      | Size of the file's data, in bytes.                                                                              |
196/// | 8     | u64                      | Timestamp of the file, if the header has the HAS_INDEX_WITH_TIMESTAMPS flag enabled. Only in PFH2 and PFH3.     |
197/// | 4     | u32                      | Truncated timestamp of the file, if the header has the HAS_INDEX_WITH_TIMESTAMPS flag enabled. Only since PFH4. |
198/// | 1     | bool                     | If the file is compressed. Only since PFH5.                                                                     |
199/// | *     | Null-terminated StringU8 | File's path within the Pack.                                                                                    |
200///
201/// ## File Data
202///
203/// The raw data of the files contained by this Pack, in the same order as their indexes. Not much to explain here.
204///
205#[derive(Debug, Clone, PartialEq, Getters, MutGetters, Setters, Default, Serialize, Deserialize)]
206#[getset(get = "pub", get_mut = "pub", set = "pub")]
207pub struct Pack {
208
209    /// The path of the Pack on disk, if exists. If not, then this should be empty.
210    disk_file_path: String,
211
212    /// The offset on the disk file the data of this Pack starts. Usually 0.
213    disk_file_offset: u64,
214
215    /// Timestamp from the moment this Pack was open. To check if the file was edited on disk while we had it open.
216    local_timestamp: u64,
217
218    /// If the files in this Pack should be compressed. Compression format is set in the Pack Settings.
219    #[getset(skip)]
220    compress: bool,
221
222    /// Header data of this Pack.
223    header: PackHeader,
224
225    /// List of Packs this Pack depends on. If the boolean is true, the packs are also required to be loaded before himself when starting the game.
226    ///
227    /// In other places, we may refer to this as the `Dependency List`.
228    dependencies: Vec<(bool, String)>,
229
230    /// List of files this Pack contains.
231    files: HashMap<String, RFile>,
232
233    /// List of file paths lowercased, with their casing counterparts. To quickly find files.
234    paths: HashMap<String, Vec<String>>,
235
236    /// Notes added to the Pack. Exclusive of this lib.
237    notes: PackNotes,
238
239    /// Settings stored in the Pack itself, to be able to share them between installations.
240    settings: PackSettings,
241}
242
243/// Header of a Pack, containing all the header-related info of said Pack.
244///
245/// # Header Structure.
246///
247/// | Bytes | Type                               | Data                                                                                                            |
248/// | ----- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------- |
249/// | 8     | 00-Padded StringU8                 | Fake Preamble/Id of this Pack. Usually "MFH" and a bunch of 00. Only in old Steam Workshop files.               |
250/// | 4     | StringU8                           | Preamble/Id of this Pack. Contains the "version" of this Pack.                                                  |
251/// | 4     | u32                                | Pack Type + Bitwised flags for tweaking certain Pack configurations.                                            |
252/// | 4     | u32                                | Amount of items in the Pack Index of this Pack.                                                                 |
253/// | 4     | u32                                | Lenght in bytes of the Pack Index.                                                                              |
254/// | 4     | u32                                | Amount of items in the File Index of this Pack.                                                                 |
255/// | 4     | u32                                | Lenght in bytes of the File Index.                                                                              |
256/// | 8     | u64                                | Timestamp when this Pack was last edited. Only in PFH2 and PFH3.                                                |
257/// | 20    | `Vec<u8>`                          | Extended header data. Only if HAS_EXTENDED_HEADER flag is set.                                                  |
258/// | 280   | [Subheader](#subheader-structure)  | Subheader data. Only since PFH6.                                                                                |
259///
260/// # Subheader Structure.
261///
262/// Subheader containing extra metadata for the Pack. Only in PFH6.
263///
264/// | Bytes | Type               | Data                                                                                         |
265/// | ----- | ------------------ | -------------------------------------------------------------------------------------------- |
266/// | 4     | u32                | Subheader marker. Marks the begining of the subheader. If missing, there's no subheader.     |
267/// | 4     | u32                | Subheader version.                                                                           |
268/// | 4     | u32                | Game version this Pack was done for.                                                         |
269/// | 4     | u32                | Build number of the game version this Pack was done for.                                     |
270/// | 8     | 00-Padded StringU8 | Tool that made this Pack.                                                                    |
271/// | 256   | `Vec<u8>`          | Unused bytes.                                                                                |
272#[derive(Debug, Clone, PartialEq, Eq, Getters, Setters, Serialize, Deserialize)]
273#[getset(get = "pub", set = "pub")]
274pub struct PackHeader {
275
276    /// The version of the Pack.
277    pfh_version: PFHVersion,
278
279    /// The type of the Pack.
280    pfh_file_type: PFHFileType,
281
282    /// The bitmasks applied to the Pack.
283    bitmask: PFHFlags,
284
285    /// The timestamp of the last time the Pack was saved.
286    internal_timestamp: u64,
287
288    /// Game version this Pack is intended for. This usually triggers the "outdated mod" warning in the launcher if it doesn't match the current exe version.
289    game_version: u32,
290
291    /// Build number of the game.
292    build_number: u32,
293
294    /// Tool that created the Pack. Max 8 characters, 00-padded.
295    authoring_tool: String,
296
297    /// Extra subheader data, in case it's used in the future.
298    extra_subheader_data: Vec<u8>,
299}
300
301/// Pack-specific settings stored within the pack itself.
302///
303/// These settings are serialized to a reserved file within the pack, allowing them
304/// to be shared when the pack is distributed. Common settings include compression
305/// format, diagnostic ignore lists, and import configurations.
306///
307/// # Built-in Settings
308///
309/// - `compression_format`: The compression format to use (None, Lz4, Zlib)
310/// - `diagnostics_files_to_ignore`: Files/diagnostics to skip during validation
311/// - `import_files_to_ignore`: Files to skip during folder imports
312/// - `disable_autosaves`: Disable automatic saving
313/// - `do_not_generate_existing_locs`: Skip generating loc entries that already exist
314#[derive(Clone, Debug, PartialEq, Eq, Getters, MutGetters, Setters, Serialize, Deserialize)]
315#[getset(get = "pub", get_mut = "pub", set = "pub")]
316pub struct PackSettings {
317
318    /// Multi-line text settings (e.g., file ignore lists).
319    settings_text: BTreeMap<String, String>,
320
321    /// Single-line string settings (e.g., compression format).
322    settings_string: BTreeMap<String, String>,
323
324    /// Boolean flag settings (e.g., disable_autosaves).
325    settings_bool: BTreeMap<String, bool>,
326
327    /// Integer settings (e.g., thresholds, limits).
328    settings_number: BTreeMap<String, i32>,
329}
330
331/// Pack notes for documentation and collaboration.
332///
333/// Notes are stored within the pack and include both pack-level notes (general
334/// documentation) and file-specific notes (annotations on individual files or tables).
335/// Notes are serialized as JSON and can be shared when the pack is distributed.
336#[derive(Clone, Debug, PartialEq, Eq, Default, Getters, MutGetters, Setters, Serialize, Deserialize)]
337#[getset(get = "pub", get_mut = "pub", set = "pub")]
338pub struct PackNotes {
339
340    /// General notes for the entire pack (Markdown format).
341    pack_notes: String,
342
343    /// File-specific notes, keyed by lowercase file path.
344    ///
345    /// For DB tables, notes are shared across all tables of the same type
346    /// (path is truncated to `db/table_name/`).
347    file_notes: HashMap<String, Vec<Note>>,
348}
349
350/// Parsed entry from the `diagnostics_files_to_ignore` pack setting.
351///
352/// Tuple shape: `(path, ignored_diagnostic_codes, ignored_field_names)`.
353pub type DiagnosticIgnoreEntry = (String, Vec<String>, Vec<String>);
354
355//---------------------------------------------------------------------------//
356//                           Structs Implementations
357//---------------------------------------------------------------------------//
358
359impl Container for Pack {
360
361    /// This method allows us to extract the metadata associated to the provided container as `.json` or `.md` files.
362    ///
363    /// [Pack] implementation extracts the [PackSettings] of the provided Pack and its associated notes.
364    fn extract_metadata(&mut self, destination_path: &Path) -> Result<Vec<PathBuf>> {
365        let mut paths = vec![];
366        let mut data = vec![];
367        data.write_all(to_string_pretty(&self.notes)?.as_bytes())?;
368        data.extend_from_slice(b"\n"); // Add newline to the end of the file
369
370        let path = destination_path.join(RESERVED_NAME_NOTES_EXTRACTED);
371        paths.push(path.to_owned());
372        let mut file = BufWriter::new(File::create(path)?);
373        file.write_all(&data)?;
374        file.flush()?;
375
376        let mut data = vec![];
377        data.write_all(to_string_pretty(&self.settings)?.as_bytes())?;
378        data.extend_from_slice(b"\n"); // Add newline to the end of the file
379
380        let path = destination_path.join(RESERVED_NAME_SETTINGS_EXTRACTED);
381        paths.push(path.to_owned());
382        let mut file = BufWriter::new(File::create(path)?);
383        file.write_all(&data)?;
384        file.flush()?;
385
386        Ok(paths)
387    }
388
389    fn insert(&mut self, mut file: RFile) -> Result<Option<ContainerPath>> {
390
391        // Filter out special files, so we only leave the normal files in.
392        let path_container = file.path_in_container();
393        let path = file.path_in_container_raw();
394        if path == RESERVED_NAME_NOTES_EXTRACTED {
395            self.notes = PackNotes::load(&file.encode(&None, false, false, true)?.unwrap())?;
396            Ok(None)
397        } else if path == RESERVED_NAME_SETTINGS_EXTRACTED {
398            self.settings = PackSettings::load(&file.encode(&None, false, false, true)?.unwrap())?;
399            Ok(None)
400        } else if path == RESERVED_NAME_DEPENDENCIES_MANAGER_V2 {
401            self.dependencies = from_slice(&file.encode(&None, false, false, true)?.unwrap())?;
402            Ok(None)
403        }
404
405        // If it's not filtered out, add it to the Pack.
406        else {
407            self.paths_cache_insert_path(path);
408            self.files.insert(path.to_owned(), file);
409
410            Ok(Some(path_container))
411        }
412    }
413
414    fn disk_file_path(&self) -> &str {
415       &self.disk_file_path
416    }
417
418    fn files(&self) -> &HashMap<String, RFile> {
419        &self.files
420    }
421
422    fn files_mut(&mut self) -> &mut HashMap<String, RFile> {
423        &mut self.files
424    }
425
426    fn disk_file_offset(&self) -> u64 {
427       self.disk_file_offset
428    }
429
430    fn paths_cache(&self) -> &HashMap<String, Vec<String>> {
431        &self.paths
432    }
433
434    fn paths_cache_mut(&mut self) -> &mut HashMap<String, Vec<String>> {
435        &mut self.paths
436    }
437
438    fn internal_timestamp(&self) -> u64 {
439       self.header.internal_timestamp
440    }
441
442    fn local_timestamp(&self) -> u64 {
443       self.local_timestamp
444    }
445
446    /// This function allows you to *move* any RFile of folder of RFiles from one folder to another.
447    ///
448    /// It returns a list with all the new [ContainerPath].
449    fn move_path(&mut self, source_path: &ContainerPath, destination_path: &ContainerPath) -> Result<Vec<(ContainerPath, ContainerPath)>> {
450        match source_path {
451            ContainerPath::File(source_path) => match destination_path {
452                ContainerPath::File(destination_path) => {
453                    if RESERVED_RFILE_NAMES.contains(&&**destination_path) {
454                        return Err(RLibError::ReservedFiles);
455                    }
456
457                    if destination_path.is_empty() {
458                        return Err(RLibError::EmptyDestiny);
459                    }
460
461                    self.paths_cache_remove_path(source_path);
462                    let mut moved = self.files_mut()
463                        .remove(source_path)
464                        .ok_or_else(|| RLibError::FileNotFound(source_path.to_string()))?;
465
466                    moved.set_path_in_container_raw(destination_path);
467
468                    self.insert(moved).map(|x| match x {
469                        Some(x) => vec![(ContainerPath::File(source_path.to_string()), x); 1],
470                        None => Vec::with_capacity(0),
471                    })
472                },
473                ContainerPath::Folder(_) => unreachable!("move_path_pack_1"),
474            },
475            ContainerPath::Folder(source_path) => match destination_path {
476                ContainerPath::File(_) => unreachable!("move_path_pack_2"),
477                ContainerPath::Folder(destination_path) => {
478                    if destination_path.is_empty() {
479                        return Err(RLibError::EmptyDestiny);
480                    }
481
482                    // Fix to avoid false positives.
483                    let mut source_path_end = source_path.to_owned();
484                    if !source_path_end.ends_with('/') {
485                        source_path_end.push('/');
486                    }
487
488                    let moved_paths = self.files()
489                        .par_iter()
490                        .filter_map(|(path, _)| if path.starts_with(&source_path_end) { Some(path.to_owned()) } else { None })
491                        .collect::<Vec<_>>();
492
493                    let moved = moved_paths.iter()
494                        .filter_map(|x| {
495                            self.paths_cache_remove_path(x);
496                            self.files_mut().remove(x)
497                        })
498                        .collect::<Vec<_>>();
499
500                    let mut new_paths = Vec::with_capacity(moved.len());
501                    for mut moved in moved {
502                        let old_path = moved.path_in_container();
503                        let new_path = moved.path_in_container_raw().replacen(source_path, destination_path, 1);
504                        moved.set_path_in_container_raw(&new_path);
505
506                        if let Some(new_path) = self.insert(moved)? {
507                            new_paths.push((old_path, new_path));
508                        }
509                    }
510
511                    Ok(new_paths)
512                },
513            },
514        }
515    }
516}
517
518impl Decodeable for Pack {
519
520    fn decode<R: ReadBytes>(data: &mut R, extra_data: &Option<DecodeableExtraData>) -> Result<Self> {
521        Self::read(data, extra_data)
522    }
523}
524
525impl Encodeable for Pack {
526
527    fn encode<W: WriteBytes>(&mut self, buffer: &mut W, extra_data: &Option<EncodeableExtraData>) -> Result<()> {
528        self.write(buffer, extra_data)
529    }
530}
531
532/// Implementation of `Pack`.
533impl Pack {
534
535    /// This function creates a new empty Pack with a specific PFHVersion.
536    pub fn new_with_version(pfh_version: PFHVersion) -> Self {
537        let mut pack = Self::default();
538        pack.header.pfh_version = pfh_version;
539        pack
540    }
541
542    /// This function creates a new empty Pack with a name and a specific PFHVersion.
543    pub fn new_with_name_and_version(name: &str, pfh_version: PFHVersion) -> Self {
544        let mut pack = Self::default();
545        pack.header.pfh_version = pfh_version;
546        pack.disk_file_path = name.to_owned();
547        pack
548    }
549
550    /// This function tries to read a `Pack` from raw data.
551    ///
552    /// If `lazy_load` is false, the data of all the files inside the `Pack` will be preload to memory.
553    fn read<R: ReadBytes>(data: &mut R, extra_data: &Option<DecodeableExtraData>) -> Result<Self> {
554        let extra_data = extra_data.as_ref().ok_or(RLibError::DecodingMissingExtraData)?;
555
556        // GameInfo is required now, to properly support per-game particularities.
557        let game_info = match extra_data.game_info {
558            Some(game_info) => game_info,
559            None => return Err(RLibError::GameInfoMissingFromDecodingFunction),
560        };
561
562        // If we're reading from a file on disk, we require a valid path.
563        // If we're reading from a file on memory, we don't need a valid path.
564        let disk_file_path = match extra_data.disk_file_path {
565            Some(path) => {
566                let file_path = PathBuf::from_str(path).map_err(|_|RLibError::DecodingMissingExtraDataField("disk_file_path".to_owned()))?;
567                if file_path.is_file() {
568                    path.to_owned()
569                } else {
570                    return Err(RLibError::DecodingMissingExtraData)
571                }
572            }
573            None => String::new()
574        };
575
576        let disk_file_offset = extra_data.disk_file_offset;
577        let disk_file_size = if extra_data.data_size > 0 { extra_data.data_size } else { data.len()? };
578        let timestamp = extra_data.timestamp;
579        let is_encrypted = extra_data.is_encrypted;
580        let skip_path_cache_generation = extra_data.skip_path_cache_generation;
581
582        // If we don't have a path, or the file is encrypted, we can't lazy-load.
583        let lazy_load = !disk_file_path.is_empty() && !is_encrypted && extra_data.lazy_load;
584
585        // First, we do some quick checks to ensure it's a valid Pack.
586        // A valid Pack, bare and empty, needs at least 24 bytes, regardless of game or type.
587        let data_len = disk_file_size;
588        if data_len < 24 {
589            return Err(RLibError::PackHeaderNotComplete);
590        }
591
592        // Check if it has the weird steam-only header, and skip it if found.
593        let start = if data.read_string_u8(3)? == MFH_PREAMBLE { 8 } else { 0 };
594        data.seek(SeekFrom::Current(-3))?;
595        data.seek(SeekFrom::Current(start))?;
596
597        // Create the default Pack and start populating it.
598        let mut pack = Self {
599            disk_file_path,
600            disk_file_offset,
601            local_timestamp: timestamp,
602            ..Default::default()
603        };
604
605        pack.header.pfh_version = PFHVersion::version(&data.read_string_u8(4)?)?;
606
607        let pack_type = data.read_u32()?;
608        pack.header.pfh_file_type = PFHFileType::try_from(pack_type & 15)?;
609        pack.header.bitmask = PFHFlags::from_bits_truncate(pack_type & !15);
610
611        // Each Pack version has its own read function, to avoid breaking support for older Packs
612        // when implementing support for a new Pack version.
613        let expected_data_len = match pack.header.pfh_version {
614            PFHVersion::PFH6 => pack.read_pfh6(data, extra_data)?,
615            PFHVersion::PFH5 => pack.read_pfh5(data, extra_data)?,
616            PFHVersion::PFH4 => pack.read_pfh4(data, extra_data)?,
617            PFHVersion::PFH3 => pack.read_pfh3(data, extra_data)?,
618            PFHVersion::PFH2 => pack.read_pfh2(data, extra_data)?,
619            PFHVersion::PFH0 => pack.read_pfh0(data, extra_data)?,
620        };
621
622        // Remove the reserved files from the Pack and read them properly.
623        if let Some(mut notes) = pack.files.remove(RESERVED_NAME_NOTES) {
624            notes.load()?;
625            let data = notes.cached()?;
626
627            // Migration logic from 3.X to 4.X notes: iff we detect old notes, we don't fail.
628            // We instead generate a new 4.X note and fill the pack message with the old 3.X note.
629            match PackNotes::load(data) {
630                Ok(notes) => pack.notes = notes,
631                Err(_) => {
632                    let len = data.len();
633                    let mut data = Cursor::new(data);
634                    pack.notes = PackNotes::default();
635                    pack.notes.pack_notes = data.read_string_u8(len)?;
636                }
637            }
638        }
639
640        if let Some(mut settings) = pack.files.remove(RESERVED_NAME_SETTINGS) {
641            settings.load()?;
642            let data = settings.cached()?;
643            pack.settings.load_and_update(data)?;
644        }
645
646        if let Some(mut deps) = pack.files.remove(RESERVED_NAME_DEPENDENCIES_MANAGER_V2) {
647            deps.load()?;
648            let data = deps.cached()?;
649            pack.dependencies = from_slice(data)?;
650        }
651
652        // Generate the path list.
653        if !skip_path_cache_generation {
654            pack.paths_cache_generate();
655        }
656
657        // Once we're done reading files, we have to initialize the compression format.
658        // The reason we do this here is because the pack only contains if the files are compressed or not, not which format is used.
659        // To support multiple formats, we have to make sure we save the last-used format in the the pack settings.
660        // If no format has been saved but the files are compressed, we default to the more modern one supported by the game.
661        let preferred_cf = game_info.compression_formats_supported().first().cloned().unwrap_or_default();
662        let current_cf_str = pack.settings().setting_string(SETTING_KEY_CF).cloned().unwrap_or_default();
663        let current_cf = CompressionFormat::from(&*current_cf_str);
664
665        if pack.compress && current_cf == CompressionFormat::None {
666            pack.settings_mut().set_setting_string(SETTING_KEY_CF, preferred_cf.to_string().as_str());
667        }
668
669        // If at this point we have not reached the end of the Pack, there is something wrong with it.
670        // NOTE: Arena Packs have extra data at the end. If we detect one of those Packs, take that into account.
671        //if pack.header.pfh_version == PFHVersion::PFH5 && pack.header.bitmask.contains(PFHFlags::HAS_EXTENDED_HEADER) {
672        //    if expected_data_len + 256 != data_len { return Err(RLibError::DecodingMismatchSizeError(data_len as usize, expected_data_len as usize)) }
673        //}
674        if expected_data_len != data_len { return Err(RLibError::DecodingMismatchSizeError(data_len as usize, expected_data_len as usize)) }
675
676        // Guess the file's types. Do this here because this can be very slow and here we can do it in paralell.
677        pack.files.par_iter_mut().map(|(_, file)| file.guess_file_type()).collect::<Result<()>>()?;
678
679        // If we disabled lazy-loading, load every File to memory.
680        if !lazy_load {
681            pack.files.par_iter_mut().try_for_each(|(_, file)| file.load())?;
682        }
683
684        // Return our Pack.
685        Ok(pack)
686    }
687
688    /// This function writes a `Pack` into the provided buffer.
689    fn write<W: WriteBytes>(&mut self, buffer: &mut W, extra_data: &Option<EncodeableExtraData>) -> Result<()> {
690        let test_mode = if let Some(extra_data) = extra_data {
691            extra_data.test_mode
692        } else {
693            false
694        };
695
696        if !test_mode {
697
698            // Only do this in non-vanilla files.
699            if self.header.pfh_file_type == PFHFileType::Mod || self.header.pfh_file_type == PFHFileType::Movie {
700
701                // Save notes, if needed.
702                let mut data = vec![];
703                data.write_all(to_string_pretty(&self.notes)?.as_bytes())?;
704                let file = RFile::new_from_vec(&data, FileType::Text, 0, RESERVED_NAME_NOTES);
705                self.files.insert(RESERVED_NAME_NOTES.to_owned(), file);
706
707                // Saving Pack settings.
708                let mut data = vec![];
709                data.write_all(to_string_pretty(&self.settings)?.as_bytes())?;
710                let file = RFile::new_from_vec(&data, FileType::Text, 0, RESERVED_NAME_SETTINGS);
711                self.files.insert(RESERVED_NAME_SETTINGS.to_owned(), file);
712
713                // Saving Pack dependencies.
714                let mut data = vec![];
715                data.write_all(to_string_pretty(&self.dependencies)?.as_bytes())?;
716                let file = RFile::new_from_vec(&data, FileType::Text, 0, RESERVED_NAME_DEPENDENCIES_MANAGER_V2);
717                self.files.insert(RESERVED_NAME_DEPENDENCIES_MANAGER_V2.to_owned(), file);
718
719            }
720        }
721
722        match self.header.pfh_version {
723            PFHVersion::PFH6 => self.write_pfh6(buffer, extra_data)?,
724            PFHVersion::PFH5 => self.write_pfh5(buffer, extra_data)?,
725            PFHVersion::PFH4 => self.write_pfh4(buffer, extra_data)?,
726            PFHVersion::PFH3 => self.write_pfh3(buffer, extra_data)?,
727            PFHVersion::PFH2 => self.write_pfh2(buffer, extra_data)?,
728            PFHVersion::PFH0 => self.write_pfh0(buffer, extra_data)?,
729        }
730
731        // Remove again the reserved Files.
732        self.remove(&ContainerPath::File(RESERVED_NAME_NOTES.to_owned()));
733        self.remove(&ContainerPath::File(RESERVED_NAME_SETTINGS.to_owned()));
734        self.remove(&ContainerPath::File(RESERVED_NAME_DEPENDENCIES_MANAGER_V2.to_owned()));
735
736        // If nothing has failed, return success.
737        Ok(())
738    }
739
740    //-----------------------------------------------------------------------//
741    //                        Convenience functions
742    //-----------------------------------------------------------------------//
743
744    /// This function reads and returns all CA Packs for the provided game merged as one, for easy manipulation.
745    ///
746    /// This needs a [GameInfo] to get the Packs from, and a game path to search the Packs on.
747    pub fn read_and_merge_ca_packs(game: &GameInfo, game_path: &Path) -> Result<Self> {
748        let paths = game.ca_packs_paths(game_path)?;
749        let mut pack = Self::read_and_merge(&paths, game, true, true, false)?;
750
751        // Make sure it's not mod type.
752        pack.header_mut().set_pfh_file_type(PFHFileType::Release);
753        Ok(pack)
754    }
755
756    /// Convenience function to open multiple Packs as one, taking care of overwriting files when needed.
757    ///
758    /// If this function receives only one path, it works as a normal read_from_disk function. If it receives none, an error will be returned.
759    pub fn read_and_merge(pack_paths: &[PathBuf], game: &GameInfo, lazy_load: bool, ignore_mods: bool, keep_order: bool) -> Result<Self> {
760        if pack_paths.is_empty() {
761            return Err(RLibError::NoPacksProvided);
762        }
763
764        let mut extra_data = DecodeableExtraData {
765            lazy_load,
766            game_info: Some(game),
767            ..Default::default()
768        };
769
770        // If we only got one path, just decode the Pack on it.
771        if pack_paths.len() == 1 {
772            let mut data = BufReader::new(File::open(&pack_paths[0])
773                .map_err(|error| RLibError::IOErrorPath(Box::new(RLibError::IOError(error)), pack_paths[0].to_path_buf()))?);
774            let path_str = pack_paths[0].to_string_lossy().replace('\\', "/");
775
776            extra_data.set_disk_file_path(Some(&path_str));
777            extra_data.set_timestamp(last_modified_time_from_file(data.get_ref()).unwrap());
778
779            return Self::read(&mut data, &Some(extra_data))
780        }
781
782        // Skip path cache generation for each pack, as we're not going to use it. Instead we're going to generate one for the merged pack.
783        extra_data.set_skip_path_cache_generation(true);
784
785        // Generate a new empty Pack to act as merged one.
786        let mut pack_new = Pack::default();
787        let mut packs = pack_paths.par_iter()
788            .map(|path| {
789                let mut data = BufReader::new(File::open(path)
790                    .map_err(|error| RLibError::IOErrorPath(Box::new(RLibError::IOError(error)), pack_paths[0].to_path_buf()))?);
791                let path_str = path.to_string_lossy().replace('\\', "/");
792
793                let mut extra_data = extra_data.to_owned();
794                extra_data.set_disk_file_path(Some(&path_str));
795                extra_data.set_timestamp(last_modified_time_from_file(data.get_ref())?);
796
797                Self::read(&mut data, &Some(extra_data))
798            }).collect::<Result<Vec<Pack>>>()?;
799
800        // Group different type files, and sort them by name.
801        packs.sort_by(|pack_a, pack_b| if pack_a.pfh_file_type() != pack_b.pfh_file_type() {
802            pack_a.pfh_file_type().cmp(&pack_b.pfh_file_type())
803        } else if !keep_order {
804            pack_a.disk_file_path.cmp(&pack_b.disk_file_path)
805        } else {
806            Ordering::Equal
807        });
808
809        packs.iter()
810            .chunk_by(|pack| pack.header.pfh_file_type)
811            .into_iter()
812            .for_each(|(pfh_type, packs)| {
813                if pfh_type != PFHFileType::Mod || !ignore_mods {
814                    let mut packs = packs.collect::<Vec<_>>();
815                    packs.reverse();
816                    packs.iter()
817                        .for_each(|pack| {
818                        pack_new.files_mut().extend(pack.files().clone())
819                    });
820                }
821            });
822
823        // Fix the dependencies of the merged pack.
824        let pack_names = packs.iter().map(|pack| pack.disk_file_name()).collect::<Vec<_>>();
825        let mut dependencies = packs.iter()
826            .flat_map(|pack| pack.dependencies()
827                .iter()
828                .filter(|(_, dependency)| !pack_names.contains(dependency))
829                .cloned()
830                .collect::<Vec<_>>())
831            .collect::<Vec<_>>();
832
833        // Dedup the dependencies while preserving the order.
834        let mut set = HashSet::new();
835        dependencies.retain(|x| set.insert(x.clone()));
836        pack_new.set_dependencies(dependencies);
837
838        // Fix the pack version and header.
839        pack_new.set_pfh_file_type(packs[0].pfh_file_type());
840        pack_new.set_pfh_version(game.pfh_version_by_file_type(pack_new.pfh_file_type()));
841
842        // Generate the path list.
843        pack_new.paths_cache_generate();
844
845        Ok(pack_new)
846    }
847
848    /// Convenience function to merge open Packs as one, taking care of overwriting files when needed.
849    ///
850    /// Packs are merged in the order they are provided. If you need to use a custom order,
851    /// sort them before merging, or use the `read_and_merge` function instead.
852    ///
853    /// Internal files are left in the state they were before. If you need them loaded, do it after this.
854    pub fn merge(packs: &[Self]) -> Result<Self> {
855        if packs.is_empty() {
856            return Err(RLibError::NoPacksProvided);
857        }
858
859        // If we only got one pack, clone it and return it.
860        if packs.len() == 1 {
861            return Ok(packs[0].clone());
862        }
863
864        // Generate a new empty Pack to act as merged one. If all packs to merge share the same type, use that pack type.
865        let mut pack_new = Pack::default();
866
867        let mut pfh_types = packs.iter().map(|pack| pack.pfh_file_type()).collect::<Vec<_>>();
868        pfh_types.sort();
869        pfh_types.dedup();
870
871        if pfh_types.len() == 1 {
872            pack_new.set_pfh_file_type(pfh_types[0]);
873        }
874
875        packs.iter()
876            .chunk_by(|pack| pack.header.pfh_file_type)
877            .into_iter()
878            .for_each(|(_, packs)| {
879                let mut packs = packs.collect::<Vec<_>>();
880                packs.reverse();
881                packs.iter()
882                    .for_each(|pack| {
883                    pack_new.files_mut().extend(pack.files().clone())
884                });
885            });
886
887        // Fix the dependencies of the merged pack.
888        let pack_names = packs.iter().map(|pack| pack.disk_file_name()).collect::<Vec<_>>();
889        let mut dependencies = packs.iter()
890            .flat_map(|pack| pack.dependencies()
891                .iter()
892                .filter(|(_, dependency)| !pack_names.contains(dependency))
893                .cloned()
894                .collect::<Vec<_>>())
895            .collect::<Vec<_>>();
896
897        // Dedup the dependencies while preserving the order.
898        let mut set = HashSet::new();
899        dependencies.retain(|x| set.insert(x.clone()));
900        pack_new.set_dependencies(dependencies);
901
902        // Fix the pack version and header.
903        pack_new.set_pfh_file_type(packs[0].pfh_file_type());
904        pack_new.set_pfh_version(packs[0].pfh_version());
905
906        // Generate the path list.
907        pack_new.paths_cache_generate();
908
909        Ok(pack_new)
910    }
911
912    /// Convenience function to easily save a Pack to disk.
913    ///
914    /// If a path is provided, the Pack will be saved to that path. Otherwise, it'll use whatever path it had set before.
915    pub fn save(&mut self, path: Option<&Path>, game_info: &GameInfo, extra_data: &Option<EncodeableExtraData>) -> Result<()> {
916        if let Some(path) = path {
917            self.disk_file_path = path.to_string_lossy().to_string();
918        }
919
920        // Before truncating the file, make sure we loaded everything to memory.
921        self.files.iter_mut().try_for_each(|(_, file)| file.load())?;
922
923        let mut file = BufWriter::new(File::create(&self.disk_file_path)?);
924        let extra_data = if extra_data.is_some() {
925            extra_data.clone()
926        } else {
927            Some(EncodeableExtraData::new_from_game_info(game_info))
928        };
929
930        self.encode(&mut file, &extra_data)
931    }
932
933    //-----------------------------------------------------------------------//
934    //                           Getters & Setters
935    //-----------------------------------------------------------------------//
936
937    /// This function returns the current PFH Version of the provided Pack.
938    pub fn pfh_version(&self) -> PFHVersion {
939        *self.header.pfh_version()
940    }
941
942    /// This function returns the current PFH File Type of the provided Pack.
943    pub fn pfh_file_type(&self) -> PFHFileType {
944        *self.header.pfh_file_type()
945    }
946
947    /// This function returns the bitmask applied to the provided Pack.
948    pub fn bitmask(&self) -> PFHFlags {
949        *self.header.bitmask()
950    }
951
952    /// This function returns the timestamp of the last time the Pack was saved.
953    pub fn internal_timestamp(&self) -> u64 {
954        *self.header.internal_timestamp()
955    }
956
957    /// This function returns the Game version this Pack is intended for.
958    pub fn game_version(&self) -> u32 {
959        *self.header.game_version()
960    }
961
962    /// This function returns the build number of the game this Pack is intended for.
963    pub fn build_number(&self) -> u32 {
964        *self.header.build_number()
965    }
966
967    /// This function returns the tool that created the Pack. Max 8 characters, 00-padded.
968    pub fn authoring_tool(&self) -> &str {
969        self.header.authoring_tool()
970    }
971
972    /// This function returns the Extra Subheader Data, if any.
973    pub fn extra_subheader_data(&self) -> &[u8] {
974        self.header.extra_subheader_data()
975    }
976/*
977    /// This function changes the path of the Pack.
978    ///
979    /// This can fail if you pass it an empty path.
980    pub fn set_file_path(&mut self, path: &Path) -> Result<()> {
981        if path.components().count() == 0 { return Err(ErrorKind::EmptyInput.into()) }
982        self.file_path = path.to_path_buf();
983
984        // We have to change the name of the Pack in all his `Files` too.
985        let file_name = self.disk_file_name();
986        self.files.iter_mut().for_each(|x| x.get_ref_mut_raw().set_packfile_name(&file_name));
987        Ok(())
988    }*/
989
990    /// This function returns the compression format of the Pack.
991    pub fn compression_format(&self) -> CompressionFormat {
992        let cf = self.settings().setting_string(SETTING_KEY_CF).map(|x| x.to_owned());
993        CompressionFormat::from(cf.unwrap_or_default().as_str())
994    }
995
996    /// This function sets the current Pack PFH Version to the provided one.
997    pub fn set_pfh_version(&mut self, version: PFHVersion) {
998        self.header.set_pfh_version(version);
999    }
1000
1001    /// This function sets the current Pack PFH File Type to the provided one.
1002    pub fn set_pfh_file_type(&mut self, file_type: PFHFileType) {
1003        self.header.set_pfh_file_type(file_type);
1004    }
1005
1006    /// This function sets the current Pack bitmask to the provided one.
1007    pub fn set_bitmask(&mut self, bitmask: PFHFlags) {
1008        self.header.set_bitmask(bitmask);
1009    }
1010
1011    /// This function sets the current Pack timestamp to the provided one.
1012    pub fn set_internal_timestamp(&mut self, timestamp: u64) {
1013        self.header.set_internal_timestamp(timestamp);
1014    }
1015
1016    /// This function sets the game version (as in X.Y.Z) this Pack is for.
1017    pub fn set_game_version(&mut self, game_version: u32) {
1018        self.header.set_game_version(game_version);
1019    }
1020
1021    /// This function sets the build number this Pack is for.
1022    pub fn set_build_number(&mut self, build_number: u32) {
1023        self.header.set_build_number(build_number);
1024    }
1025
1026    /// This function sets the authoring tool that last edited this Pack.
1027    pub fn set_authoring_tool(&mut self, authoring_tool: &str) {
1028        self.header.set_authoring_tool(authoring_tool.to_string());
1029    }
1030
1031    /// This function sets the Extra Subheader Data of the Pack.
1032    pub fn set_extra_subheader_data(&mut self, extra_subheader_data: &[u8]) {
1033        self.header.set_extra_subheader_data(extra_subheader_data.to_vec());
1034    }
1035
1036    /// This function sets the compression format the pack should use.
1037    ///
1038    /// Returns the new compression format.
1039    /// Support for each format varies depending on the game.
1040    pub fn set_compression_format(&mut self, cf: CompressionFormat, game_info: &GameInfo) -> CompressionFormat {
1041        if cf == CompressionFormat::None || !game_info.compression_formats_supported().contains(&cf) {
1042            self.compress = false;
1043            self.settings_mut().set_setting_string(SETTING_KEY_CF, CompressionFormat::None.to_string().as_str());
1044            CompressionFormat::None
1045        } else {
1046            self.compress = true;
1047            self.settings_mut().set_setting_string(SETTING_KEY_CF, cf.to_string().as_str());
1048            cf
1049        }
1050    }
1051
1052    //-----------------------------------------------------------------------//
1053    //                             Util functions
1054    //-----------------------------------------------------------------------//
1055
1056    /// This function allows to toggle CA Authoring tool spoofing for this Pack.
1057    ///
1058    /// Passing spoof as false will reset the Authoring Tool to the default one.
1059    pub fn spoof_ca_authoring_tool(&mut self, spoof: bool) {
1060        if spoof {
1061            self.header.set_authoring_tool(AUTHORING_TOOL_CA.to_string());
1062        } else {
1063            self.header.set_authoring_tool(AUTHORING_TOOL_RPFM.to_string());
1064        }
1065    }
1066
1067    /// This function returns if the Pack is compressible or not.
1068    pub fn is_compressible(&self) -> bool {
1069        matches!(self.header.pfh_version, PFHVersion::PFH6 | PFHVersion::PFH5)
1070    }
1071
1072    /// This function returns the paths (as Strings) of the files used for missing loc data generation for the loaded Pack.
1073    ///
1074    /// The first one is the one for existing entries. The second one is the one for new entries.
1075    pub fn missing_locs_paths(&self) -> (String, String) {
1076        (
1077            MISSING_LOCS_PATH_START_EXISTING.to_owned() + &self.disk_file_name() + ".loc",
1078            MISSING_LOCS_PATH_START_NEW.to_owned() + &self.disk_file_name() + ".loc"
1079        )
1080    }
1081
1082    /// This function is used to generate all loc entries missing from a Pack into a missing.loc file.
1083    pub fn generate_missing_loc_data(&mut self, existing_locs: &HashMap<String, String>) -> Result<Vec<ContainerPath>> {
1084        let mut new_files = vec![];
1085
1086        let (missing_locs_path_existing, missing_locs_path_new) = self.missing_locs_paths();
1087
1088        let db_tables = self.files_by_type(&[FileType::DB]);
1089        let loc_tables = self.files_by_type(&[FileType::Loc]);
1090        let mut missing_trads_file_new = Loc::new();
1091        let mut missing_trads_file_overwritten = Loc::new();
1092
1093        let loc_keys_from_memory = loc_tables.par_iter().filter_map(|rfile| {
1094            if rfile.path_in_container_raw() != missing_locs_path_new && rfile.path_in_container_raw() != missing_locs_path_existing {
1095                if let Ok(RFileDecoded::Loc(table)) = rfile.decoded() {
1096                    Some(table.data().iter().filter_map(|x| {
1097                        if let DecodedData::StringU16(data) = &x[0] {
1098                            Some(data.to_owned())
1099                        } else {
1100                            None
1101                        }
1102                    }).collect::<HashSet<String>>())
1103                } else { None }
1104            } else { None }
1105        }).flatten().collect::<HashSet<String>>();
1106
1107        let (missing_trads_new, missing_trads_overwritten) = db_tables.par_iter().filter_map(|rfile| {
1108            if let Ok(RFileDecoded::DB(table)) = rfile.decoded() {
1109                let definition = table.definition();
1110                let loc_fields = definition.localised_fields();
1111                let table_data = table.data();
1112                let table_name = table.table_name_without_tables();
1113                let fields_processed = definition.fields_processed();
1114
1115                let has_loc_fields = !loc_fields.is_empty();
1116                let is_building_culture_variants = table_name == "building_culture_variants";
1117
1118                if has_loc_fields || is_building_culture_variants {
1119                    // Get the keys, which may be concatenated. We get them IN THE ORDER THEY ARE IN THE BINARY FILE.
1120                    let localised_order = definition.localised_key_order();
1121                    let mut new_rows_new = vec![];
1122                    let mut new_rows_overwritten = vec![];
1123
1124                    for row in table_data.iter() {
1125
1126                        // Generate locs for the table's own localised fields.
1127                        if has_loc_fields {
1128                            for loc_field in loc_fields {
1129                                let key = localised_order.iter().map(|pos| row[*pos as usize].data_to_string()).join("");
1130
1131                                // Key can be empty due to incomplete schema. Ignore those.
1132                                if !key.is_empty() {
1133                                    let loc_key = format!("{}_{}_{}", table_name, loc_field.name(), key);
1134
1135                                    if let Some(value) = existing_locs.get(&loc_key) {
1136                                        let mut new_row = missing_trads_file_overwritten.new_row();
1137                                        new_row[0] = DecodedData::StringU16(loc_key);
1138                                        new_row[1] = DecodedData::StringU16(value.to_owned());
1139                                        new_rows_overwritten.push(new_row);
1140
1141                                    } else if !loc_keys_from_memory.contains(&*loc_key) {
1142                                        let mut new_row = missing_trads_file_new.new_row();
1143                                        new_row[0] = DecodedData::StringU16(loc_key);
1144                                        new_row[1] = DecodedData::StringU16("PLACEHOLDER".to_owned());
1145                                        new_rows_new.push(new_row);
1146                                    }
1147                                }
1148                            }
1149                        }
1150
1151                        // Special case: building_culture_variants has a short_description column that references
1152                        // building_short_description_texts, a table that only exists in the Assembly Kit. We need to
1153                        // generate loc entries for it using the referenced table name as the loc key prefix.
1154                        if is_building_culture_variants {
1155                            if let Some(short_desc_index) = fields_processed.iter().position(|x| x.name() == "short_description") {
1156                                let key = row[short_desc_index].data_to_string();
1157                                if !key.is_empty() {
1158                                    let loc_key = format!("building_short_description_texts_short_description_{}", key);
1159
1160                                    if let Some(value) = existing_locs.get(&loc_key) {
1161                                        let mut new_row = missing_trads_file_overwritten.new_row();
1162                                        new_row[0] = DecodedData::StringU16(loc_key);
1163                                        new_row[1] = DecodedData::StringU16(value.to_owned());
1164                                        new_rows_overwritten.push(new_row);
1165
1166                                    } else if !loc_keys_from_memory.contains(&*loc_key) {
1167                                        let mut new_row = missing_trads_file_new.new_row();
1168                                        new_row[0] = DecodedData::StringU16(loc_key);
1169                                        new_row[1] = DecodedData::StringU16("PLACEHOLDER".to_owned());
1170                                        new_rows_new.push(new_row);
1171                                    }
1172                                }
1173                            }
1174                        }
1175                    }
1176
1177                    return Some((new_rows_new, new_rows_overwritten))
1178                }
1179            }
1180            None
1181        }).collect::<(Vec<Vec<Vec<DecodedData>>>, Vec<Vec<Vec<DecodedData>>>)>();
1182
1183        // NOTE: We do not use rayon's .flatten() because for some reason it eats values it's supposed to keep.
1184        let missing_trads_new = missing_trads_new.into_iter().flatten().collect::<Vec<_>>();
1185        let missing_trads_overwritten = missing_trads_overwritten.into_iter().flatten().collect::<Vec<_>>();
1186
1187        // Save the missing translations to two files: one for new translations, and another one for translations in use by this pack.
1188        if !missing_trads_new.is_empty() {
1189            let _ = missing_trads_file_new.set_data(&missing_trads_new);
1190            let packed_file = RFile::new_from_decoded(&RFileDecoded::Loc(missing_trads_file_new), 0, &missing_locs_path_new);
1191            new_files.push(self.insert(packed_file)?.unwrap());
1192        }
1193
1194        if !missing_trads_overwritten.is_empty() && !self.settings.setting_bool("do_not_generate_existing_locs").unwrap_or(&false) {
1195            let _ = missing_trads_file_overwritten.set_data(&missing_trads_overwritten);
1196            let packed_file = RFile::new_from_decoded(&RFileDecoded::Loc(missing_trads_file_overwritten), 0, &missing_locs_path_existing);
1197            new_files.push(self.insert(packed_file)?.unwrap());
1198        }
1199
1200        Ok(new_files)
1201    }
1202
1203    /// This function is used to patch Warhammer I & II Siege map packs so their AI actually works.
1204    ///
1205    /// This also removes the useless xml files left by Terry in the Pack.
1206    pub fn patch_siege_ai(&mut self) -> Result<(String, Vec<ContainerPath>)> {
1207
1208        // If there are no files, directly return an error.
1209        if self.files().is_empty() {
1210            return Err(RLibError::PatchSiegeAIEmptyPack)
1211        }
1212
1213        let mut files_patched = 0;
1214        let mut files_to_delete: Vec<ContainerPath> = vec![];
1215        let mut multiple_defensive_hill_hints = false;
1216
1217        // We only need to change stuff inside the map folder, so we only check the maps in that folder.
1218        for file in self.files_by_path_mut(&ContainerPath::Folder(TERRY_MAP_PATH.to_owned()), true) {
1219            let path = file.path_in_container_raw();
1220            let idx = path.rfind('/').unwrap_or(0);
1221            let name = if path.get(idx + 1..).is_some() {
1222                &path[idx + 1..]
1223            } else {
1224                continue
1225            };
1226
1227            // The files we need to process are `bmd_data.bin` and all the `catchment_` files the map has.
1228            if name == DEFAULT_BMD_DATA || (name.starts_with("catchment_") && name.ends_with(".bin")) {
1229                file.load()?;
1230                let data = file.cached_mut()?;
1231
1232                // The patching process it's simple. First, we check if there is SiegeAI stuff in the file by checking if there is an Area Node.
1233                // If we find one, we check if there is a defensive hill hint in the same file, and patch it if there is one.
1234                if data.windows(19).any(|window: &[u8]|window == SIEGE_AREA_NODE_HINT) {
1235                    if let Some(index) = data.windows(18).position(|window: &[u8]|window == DEFENSIVE_HILL_HINT) {
1236                        data.splice(index..index + 18, FORT_PERIMETER_HINT.iter().cloned());
1237                        files_patched += 1;
1238                    }
1239
1240                    // If there is more than one defensive hill in one file, is a valid file, but we want to warn the user about it.
1241                    if data.windows(18).any(|window: &[u8]|window == DEFENSIVE_HILL_HINT) {
1242                        multiple_defensive_hill_hints = true;
1243                    }
1244                }
1245            }
1246
1247            // All xml in this folder are useless, so we mark them all for deletion.
1248            else if name.ends_with(".xml") {
1249                files_to_delete.push(ContainerPath::File(file.path_in_container_raw().to_string()));
1250            }
1251        }
1252
1253        // If there are files to delete, we delete them.
1254        files_to_delete.iter().for_each(|x| { self.remove(x); });
1255
1256        // If we didn't found any file to patch or delete, return an error.
1257        if files_patched == 0 && files_to_delete.is_empty() { Err(RLibError::PatchSiegeAINoPatchableFiles) }
1258
1259        // TODO: make this more.... `fluent`.
1260        // If we found files to delete, but not to patch, return a message reporting it.
1261        else if files_patched == 0 {
1262            Ok((format!("No file suitable for patching has been found.\n{} files deleted.", files_to_delete.len()), files_to_delete))
1263        }
1264
1265        // If we found multiple defensive hill hints... it's ok, but we return a warning.
1266        else if multiple_defensive_hill_hints {
1267
1268            // The message is different depending on the amount of files deleted.
1269            if files_to_delete.is_empty() {
1270                Ok((format!("{files_patched} files patched.\nNo file suitable for deleting has been found.\
1271                \n\n\
1272                WARNING: Multiple Defensive Hints have been found and we only patched the first one.\
1273                 If you are using SiegeAI, you should only have one Defensive Hill in the map (the \
1274                 one acting as the perimeter of your fort/city/castle). Due to SiegeAI being present, \
1275                 in the map, normal Defensive Hills will not work anyways, and the only thing they do \
1276                 is interfere with the patching process. So, if your map doesn't work properly after \
1277                 patching, delete all the extra Defensive Hill Hints. They are the culprit."), files_to_delete))
1278            }
1279            else {
1280                Ok((format!("{} files patched.\n{} files deleted.\
1281                \n\n\
1282                WARNING: Multiple Defensive Hints have been found and we only patched the first one.\
1283                 If you are using SiegeAI, you should only have one Defensive Hill in the map (the \
1284                 one acting as the perimeter of your fort/city/castle). Due to SiegeAI being present, \
1285                 in the map, normal Defensive Hills will not work anyways, and the only thing they do \
1286                 is interfere with the patching process. So, if your map doesn't work properly after \
1287                 patching, delete all the extra Defensive Hill Hints. They are the culprit.",
1288                files_patched, files_to_delete.len()), files_to_delete))
1289            }
1290        }
1291
1292        // If no files to delete were found, but we got files patched, report it.
1293        else if files_to_delete.is_empty() {
1294            Ok((format!("{files_patched} files patched.\nNo file suitable for deleting has been found."), files_to_delete))
1295        }
1296
1297        // And finally, if we got some files patched and some deleted, report it too.
1298        else {
1299            Ok((format!("{} files patched.\n{} files deleted.", files_patched, files_to_delete.len()), files_to_delete))
1300        }
1301    }
1302
1303    /// Function to perform a live extraction, meaning the files will be extracted while the game is running,
1304    /// allowing for real-time updates and modifications without the need for a full game restart.
1305    ///
1306    /// Only works in Warhammer 3.
1307    pub fn live_export(&mut self, game: &GameInfo, game_path: &Path, disable_regen_table_guid: bool, keys_first: bool) -> Result<()> {
1308
1309        // If there are no files, directly return an error.
1310        if self.files().is_empty() {
1311            return Err(RLibError::LiveExportNoFilesToExport);
1312        }
1313
1314        let extra_data = Some(EncodeableExtraData::new_from_game_info_and_settings(game, self.compression_format(), disable_regen_table_guid));
1315        let data_path = game.data_path(game_path)?;
1316
1317        // We're interested in lua and xml files only, not those entire folders.
1318        let files = self.files_by_type_and_paths(&[FileType::Text], &[ContainerPath::Folder("script/".to_string()), ContainerPath::Folder("ui/".to_string())], true)
1319            .into_iter()
1320            .cloned()
1321            .collect::<Vec<RFile>>();
1322
1323        let mut correlations = HashMap::new();
1324        for mut file in files.into_iter() {
1325            let mut path_split = file.path_in_container_split().iter().map(|x| x.to_owned()).collect::<Vec<_>>();
1326            let mut hasher = DefaultHasher::new();
1327
1328            // Use time to ensure we never collide with a previous live export.
1329            std::time::SystemTime::now().hash(&mut hasher);
1330            let value = hasher.finish();
1331            let new_name = format!("{}_{}", value, path_split.last().unwrap());
1332
1333            *path_split.last_mut().unwrap() = &new_name;
1334            let new_path = path_split.join("/");
1335
1336            correlations.insert(file.path_in_container_raw().to_owned(), new_path.to_owned());
1337            file.set_path_in_container_raw(&new_path);
1338
1339            // To avoid duplicating logic, we insert these files into the pack, extract them, then delete them from the Pack.
1340            let container_path = file.path_in_container();
1341            self.insert(file)?;
1342            self.extract(container_path.clone(), &data_path, true, &None, false, keys_first, &extra_data)?;
1343
1344            self.remove(&container_path);
1345        }
1346
1347        // This is the file you have to call from lua later on.
1348        let summary_data_str = correlations.iter().map(|(key, value)| format!("    [\"{key}\"] = \"{value}\",")).join("\n");
1349        let summary_data_lua = format!("return {{\n{summary_data_str}\n}}");
1350        let summary_path = game_path.join("lua_path_mappings.txt");
1351        let mut file = BufWriter::new(File::create(summary_path)?);
1352        file.write_all(summary_data_lua.as_bytes())?;
1353
1354        Ok(())
1355    }
1356
1357    /// Function to update the anim ids of the pack on mass, based on a starting id and an offset.
1358    pub fn update_anim_ids(&mut self, game: &GameInfo, starting_id: i32, offset: i32) -> Result<Vec<ContainerPath>> {
1359        if offset == 0 {
1360            return Err(RLibError::UpdateAnimIdsError("Offset must be different than 0.".to_owned()))
1361        }
1362
1363        if starting_id < 0 {
1364            return Err(RLibError::UpdateAnimIdsError("Starting Id must be greater than 0.".to_owned()))
1365        }
1366
1367        // First, do a pass over sparse files.
1368        let mut extra_data = DecodeableExtraData::default();
1369        extra_data.set_game_info(Some(game));
1370        let extra_data = Some(extra_data);
1371
1372        let mut files = self.files_by_type_mut(&[FileType::AnimFragmentBattle]);
1373        let mut paths = files.par_iter_mut()
1374            .filter_map(|file| {
1375                let mut changed = false;
1376                if let Ok(Some(RFileDecoded::AnimFragmentBattle(mut table))) = file.decode(&extra_data, false, true) {
1377                    if *table.max_id() >= starting_id as u32 {
1378                        table.set_max_id(*table.max_id() + offset as u32);
1379                        changed = true;
1380                    }
1381
1382                    for entry in table.entries_mut() {
1383                        if *entry.animation_id() >= starting_id as u32 {
1384                            entry.set_animation_id(*entry.animation_id() + offset as u32);
1385                            changed = true;
1386                        }
1387
1388                        if *entry.slot_id() >= starting_id as u32 {
1389                            entry.set_slot_id(*entry.slot_id() + offset as u32);
1390                            changed = true;
1391                        }
1392                    }
1393
1394                    if changed {
1395                        let _ = file.set_decoded(RFileDecoded::AnimFragmentBattle(table));
1396                        Some(file.path_in_container())
1397                    } else {
1398                        None
1399                    }
1400                } else {
1401                    None
1402                }
1403            }
1404        ).collect::<Vec<_>>();
1405
1406        // Then, do another pass over files in AnimPacks. No need to do a par_iter because there is often less than 10 animpacks in packs.
1407        let mut anim_packs = self.files_by_type_mut(&[FileType::AnimPack]);
1408
1409        for anim_pack in anim_packs.iter_mut() {
1410            let mut changed = false;
1411            if let Ok(Some(RFileDecoded::AnimPack(mut pack))) = anim_pack.decode(&extra_data, false, true) {
1412
1413                let mut files = pack.files_by_type_mut(&[FileType::AnimFragmentBattle]);
1414                for file in files.iter_mut() {
1415                    if let Ok(Some(RFileDecoded::AnimFragmentBattle(mut table))) = file.decode(&extra_data, false, true) {
1416                        if *table.max_id() >= starting_id as u32 {
1417                            table.set_max_id(*table.max_id() + offset as u32);
1418                            changed = true;
1419                        }
1420
1421                        for entry in table.entries_mut() {
1422                            if *entry.animation_id() >= starting_id as u32 {
1423                                entry.set_animation_id(*entry.animation_id() + offset as u32);
1424                                changed = true;
1425                            }
1426
1427                            if *entry.slot_id() >= starting_id as u32 {
1428                                entry.set_slot_id(*entry.slot_id() + offset as u32);
1429                                changed = true;
1430                            }
1431                        }
1432
1433                        if changed {
1434                            let _ = file.set_decoded(RFileDecoded::AnimFragmentBattle(table));
1435                        }
1436                    }
1437                }
1438
1439                if changed {
1440                    let _ = anim_pack.set_decoded(RFileDecoded::AnimPack(pack));
1441                    paths.push(anim_pack.path_in_container());
1442                }
1443            }
1444        }
1445
1446        Ok(paths)
1447    }
1448}
1449
1450impl PackNotes {
1451
1452    /// This function tries to load the notes from the current Pack and return them.
1453    pub fn load(data: &[u8]) -> Result<Self> {
1454        from_slice(data).map_err(From::from)
1455    }
1456
1457    /// This function returns all notes afecting the provided path.
1458    pub fn notes_by_path(&self, path: &str) -> Vec<Note> {
1459        let path_lower = path.to_lowercase();
1460        self.file_notes()
1461            .iter()
1462            .filter(|(path, _)| path.is_empty() || path_lower.starts_with(*path) || &&path_lower == path)
1463            .flat_map(|(_, notes)| notes.to_vec())
1464            .collect()
1465    }
1466
1467    /// This function adds a note for an specific path.
1468    ///
1469    /// Note: for DB tables, notes are added for all tables with the same table name instead of specific tables.
1470    pub fn add_note(&mut self, mut note: Note) -> Note {
1471
1472        // For tables, share notes between same-type tables.
1473        let mut path = note.path().to_lowercase();
1474        if path.starts_with("db/") || path.starts_with("ceo_db/") {
1475            let mut new_path = path.split('/').collect::<Vec<_>>();
1476            if new_path.len() == 3 {
1477                new_path.pop();
1478            }
1479            path = new_path.join("/");
1480        }
1481        note.set_path(path.to_owned());
1482
1483        match self.file_notes_mut().get_mut(&path) {
1484            Some(notes) => {
1485
1486                // If it already has an id greater than 0, we're trying to replace and existing note if found.
1487                if *note.id() == 0 {
1488                    let id = notes.iter().map(|note| note.id()).max().unwrap();
1489                    note.set_id(*id + 1);
1490                } else {
1491                    notes.retain(|x| x.id() != note.id());
1492                }
1493
1494                notes.push(note.clone());
1495                note
1496            },
1497            None => {
1498                let notes = vec![note.clone()];
1499                self.file_notes_mut().insert(path.to_owned(), notes);
1500                note
1501            }
1502        }
1503    }
1504
1505    /// This function deletes a note with the specified path and id.
1506    pub fn delete_note(&mut self, path: &str, id: u64) {
1507        let path_lower = path.to_lowercase();
1508
1509        if let Some(notes) = self.file_notes_mut().get_mut(&path_lower) {
1510            notes.retain(|note| note.id() != &id);
1511            if notes.is_empty() {
1512                self.file_notes_mut().remove(&path_lower);
1513            }
1514        }
1515    }
1516}
1517
1518impl PackSettings {
1519
1520    /// This function tries to load the settings from a slice and return them.
1521    pub fn load(data: &[u8]) -> Result<Self> {
1522        from_slice(data).map_err(From::from)
1523    }
1524
1525    /// This function tries to load the settings from a slice, update them so they don't have any missing values and return them.
1526    pub fn load_and_update(&mut self, data: &[u8]) -> Result<()> {
1527        let settings: Self = from_slice(data)?;
1528
1529        self.settings_bool.extend(settings.settings_bool);
1530        self.settings_number.extend(settings.settings_number);
1531        self.settings_string.extend(settings.settings_string);
1532        self.settings_text.extend(settings.settings_text);
1533
1534        Ok(())
1535    }
1536
1537    /// This function returns the provided string setting, if found.
1538    pub fn setting_string(&self, key: &str) -> Option<&String> {
1539        self.settings_string.get(key)
1540    }
1541
1542    /// This function returns the provided text setting (multiline string), if found.
1543    pub fn setting_text(&self, key: &str) -> Option<&String> {
1544        self.settings_text.get(key)
1545    }
1546
1547    /// This function returns the provided bool setting, if found.
1548    pub fn setting_bool(&self, key: &str) -> Option<&bool> {
1549        self.settings_bool.get(key)
1550    }
1551
1552    /// This function returns the provided numeric setting, if found.
1553    pub fn setting_number(&self, key: &str) -> Option<&i32> {
1554        self.settings_number.get(key)
1555    }
1556
1557    /// This function sets the string setting provided with the value you passed.
1558    ///
1559    /// If the value already existed, it gets overwritten.
1560    pub fn set_setting_string(&mut self, key: &str, value: &str) {
1561        self.settings_string.insert(key.to_owned(), value.to_owned());
1562    }
1563
1564    /// This function sets the text (multiline string) setting provided with the value you passed.
1565    ///
1566    /// If the value already existed, it gets overwritten.
1567    pub fn set_setting_text(&mut self, key: &str, value: &str) {
1568        self.settings_text.insert(key.to_owned(), value.to_owned());
1569    }
1570
1571    /// This function sets the bool setting provided with the value you passed.
1572    ///
1573    /// If the value already existed, it gets overwritten.
1574    pub fn set_setting_bool(&mut self, key: &str, value: bool) {
1575        self.settings_bool.insert(key.to_owned(), value);
1576    }
1577
1578    /// This function sets the numeric setting provided with the value you passed.
1579    ///
1580    /// If the value already existed, it gets overwritten.
1581    pub fn set_setting_number(&mut self, key: &str, value: i32) {
1582        self.settings_number.insert(key.to_owned(), value);
1583    }
1584
1585    /// This function returns the list of paths which the diagnostic tool should ignore.
1586    ///
1587    /// TODO: Move this to rpfm_extensions.
1588    pub fn diagnostics_files_to_ignore(&self) -> Option<Vec<DiagnosticIgnoreEntry>> {
1589        self.settings_text.get("diagnostics_files_to_ignore").map(|files_to_ignore| {
1590            let files = files_to_ignore.split('\n').collect::<Vec<&str>>();
1591
1592            // Ignore commented out rows.
1593            files.iter().filter_map(|x| {
1594                if !x.starts_with('#') {
1595                    let path = x.splitn(3, ';').collect::<Vec<&str>>();
1596                    if path.len() == 3 {
1597                        Some((path[0].to_string(), path[1].split(',').filter_map(|y| if !y.is_empty() { Some(y.to_owned()) } else { None }).collect::<Vec<String>>(), path[2].split(',').filter_map(|y| if !y.is_empty() { Some(y.to_owned()) } else { None }).collect::<Vec<String>>()))
1598                    } else if path.len() == 2 {
1599                        Some((path[0].to_string(), path[1].split(',').filter_map(|y| if !y.is_empty() { Some(y.to_owned()) } else { None }).collect::<Vec<String>>(), vec![]))
1600                    } else if path.len() == 1 {
1601                        Some((path[0].to_string(), vec![], vec![]))
1602                    } else {
1603                        None
1604                    }
1605                } else {
1606                    None
1607                }
1608            }).collect::<Vec<DiagnosticIgnoreEntry>>()
1609        })
1610    }
1611}
1612
1613impl Default for PackHeader {
1614    fn default() -> Self {
1615        Self {
1616            pfh_version: Default::default(),
1617            pfh_file_type: Default::default(),
1618            bitmask: Default::default(),
1619            internal_timestamp: Default::default(),
1620            game_version: Default::default(),
1621            build_number: Default::default(),
1622            authoring_tool: AUTHORING_TOOL_RPFM.to_owned(),
1623            extra_subheader_data: Default::default(),
1624        }
1625    }
1626}
1627
1628impl Default for PFHFlags {
1629    fn default() -> Self {
1630        Self::empty()
1631    }
1632}
1633
1634impl Default for PackSettings {
1635    fn default() -> Self {
1636        let mut settings = Self {
1637            settings_text: BTreeMap::new(),
1638            settings_string: BTreeMap::new(),
1639            settings_bool: BTreeMap::new(),
1640            settings_number: BTreeMap::new(),
1641        };
1642
1643        settings.settings_text_mut().insert("diagnostics_files_to_ignore".to_owned(), "".to_owned());
1644        settings.settings_text_mut().insert("import_files_to_ignore".to_owned(), "".to_owned());
1645        settings.settings_bool_mut().insert("disable_autosaves".to_owned(), false);
1646        settings.settings_bool_mut().insert("do_not_generate_existing_locs".to_owned(), false);
1647        settings.settings_string_mut().insert(SETTING_KEY_CF.to_owned(), "None".to_owned());
1648        settings
1649    }
1650}