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