Skip to main content

rpfm_lib/games/
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//! Game-specific configuration and metadata for Total War games.
12//!
13//! This module provides comprehensive information about supported Total War games,
14//! including file formats, installation locations, and game-specific behaviors.
15//!
16//! # Overview
17//!
18//! RPFM supports multiple Total War games, each with different:
19//! - PackFile (PFH) format versions
20//! - Installation types (Steam/Epic/Wargaming, Windows/Linux)
21//! - File locations (/data, /content, config paths)
22//! - Assembly Kit versions and schemas
23//! - Localization and language support
24//! - Workshop tags and Steam integration
25//!
26//! # Main Types
27//!
28//! - [`GameInfo`]: Complete game configuration including paths, versions, and features
29//! - [`SupportedGames`]: Registry of all games supported by RPFM
30//! - [`InstallType`]: Platform and store variant (Steam/Epic/Wargaming, Windows/Linux)
31//! - [`InstallData`]: Installation-specific paths and identifiers
32//! - [`Manifest`]: Game manifest file parser for vanilla PackFile lists
33//! - [`PFHFileType`]: Type of PackFile (Boot, Release, Patch, Mod, Movie)
34//! - [`PFHVersion`]: PackFile format version
35//!
36//! # Usage Patterns
37//!
38//! ## Getting Game Information
39//!
40//! ```ignore
41//! use rpfm_lib::games::supported_games::{SupportedGames, KEY_WARHAMMER_3};
42//!
43//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
44//! let supported_games = SupportedGames::default();
45//! let game = supported_games.game(&KEY_WARHAMMER_3).unwrap();
46//!
47//! println!("Game: {}", game.display_name());
48//! println!("Schema: {}", game.schema_file_name());
49//! # Ok(())
50//! # }
51//! ```
52//!
53//! ## Working with Game Paths
54//!
55//! ```ignore
56//! # use rpfm_lib::games::supported_games::{SupportedGames, KEY_WARHAMMER_3};
57//! # use std::path::Path;
58//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
59//! # let supported_games = SupportedGames::default();
60//! # let game = supported_games.game(&KEY_WARHAMMER_3).unwrap();
61//! let game_path = Path::new("/path/to/game");
62//!
63//! // Get various game-specific paths
64//! let data_path = game.data_path(game_path)?;
65//! let content_path = game.content_path(game_path)?;
66//! let local_mods_path = game.local_mods_path(game_path)?;
67//!
68//! // Get vanilla PackFiles
69//! let ca_packs = game.ca_packs_paths(game_path)?;
70//! # Ok(())
71//! # }
72//! ```
73//!
74//! ## Detecting Installation Type
75//!
76//! ```ignore
77//! # use rpfm_lib::games::supported_games::{SupportedGames, KEY_WARHAMMER_3};
78//! # use rpfm_lib::games::InstallType;
79//! # use std::path::Path;
80//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
81//! # let supported_games = SupportedGames::default();
82//! # let game = supported_games.game(&KEY_WARHAMMER_3).unwrap();
83//! # let game_path = Path::new("/path/to/game");
84//! let install_type = game.install_type(game_path)?;
85//!
86//! match install_type {
87//!     InstallType::WinSteam => println!("Windows Steam version"),
88//!     InstallType::LnxSteam => println!("Linux Steam version"),
89//!     InstallType::WinEpic => println!("Windows Epic version"),
90//!     InstallType::WinWargaming => println!("Windows Wargaming version"),
91//! }
92//! # Ok(())
93//! # }
94//! ```
95//!
96//! # Submodules
97//!
98//! - [`supported_games`]: Game registry with all supported Total War titles
99//! - [`manifest`]: Manifest file parsing for game PackFile lists
100//! - [`pfh_file_type`]: PackFile type classifications
101//! - [`pfh_version`]: PackFile format version definitions
102
103use directories::ProjectDirs;
104use getset::*;
105use log::{info, warn};
106use steamlocate::SteamDir;
107
108use std::collections::HashMap;
109use std::{fmt, fmt::Display};
110use std::fs::{DirBuilder, File};
111use std::io::{BufReader, Read};
112use std::path::{Path, PathBuf};
113use std::sync::{Arc, RwLock};
114
115use crate::compression::CompressionFormat;
116use crate::error::{RLibError, Result};
117use crate::utils::*;
118
119use self::supported_games::*;
120use self::manifest::Manifest;
121use self::pfh_file_type::PFHFileType;
122use self::pfh_version::PFHVersion;
123
124pub mod supported_games;
125pub mod manifest;
126pub mod pfh_file_type;
127pub mod pfh_version;
128
129/// Language code: Brazilian Portuguese
130pub const BRAZILIAN: &str = "br";
131/// Language code: Simplified Chinese
132pub const SIMPLIFIED_CHINESE: &str = "cn";
133/// Language code: Czech
134pub const CZECH: &str = "cz";
135/// Language code: English
136pub const ENGLISH: &str = "en";
137/// Language code: French
138pub const FRENCH: &str = "fr";
139/// Language code: German
140pub const GERMAN: &str = "ge";
141/// Language code: Italian
142pub const ITALIAN: &str = "it";
143/// Language code: Korean
144pub const KOREAN: &str = "kr";
145/// Language code: Polish
146pub const POLISH: &str = "pl";
147/// Language code: Russian
148pub const RUSSIAN: &str = "ru";
149/// Language code: Spanish
150pub const SPANISH: &str = "sp";
151/// Language code: Turkish
152pub const TURKISH: &str = "tr";
153/// Language code: Traditional Chinese
154pub const TRADITIONAL_CHINESE: &str = "zh";
155
156/// Local folder name for Lua autogen files
157pub const LUA_AUTOGEN_FOLDER: &str = "tw_autogen";
158/// Git repository URL for Lua autogen type definitions
159pub const LUA_REPO: &str = "https://github.com/chadvandy/tw_autogen";
160/// Git remote name for Lua autogen repository
161pub const LUA_REMOTE: &str = "origin";
162/// Git branch name for Lua autogen repository
163pub const LUA_BRANCH: &str = "main";
164
165/// Git repository URL for old (pre-Shogun 2) Assembly Kit files
166pub const OLD_AK_REPO: &str = "https://github.com/Frodo45127/total_war_ak_files_pre_shogun_2";
167/// Git remote name for old Assembly Kit repository
168pub const OLD_AK_REMOTE: &str = "origin";
169/// Git branch name for old Assembly Kit repository
170pub const OLD_AK_BRANCH: &str = "master";
171
172/// Git repository URL for community translation hub
173pub const TRANSLATIONS_REPO: &str = "https://github.com/Frodo45127/total_war_translation_hub";
174/// Git remote name for translations repository
175pub const TRANSLATIONS_REMOTE: &str = "origin";
176/// Git branch name for translations repository
177pub const TRANSLATIONS_BRANCH: &str = "master";
178
179//-------------------------------------------------------------------------------//
180//                              Enums & Structs
181//-------------------------------------------------------------------------------//
182
183/// Complete configuration and metadata for a supported Total War game.
184///
185/// This struct contains all information needed for RPFM to work with a specific
186/// Total War game, including file formats, paths, features, and game-specific behaviors.
187///
188/// # Organization
189///
190/// The struct can be organized into several logical groups:
191/// - **Identity**: Key, display name
192/// - **File Formats**: PFH versions, schema files, compression formats
193/// - **Features**: Editing support, GUID requirements, portrait settings
194/// - **Assembly Kit**: Raw DB version, lost fields list
195/// - **Paths & Installation**: Install data per platform/store
196/// - **Localization**: Language file, locale support
197/// - **Tools**: Lua autogen, tool variables
198/// - **Restrictions**: Banned files, validation logic
199///
200/// # Access Patterns
201///
202/// Most fields are accessed through getters provided by the `Getters` derive macro.
203/// Some methods provide computed values based on installation detection:
204/// - [`GameInfo::install_type()`] - Detects the installation variant
205/// - [`GameInfo::data_path()`] - Resolves game-specific /data path
206/// - [`GameInfo::ca_packs_paths()`] - Lists vanilla PackFiles
207///
208/// # Installation Detection
209///
210/// The struct supports multiple installation types per game and automatically
211/// detects which one is present by examining executables and DLL files in the
212/// game directory. See [`GameInfo::install_type()`] for details.
213#[derive(Getters, Clone, Debug)]
214#[getset(get = "pub")]
215pub struct GameInfo {
216
217    /// Internal game identifier key (e.g., `"warhammer_3"`).
218    ///
219    /// Used for directory names, file lookups, and programmatic identification.
220    #[getset(skip)]
221    key: &'static str,
222
223    /// User-friendly display name (e.g., `"Warhammer 3"`).
224    ///
225    /// Shown in UI dropdowns and messages.
226    display_name: &'static str,
227
228    /// PackFile header versions by file type.
229    ///
230    /// Maps [`PFHFileType`] (Boot, Release, Patch, Mod, Movie) to the appropriate
231    /// [`PFHVersion`] for this game. If a type isn't in the map, defaults to Mod type.
232    pfh_versions: HashMap<PFHFileType, PFHVersion>,
233
234    /// Schema file name for this game (e.g., `"schema_wh3.ron"`).
235    ///
236    /// Used to load table definitions for decoding DB files.
237    schema_file_name: String,
238
239    /// Dependencies cache file name for this game.
240    ///
241    /// Stores cached dependency tree for faster pack loading.
242    dependencies_cache_file_name: String,
243
244    /// Assembly Kit version for raw database files.
245    ///
246    /// - `-1`: No Assembly Kit available
247    /// - `0`: Empire/Napoleon format
248    /// - `1`: Shogun 2 format
249    /// - `2`: Rome 2 and later format
250    raw_db_version: i16,
251
252    /// Portrait settings file version for this game.
253    ///
254    /// `None` if the game doesn't use portrait settings files.
255    portrait_settings_version: Option<u32>,
256
257    /// Whether PackFiles can be saved for this game.
258    ///
259    /// Some very old games are read-only.
260    supports_editing: bool,
261
262    /// Whether DB table headers include GUIDs.
263    ///
264    /// Newer games include a GUID in the table header for identification.
265    db_tables_have_guid: bool,
266
267    /// Language/locale file name (e.g., `"language.txt"`).
268    ///
269    /// `None` if the game doesn't use a language file, or if all locales are loaded.
270    locale_file_name: Option<String>,
271
272    /// Paths to files that RPFM should never edit.
273    ///
274    /// Contains table names or file paths that are protected by the game's integrity
275    /// checks to prevent bypassing DLC ownership validation.
276    banned_packedfiles: Vec<String>,
277
278    /// Small icon file name for UI display.
279    icon_small: String,
280
281    /// Large icon file name for UI display.
282    icon_big: String,
283
284    /// Logic for naming vanilla DB table files.
285    ///
286    /// Some games name tables after their folder, others use a default name.
287    vanilla_db_table_name_logic: VanillaDBTableNameLogic,
288
289    /// Installation data per platform/store combination.
290    ///
291    /// Contains paths, executables, and store IDs for different installation types.
292    /// Not exposed by getters - use [`GameInfo::install_data()`] instead.
293    #[getset(skip)]
294    install_data: HashMap<InstallType, InstallData>,
295
296    /// Game-specific tool variables.
297    ///
298    /// Key-value pairs for tool-specific configuration.
299    tool_vars: HashMap<String, String>,
300
301    /// Subdirectory name in Lua autogen repository for this game.
302    ///
303    /// `None` if Lua autogen doesn't support this game.
304    lua_autogen_folder: Option<String>,
305
306    /// Assembly Kit fields that are lost during export.
307    ///
308    /// List of `table_name.field_name` entries that exist in vanilla data
309    /// but don't appear in Assembly Kit exports because they are either unused
310    /// or separated from the tables on export.
311    ak_lost_fields: Vec<String>,
312
313    /// Internal cache for install type detection.
314    ///
315    /// Speeds up repeated calls to [`GameInfo::install_type()`].
316    #[getset(skip)]
317    install_type_cache: Arc<RwLock<HashMap<PathBuf, InstallType>>>,
318
319    /// Supported compression formats, newest to oldest.
320    ///
321    /// Used to determine which compression to use when saving files.
322    compression_formats_supported: Vec<CompressionFormat>,
323
324    /// Maximum CS2.parsed format version supported.
325    ///
326    /// Used for cross-game model conversion compatibility.
327    max_cs2_parsed_version: u32,
328}
329
330/// Strategy for naming vanilla DB table files.
331///
332/// Different Total War games use different conventions for naming their
333/// database table files in vanilla PackFiles.
334#[derive(Clone, Debug)]
335pub enum VanillaDBTableNameLogic {
336
337    /// Table files are named after their containing folder.
338    ///
339    /// Example: `db/units_tables/` contains file named `units_tables`
340    FolderName,
341
342    /// All table files use the same default name.
343    ///
344    /// Example: All tables are named `data` or similar
345    DefaultName(String),
346}
347
348/// Game installation platform and store variant.
349///
350/// Represents the different ways a Total War game can be installed,
351/// which affects executable names, DLL dependencies, and paths.
352#[derive(Clone, Debug, Hash, PartialEq, Eq)]
353pub enum InstallType {
354
355    /// Windows installation from Steam.
356    ///
357    /// Identified by presence of `steam_api.dll` or `steam_api64.dll`.
358    WinSteam,
359
360    /// Linux installation from Steam.
361    ///
362    /// Identified by Linux executable names.
363    LnxSteam,
364
365    /// Windows installation from Epic Games Store.
366    ///
367    /// Identified by presence of `EOSSDK-Win64-Shipping.dll`.
368    WinEpic,
369
370    /// Windows installation from Wargaming/Netease platform.
371    ///
372    /// Used for Arena and similar special distributions.
373    WinWargaming,
374}
375
376/// Installation-specific paths and identifiers.
377///
378/// Contains all the data that varies between different installation types of
379/// the same game (Steam vs Epic, Windows vs Linux, etc.).
380///
381/// # Path Relativity
382///
383/// **Important**: All paths in this struct are RELATIVE paths, either to:
384/// - The game's root directory (most paths)
385/// - The data directory (`vanilla_packs`)
386///
387/// This allows the same configuration to work across different installation
388/// locations by combining with the actual game path at runtime.
389#[derive(Getters, Clone, Debug)]
390#[getset(get = "pub")]
391pub struct InstallData {
392
393    /// Vanilla PackFile names (without paths).
394    ///
395    /// Used as fallback when no manifest file exists (Empire, Napoleon).
396    /// Paths are relative to the `data_path`.
397    vanilla_packs: Vec<String>,
398
399    /// Whether to use the game's manifest file for vanilla PackFile discovery.
400    ///
401    /// `true`: Read manifest file (most games)
402    /// `false`: Use hardcoded `vanilla_packs` list
403    use_manifest: bool,
404
405    /// Steam/store ID for the game.
406    ///
407    /// Used for Steam integration and auto-detection.
408    store_id: u64,
409
410    /// Steam/store ID for the game's Assembly Kit.
411    ///
412    /// `0` if no Assembly Kit is available on Steam.
413    store_id_ak: u64,
414
415    /// Game executable file name (with extension).
416    ///
417    /// Used to detect installation type and for launching the game.
418    /// Examples: `"Warhammer3.exe"`, `"Shogun2.exe"`
419    executable: String,
420
421    /// Data directory path relative to game root.
422    ///
423    /// Where PackFiles are stored. Usually `"data"` but varies.
424    data_path: String,
425
426    /// Language file directory path relative to game root.
427    ///
428    /// Where `language.txt` or equivalent is located.
429    /// May be different from `data_path` on Linux builds.
430    language_path: String,
431
432    /// Local mods directory path relative to game root.
433    ///
434    /// Where the game loads locally-installed mods from.
435    local_mods_path: String,
436
437    /// Downloaded mods directory path relative to game root.
438    ///
439    /// Where Steam Workshop and other downloaded mods are stored.
440    /// Empty string if the game doesn't support downloadable mods.
441    downloaded_mods_path: String,
442
443    /// Config directory name (not full path).
444    ///
445    /// Used with platform-specific config locations (AppData on Windows,
446    /// .config on Linux). `None` if game doesn't store config externally.
447    config_folder: Option<String>,
448}
449
450//-------------------------------------------------------------------------------//
451//                             Implementations
452//-------------------------------------------------------------------------------//
453
454impl Display for InstallType {
455    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
456        Display::fmt(match self {
457            Self::WinSteam => "Windows - Steam",
458            Self::LnxSteam => "Linux - Steam",
459            Self::WinEpic => "Windows - Epic",
460            Self::WinWargaming => "Windows - Wargaming",
461        }, f)
462    }
463}
464
465/// Implementation of GameInfo.
466impl GameInfo {
467
468    //---------------------------------------------------------------------------//
469    // Getters.
470    //---------------------------------------------------------------------------//
471
472    /// Returns the game's unique identifier key.
473    ///
474    /// The key is the game name in lowercase without spaces (e.g., `"warhammer_3"`, `"troy"`).
475    /// This is used for configuration files, file paths, and internal identification.
476    ///
477    /// # Returns
478    ///
479    /// A static string slice containing the game's key identifier.
480    ///
481    /// # Example
482    ///
483    /// ```ignore
484    /// use rpfm_lib::games::supported_games::{SupportedGames, KEY_WARHAMMER_3};
485    ///
486    /// let supported_games = SupportedGames::default();
487    /// let game_info = supported_games.game(&KEY_WARHAMMER_3).unwrap();
488    /// assert_eq!(game_info.key(), KEY_WARHAMMER_3);
489    /// ```
490    pub fn key(&self) -> &str {
491        self.key
492    }
493
494    /// Returns the PackFile format version for a specific file type.
495    ///
496    /// Different PackFile types (Boot, Release, Patch, Mod, Movie) may use different format
497    /// versions within the same game. This method looks up the appropriate [`PFHVersion`]
498    /// for the given [`PFHFileType`].
499    ///
500    /// # Arguments
501    ///
502    /// * `pfh_file_type` - The type of PackFile to look up
503    ///
504    /// # Returns
505    ///
506    /// The [`PFHVersion`] used for the specified file type. If no specific version is
507    /// configured for the file type, returns the version used for Mod PackFiles.
508    ///
509    /// # Example
510    ///
511    /// ```ignore
512    /// use rpfm_lib::games::supported_games::{SupportedGames, KEY_WARHAMMER_3};
513    /// use rpfm_lib::games::pfh_file_type::PFHFileType;
514    ///
515    /// let supported_games = SupportedGames::default();
516    /// let game_info = supported_games.game(&KEY_WARHAMMER_3).unwrap();
517    /// let mod_version = game_info.pfh_version_by_file_type(PFHFileType::Mod);
518    /// ```
519    pub fn pfh_version_by_file_type(&self, pfh_file_type: PFHFileType) -> PFHVersion {
520        match self.pfh_versions.get(&pfh_file_type) {
521            Some(pfh_version) => *pfh_version,
522            None => *self.pfh_versions.get(&PFHFileType::Mod).unwrap(),
523        }
524    }
525
526    //---------------------------------------------------------------------------//
527    // Advanced getters.
528    //---------------------------------------------------------------------------//
529
530    /// Detects the installation type (Steam, Epic, Wargaming, etc.) for a game installation.
531    ///
532    /// This method analyzes the game's directory structure and files to determine which
533    /// platform or distribution the game was installed from. The result is cached to avoid
534    /// repeated filesystem scans.
535    ///
536    /// # Detection Strategy
537    ///
538    /// 1. Checks for platform-specific executable names
539    /// 2. For Windows installations with multiple possible types:
540    ///    - Looks for `steam_api.dll` or `steam_api64.dll` for Steam
541    ///    - Looks for `EOSSDK-Win64-Shipping.dll` for Epic Games Store
542    ///    - Falls back to Wargaming/Netease if neither found
543    /// 3. Assumes Linux Steam for Linux installations
544    ///
545    /// # Arguments
546    ///
547    /// * `game_path` - Absolute path to the game's installation directory
548    ///
549    /// # Returns
550    ///
551    /// Returns the detected [`InstallType`], or an error if the path is invalid.
552    ///
553    /// # Performance
554    ///
555    /// Results are cached internally. First call takes ~10ms, subsequent calls are instant.
556    ///
557    /// # Example
558    ///
559    /// ```ignore
560    /// use rpfm_lib::games::supported_games::{SupportedGames, KEY_WARHAMMER_3};
561    /// use std::path::Path;
562    ///
563    /// let supported_games = SupportedGames::default();
564    /// let game_info = supported_games.game(&KEY_WARHAMMER_3).unwrap();
565    /// let game_path = Path::new("/path/to/game");
566    ///
567    /// match game_info.install_type(game_path) {
568    ///     Ok(install_type) => println!("Detected: {:?}", install_type),
569    ///     Err(e) => eprintln!("Detection failed: {}", e),
570    /// }
571    /// ```
572    pub fn install_type(&self, game_path: &Path) -> Result<InstallType> {
573
574        // This function takes 10ms to execute. In a few places, it's executed 2-5 times, and quickly adds up.
575        // So before executing it, check the cache to see if it has been executed before.
576        if let Some(install_type) = self.install_type_cache.read().unwrap().get(game_path) {
577            return Ok(install_type.clone());
578        }
579
580        // Checks to guess what kind of installation we have.
581        let base_path_files = files_from_subdir(game_path, false)?;
582        let install_type_by_exe = self.install_data.iter().filter_map(|(install_type, install_data)|
583            if base_path_files.iter().filter_map(|path| if path.is_file() { path.file_name() } else { None }).any(|filename| filename.to_ascii_lowercase() == *install_data.executable().to_lowercase()) {
584                Some(install_type)
585            } else { None }
586        ).collect::<Vec<&InstallType>>();
587
588        // If no compatible install data was found, use the first one we have.
589        if install_type_by_exe.is_empty() {
590            let install_type = self.install_data.keys().next().unwrap();
591            self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), install_type.clone());
592            Ok(install_type.clone())
593        }
594
595        // If we only have one install type compatible with the executable we have, return it.
596        else if install_type_by_exe.len() == 1 {
597            self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), install_type_by_exe[0].clone());
598            Ok(install_type_by_exe[0].clone())
599        }
600
601        // If we have multiple install data compatible, it gets more complex.
602        else {
603
604            // First, identify if we have a windows or linux build (mac only exists in your dreams.....).
605            // Can't be both because they have different exe names. Unless you're retarded and you merge both, in which case, fuck you.
606            let is_windows = install_type_by_exe.iter().any(|install_type| install_type == &&InstallType::WinSteam || install_type == &&InstallType::WinEpic || install_type == &&InstallType::WinWargaming);
607            if is_windows {
608
609                // Steam versions of the game have a "steam_api.dll" or "steam_api64.dll" file. Epic has "EOSSDK-Win64-Shipping.dll".
610                let has_steam_api_dll = base_path_files.iter().filter_map(|path| path.file_name()).any(|filename| filename == "steam_api.dll" || filename == "steam_api64.dll");
611                let has_eos_sdk_dll = base_path_files.iter().filter_map(|path| path.file_name()).any(|filename| filename == "EOSSDK-Win64-Shipping.dll");
612                if has_steam_api_dll && install_type_by_exe.contains(&&InstallType::WinSteam) {
613                    self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), InstallType::WinSteam);
614                    Ok(InstallType::WinSteam)
615                }
616
617                // If not, check wether we have epic libs.
618                else if has_eos_sdk_dll && install_type_by_exe.contains(&&InstallType::WinEpic) {
619                    self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), InstallType::WinEpic);
620                    Ok(InstallType::WinEpic)
621                }
622
623                // If neither of those are true, assume it's wargaming/netease (arena?).
624                else {
625                    self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), InstallType::WinWargaming);
626                    Ok(InstallType::WinWargaming)
627                }
628            }
629
630            // Otherwise, assume it's linux
631            else {
632                self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), InstallType::LnxSteam);
633                Ok(InstallType::LnxSteam)
634            }
635        }
636    }
637
638    /// Returns the installation-specific data for a game.
639    ///
640    /// After detecting the installation type, this method retrieves the corresponding
641    /// configuration data (executable names, paths, Steam IDs, etc.) for that installation.
642    ///
643    /// # Arguments
644    ///
645    /// * `game_path` - Absolute path to the game's installation directory
646    ///
647    /// # Returns
648    ///
649    /// Returns a reference to the [`InstallData`] for the detected installation type.
650    ///
651    /// # Errors
652    ///
653    /// Returns an error if:
654    /// - The installation type cannot be detected
655    /// - The detected installation type is not supported for this game
656    pub fn install_data(&self, game_path: &Path) -> Result<&InstallData> {
657        let install_type = self.install_type(game_path)?;
658        let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
659        Ok(install_data)
660    }
661
662    /// Returns the path to the game's `/data` directory.
663    ///
664    /// The `/data` directory contains the game's vanilla PackFiles and is the primary
665    /// location for game content. The exact directory name may vary by game and platform.
666    ///
667    /// # Arguments
668    ///
669    /// * `game_path` - Absolute path to the game's installation directory
670    ///
671    /// # Returns
672    ///
673    /// Absolute path to the `/data` directory (or platform-specific equivalent).
674    ///
675    /// # Errors
676    ///
677    /// Returns an error if the installation type cannot be detected or is not supported.
678    pub fn data_path(&self, game_path: &Path) -> Result<PathBuf> {
679        let install_type = self.install_type(game_path)?;
680        let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
681        Ok(game_path.join(install_data.data_path()))
682    }
683
684    /// Returns the path to the downloaded mods directory.
685    ///
686    /// This is the directory where Steam Workshop or other platform mods are downloaded to.
687    /// Not all games support downloaded mods through official platforms.
688    ///
689    /// # Arguments
690    ///
691    /// * `game_path` - Absolute path to the game's installation directory
692    ///
693    /// # Returns
694    ///
695    /// Absolute path to the downloaded mods directory.
696    ///
697    /// # Errors
698    ///
699    /// Returns an error if the installation type cannot be detected or is not supported.
700    pub fn content_path(&self, game_path: &Path) -> Result<PathBuf> {
701        let install_type = self.install_type(game_path)?;
702        let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
703        Ok(game_path.join(install_data.downloaded_mods_path()))
704    }
705
706    /// Returns the directory containing the game's language configuration file.
707    ///
708    /// The language configuration file (typically `language.txt` or similar) stores the
709    /// player's selected interface language. The file location varies by game and may be
710    /// nested inside a language-specific subdirectory.
711    ///
712    /// # Behavior
713    ///
714    /// If the language file is in the base directory, returns that directory. Otherwise,
715    /// searches through language-specific subdirectories (brazilian, chinese, english, etc.)
716    /// and returns the first one found.
717    ///
718    /// # Arguments
719    ///
720    /// * `game_path` - Absolute path to the game's installation directory
721    ///
722    /// # Returns
723    ///
724    /// Absolute path to the directory containing the language file.
725    ///
726    /// # Errors
727    ///
728    /// Returns an error if the installation type cannot be detected or is not supported.
729    pub fn language_path(&self, game_path: &Path) -> Result<PathBuf> {
730
731        // For games that don't support
732        let language_file_name = self.locale_file_name().clone().unwrap_or_else(|| "language.txt".to_owned());
733
734        let install_type = self.install_type(game_path)?;
735        let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
736        let base_path = game_path.join(install_data.language_path());
737
738        // The language files are either in this folder, or in a folder with the locale value inside this folder.
739        let path_with_file = base_path.join(language_file_name);
740        if path_with_file.is_file() {
741            Ok(base_path)
742        } else {
743
744            // Yes, this is ugly. But I'm not the retarded idiot that decided to put the file that sets the language used inside a folder specific of the language used.
745            let path = base_path.join(BRAZILIAN);
746            if path.is_dir() {
747                return Ok(path);
748            }
749            let path = base_path.join(SIMPLIFIED_CHINESE);
750            if path.is_dir() {
751                return Ok(path);
752            }
753            let path = base_path.join(CZECH);
754            if path.is_dir() {
755                return Ok(path);
756            }
757            let path = base_path.join(ENGLISH);
758            if path.is_dir() {
759                return Ok(path);
760            }
761            let path = base_path.join(FRENCH);
762            if path.is_dir() {
763                return Ok(path);
764            }
765            let path = base_path.join(GERMAN);
766            if path.is_dir() {
767                return Ok(path);
768            }
769            let path = base_path.join(ITALIAN);
770            if path.is_dir() {
771                return Ok(path);
772            }
773            let path = base_path.join(KOREAN);
774            if path.is_dir() {
775                return Ok(path);
776            }
777            let path = base_path.join(POLISH);
778            if path.is_dir() {
779                return Ok(path);
780            }
781            let path = base_path.join(RUSSIAN);
782            if path.is_dir() {
783                return Ok(path);
784            }
785            let path = base_path.join(SPANISH);
786            if path.is_dir() {
787                return Ok(path);
788            }
789            let path = base_path.join(TURKISH);
790            if path.is_dir() {
791                return Ok(path);
792            }
793            let path = base_path.join(TRADITIONAL_CHINESE);
794            if path.is_dir() {
795                return Ok(path);
796            }
797
798            // If no path exists, we just return the base path.
799            Ok(base_path)
800        }
801    }
802
803    /// Returns the path to the local mods directory.
804    ///
805    /// This is where locally-installed mods are loaded from by the game. The location
806    /// varies by game:
807    /// - **Troy**: A separate directory from `/data` to avoid polluting the data folder
808    /// - **Other games**: Points to the `/data` directory
809    ///
810    /// Mods placed in this directory are loaded by the game without requiring workshop
811    /// or platform distribution.
812    ///
813    /// # Arguments
814    ///
815    /// * `game_path` - Absolute path to the game's installation directory
816    ///
817    /// # Returns
818    ///
819    /// Absolute path to the local mods directory.
820    ///
821    /// # Errors
822    ///
823    /// Returns an error if the installation type cannot be detected or is not supported.
824    pub fn local_mods_path(&self, game_path: &Path) -> Result<PathBuf> {
825        let install_type = self.install_type(game_path)?;
826        let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
827        Ok(game_path.join(install_data.local_mods_path()))
828    }
829
830    /// Returns paths to all PackFiles in the downloaded mods directory.
831    ///
832    /// Recursively scans the downloaded mods directory (Steam Workshop, etc.) and returns
833    /// paths to all `.pack` and `.bin` files found. Returns `None` if the game doesn't
834    /// support downloaded mods or the directory doesn't exist.
835    ///
836    /// # Arguments
837    ///
838    /// * `game_path` - Absolute path to the game's installation directory
839    ///
840    /// # Returns
841    ///
842    /// A sorted vector of absolute paths to PackFiles, or `None` if not applicable.
843    ///
844    /// # File Extensions
845    ///
846    /// Searches for both `.pack` and `.bin` extensions as some games use `.bin` for
847    /// certain mod types.
848    pub fn content_packs_paths(&self, game_path: &Path) -> Option<Vec<PathBuf>> {
849        let install_type = self.install_type(game_path).ok()?;
850        let install_data = self.install_data.get(&install_type)?;
851        let downloaded_mods_path = install_data.downloaded_mods_path();
852
853        // If the path is empty, it means this game does not support downloaded mods.
854        if downloaded_mods_path.is_empty() {
855            return None;
856        }
857
858        let path = std::fs::canonicalize(game_path.join(downloaded_mods_path)).ok()?;
859        let mut paths = vec![];
860
861        for path in files_from_subdir(&path, true).ok()?.iter() {
862            match path.extension() {
863                Some(extension) => if extension == "pack" || extension == "bin" { paths.push(path.to_path_buf()); }
864                None => continue,
865            }
866        }
867
868        paths.sort();
869        Some(paths)
870    }
871
872    /// Returns paths to all PackFiles in a secondary mods directory.
873    ///
874    /// Some users keep additional mod collections in custom directories outside the game
875    /// installation. This method scans a user-specified secondary path for PackFiles.
876    ///
877    /// The secondary path should contain a subdirectory named after the game's key
878    /// (e.g., `secondary_path/warhammer_3/`), which is then scanned for `.pack` files.
879    ///
880    /// # Arguments
881    ///
882    /// * `secondary_path` - Absolute path to the base secondary mods directory
883    ///
884    /// # Returns
885    ///
886    /// A sorted vector of absolute paths to PackFiles, or `None` if:
887    /// - The path is not absolute, doesn't exist, or isn't a directory
888    /// - The game-specific subdirectory doesn't exist
889    /// - No `.pack` files are found
890    ///
891    /// # Path Structure
892    ///
893    /// Expected structure: `secondary_path/{game_key}/*.pack`
894    pub fn secondary_packs_paths(&self, secondary_path: &Path) -> Option<Vec<PathBuf>> {
895        if !secondary_path.is_dir() || !secondary_path.exists() || !secondary_path.is_absolute() {
896            return None;
897        }
898
899        let game_path = secondary_path.join(self.key());
900        if !game_path.is_dir() || !game_path.exists() {
901            return None;
902        }
903
904        let mut paths = vec![];
905
906        for path in files_from_subdir(&game_path, false).ok()?.iter() {
907            match path.extension() {
908                Some(extension) => if extension == "pack" {
909                    paths.push(path.to_path_buf());
910                }
911                None => continue,
912            }
913        }
914
915        paths.sort();
916        Some(paths)
917    }
918
919    /// Returns paths to all PackFiles in the game's `/data` directory.
920    ///
921    /// Scans the game's main data directory (non-recursively) for all `.pack` files.
922    /// This typically includes vanilla game PackFiles and any mods installed directly
923    /// in the data directory.
924    ///
925    /// # Arguments
926    ///
927    /// * `game_path` - Absolute path to the game's installation directory
928    ///
929    /// # Returns
930    ///
931    /// A sorted vector of absolute paths to PackFiles, or `None` if:
932    /// - The data directory cannot be determined
933    /// - The directory doesn't exist or cannot be read
934    /// - No `.pack` files are found
935    pub fn data_packs_paths(&self, game_path: &Path) -> Option<Vec<PathBuf>> {
936        let game_path = self.data_path(game_path).ok()?;
937        let mut paths = vec![];
938
939        for path in files_from_subdir(&game_path, false).ok()?.iter() {
940            match path.extension() {
941                Some(extension) => if extension == "pack" { paths.push(path.to_path_buf()); }
942                None => continue,
943            }
944        }
945
946        paths.sort();
947        Some(paths)
948    }
949
950
951    /// Returns the installation path for "MyMod" PackFiles.
952    ///
953    /// Returns the directory where mods created with RPFM's "MyMod" feature should be
954    /// installed. This is typically the local mods directory. Creates the directory
955    /// if it doesn't exist.
956    ///
957    /// # Arguments
958    ///
959    /// * `game_path` - Absolute path to the game's installation directory
960    ///
961    /// # Returns
962    ///
963    /// Absolute path to the MyMod installation directory, or `None` if:
964    /// - The installation type cannot be detected
965    /// - The directory cannot be created
966    pub fn mymod_install_path(&self, game_path: &Path) -> Option<PathBuf> {
967        let install_type = self.install_type(game_path).ok()?;
968        let install_data = self.install_data.get(&install_type)?;
969        let path = game_path.join(PathBuf::from(install_data.local_mods_path()));
970
971        // Make sure the folder exists.
972        DirBuilder::new().recursive(true).create(&path).ok()?;
973
974        Some(path)
975    }
976
977    /// Returns whether to use the game's manifest file for discovering vanilla PackFiles.
978    ///
979    /// Some games have a `manifest.txt` file that lists all official PackFiles. This method
980    /// determines whether RPFM should use that manifest or fall back to a hardcoded list
981    /// of PackFile names.
982    ///
983    /// # Decision Logic
984    ///
985    /// Returns `false` (don't use manifest) if:
986    /// - The installation is Linux (manifests may be unreliable)
987    /// - A hardcoded PackFile list exists for this game/install type
988    ///
989    /// # Arguments
990    ///
991    /// * `game_path` - Absolute path to the game's installation directory
992    ///
993    /// # Returns
994    ///
995    /// `true` if the manifest should be used, `false` if the hardcoded list should be used.
996    ///
997    /// # Errors
998    ///
999    /// Returns an error if the installation type cannot be detected or is not supported.
1000    pub fn use_manifest(&self, game_path: &Path) -> Result<bool> {
1001        let install_type = self.install_type(game_path)?;
1002        let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
1003
1004        // If the install_type is linux, or we actually have a hardcoded list, ignore all Manifests.
1005        Ok(*install_data.use_manifest())
1006    }
1007
1008    /// Returns the Steam App ID for the game installation.
1009    ///
1010    /// The Steam App ID is used for launching games via Steam, checking workshop content,
1011    /// and other Steam-specific integrations.
1012    ///
1013    /// # Arguments
1014    ///
1015    /// * `game_path` - Absolute path to the game's installation directory
1016    ///
1017    /// # Returns
1018    ///
1019    /// The Steam App ID as a 64-bit unsigned integer.
1020    ///
1021    /// # Errors
1022    ///
1023    /// Returns an error if:
1024    /// - The installation type cannot be detected
1025    /// - The installation is not a Steam installation (Windows or Linux)
1026    /// - The installation type is not supported for this game
1027    pub fn steam_id(&self, game_path: &Path) -> Result<u64> {
1028        let install_type = self.install_type(game_path)?;
1029        let install_data = match install_type {
1030            InstallType::WinSteam |
1031            InstallType::LnxSteam => self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?,
1032            _ => return Err(RLibError::ReservedFiles)
1033        };
1034
1035        Ok(*install_data.store_id())
1036    }
1037
1038    /// Returns paths to all Creative Assembly (vanilla) PackFiles.
1039    ///
1040    /// Discovers all official game PackFiles in the data directory. Uses the game's manifest
1041    /// file if available and configured, otherwise falls back to a hardcoded list or scanning
1042    /// all PackFiles.
1043    ///
1044    /// # Language Filtering
1045    ///
1046    /// For games with multiple language packs (e.g., Warhammer 3), only returns PackFiles
1047    /// matching the configured game language. This prevents loading multiple language
1048    /// localizations simultaneously.
1049    ///
1050    /// Language-specific PackFiles typically have `local_{language}` in their names
1051    /// (e.g., `local_en.pack`, `local_es.pack`).
1052    ///
1053    /// # Arguments
1054    ///
1055    /// * `game_path` - Absolute path to the game's installation directory
1056    ///
1057    /// # Returns
1058    ///
1059    /// A sorted vector of absolute paths to vanilla PackFiles.
1060    ///
1061    /// # Errors
1062    ///
1063    /// Returns an error if:
1064    /// - The game language cannot be determined
1065    /// - The data directory cannot be accessed
1066    /// - The installation type is not supported
1067    ///
1068    /// # Fallback Behavior
1069    ///
1070    /// If manifest reading fails, automatically falls back to `ca_packs_paths_no_manifest()`.
1071    pub fn ca_packs_paths(&self, game_path: &Path) -> Result<Vec<PathBuf>> {
1072
1073        // Check if we have to filter by language, to avoid overwriting our language with another one.
1074        let language = self.game_locale_from_file(game_path)?;
1075
1076        // Check if we can use the manifest for this.
1077        if !self.use_manifest(game_path)? {
1078            self.ca_packs_paths_no_manifest(game_path, &language)
1079        } else {
1080
1081            // Try to get the manifest, if exists.
1082            match Manifest::read_from_game_path(self, game_path) {
1083                Ok(manifest) => {
1084                    let data_path = self.data_path(game_path)?;
1085                    let mut paths = manifest.0.iter().filter_map(|entry|
1086                        if entry.relative_path().ends_with(".pack") {
1087
1088                            let mut pack_file_path = data_path.to_path_buf();
1089                            pack_file_path.push(entry.relative_path());
1090                            match &language {
1091                                Some(language) => {
1092
1093                                    // Filter out other language's packfiles.
1094                                    if entry.relative_path().contains("local_") {
1095                                        let language = "local_".to_owned() + language;
1096                                        if entry.relative_path().contains(&language) {
1097                                            entry.path_from_manifest_entry(pack_file_path)
1098                                        } else {
1099                                            None
1100                                        }
1101                                    } else {
1102                                        entry.path_from_manifest_entry(pack_file_path)
1103                                    }
1104                                }
1105                                None => entry.path_from_manifest_entry(pack_file_path),
1106                            }
1107                        } else { None }
1108                        ).collect::<Vec<PathBuf>>();
1109
1110                    paths.sort();
1111                    Ok(paths)
1112                }
1113
1114                // If there is no manifest, use the hardcoded file list for the game, if it has one.
1115                Err(_) => self.ca_packs_paths_no_manifest(game_path, &language)
1116            }
1117        }
1118    }
1119
1120    /// Returns vanilla PackFiles without using a manifest (internal fallback).
1121    ///
1122    /// This is an internal method used by [`ca_packs_paths`] when no manifest is available
1123    /// or manifest reading fails. Uses a hardcoded list of PackFile names if available,
1124    /// otherwise returns all `.pack` files in the data directory.
1125    ///
1126    /// # Language Filtering
1127    ///
1128    /// Like [`ca_packs_paths`], filters language-specific PackFiles to only include the
1129    /// configured game language.
1130    ///
1131    /// # Arguments
1132    ///
1133    /// * `game_path` - Absolute path to the game's installation directory
1134    /// * `language` - Optional language code to filter localization PackFiles
1135    ///
1136    /// # Returns
1137    ///
1138    /// A vector of absolute paths to vanilla PackFiles.
1139    ///
1140    /// # Errors
1141    ///
1142    /// Returns an error if:
1143    /// - The data directory cannot be accessed
1144    /// - The installation type is not supported
1145    fn ca_packs_paths_no_manifest(&self, game_path: &Path, language: &Option<String>) -> Result<Vec<PathBuf>> {
1146        let data_path = self.data_path(game_path)?;
1147        let install_type = self.install_type(game_path)?;
1148        let vanilla_packs = &self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?.vanilla_packs;
1149        let language_pack = language.clone().map(|lang| format!("local_{lang}"));
1150        if !vanilla_packs.is_empty() {
1151            Ok(vanilla_packs.iter().filter_map(|pack_name| {
1152
1153                let mut pack_file_path = data_path.to_path_buf();
1154                pack_file_path.push(pack_name);
1155                match language_pack {
1156                    Some(ref language_pack) => {
1157
1158                        // Filter out other language's packfiles.
1159                        if !pack_name.is_empty() && pack_name.starts_with("local_") {
1160                            if pack_name.starts_with(language_pack) {
1161                                std::fs::canonicalize(pack_file_path).ok()
1162                            } else {
1163                                None
1164                            }
1165                        } else {
1166                            std::fs::canonicalize(pack_file_path).ok()
1167                        }
1168                    }
1169                    None => std::fs::canonicalize(pack_file_path).ok(),
1170                }
1171            }).collect::<Vec<PathBuf>>())
1172        }
1173
1174        // If there is no hardcoded list, get every path.
1175        else {
1176            Ok(files_from_subdir(&data_path, false)?.iter()
1177                .filter_map(|x| if let Some(extension) = x.extension() {
1178                    if extension.to_string_lossy().to_lowercase() == "pack" {
1179                        Some(x.to_owned())
1180                    } else { None }
1181                } else { None }).collect::<Vec<PathBuf>>()
1182            )
1183        }
1184    }
1185
1186    /// Returns the launch URI for starting the game.
1187    ///
1188    /// Generates a platform-specific URI or command that can be used to launch the game
1189    /// from external applications or scripts.
1190    ///
1191    /// # Platform Support
1192    ///
1193    /// Currently only supports Steam installations (Windows and Linux), which use Steam URIs
1194    /// in the format `steam://rungameid/{app_id}`.
1195    ///
1196    /// # Arguments
1197    ///
1198    /// * `game_path` - Absolute path to the game's installation directory
1199    ///
1200    /// # Returns
1201    ///
1202    /// A string containing the launch URI or command.
1203    ///
1204    /// # Errors
1205    ///
1206    /// Returns an error if:
1207    /// - The installation type cannot be detected
1208    /// - The installation platform doesn't support programmatic launching (e.g., Epic, Wargaming)
1209    /// - The installation type is not supported for this game
1210    pub fn game_launch_command(&self, game_path: &Path) -> Result<String> {
1211        let install_type = self.install_type(game_path)?;
1212
1213        match install_type {
1214            InstallType::LnxSteam |
1215            InstallType::WinSteam => {
1216                let store_id = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?.store_id();
1217                Ok(format!("steam://rungameid/{store_id}"))
1218            },
1219            _ => Err(RLibError::GameInstallLaunchNotSupported(self.display_name.to_string(), install_type.to_string())),
1220        }
1221    }
1222
1223    /// Returns the path to the game's executable file.
1224    ///
1225    /// # Arguments
1226    ///
1227    /// * `game_path` - Absolute path to the game's installation directory
1228    ///
1229    /// # Returns
1230    ///
1231    /// Absolute path to the game executable, or `None` if:
1232    /// - The installation type cannot be detected
1233    /// - The installation type is not supported
1234    pub fn executable_path(&self, game_path: &Path) -> Option<PathBuf> {
1235        let install_type = self.install_type(game_path).ok()?;
1236        let install_data = self.install_data.get(&install_type)?;
1237        let executable_path = game_path.join(install_data.executable());
1238
1239        Some(executable_path)
1240    }
1241
1242    /// Returns the path to the game's configuration directory.
1243    ///
1244    /// Total War games store user configuration, preferences, and save files in a
1245    /// platform-specific configuration directory (e.g., AppData on Windows, ~/.config on Linux).
1246    ///
1247    /// # Arguments
1248    ///
1249    /// * `game_path` - Absolute path to the game's installation directory
1250    ///
1251    /// # Returns
1252    ///
1253    /// Absolute path to the game's configuration directory, or `None` if:
1254    /// - The installation type cannot be detected
1255    /// - The game doesn't have a defined configuration folder
1256    /// - The platform-specific configuration path cannot be determined
1257    pub fn config_path(&self, game_path: &Path) -> Option<PathBuf> {
1258        let install_type = self.install_type(game_path).ok()?;
1259        let install_data = self.install_data.get(&install_type)?;
1260        let config_folder = install_data.config_folder.as_ref()?;
1261
1262        ProjectDirs::from("com", "The Creative Assembly", config_folder).map(|dir| {
1263            let mut dir = dir.config_dir().to_path_buf();
1264            dir.pop();
1265            dir
1266        })
1267    }
1268
1269    /// Checks if a file path is banned from modification.
1270    ///
1271    /// Some game files are protected by integrity checks to prevent bypassing DLC
1272    /// ownership validation. This method checks if a file path matches any banned
1273    /// path prefixes.
1274    ///
1275    /// # Arguments
1276    ///
1277    /// * `path` - The file path to check (typically a PackFile-relative path)
1278    ///
1279    /// # Returns
1280    ///
1281    /// `true` if the file is banned and should not be modified, `false` otherwise.
1282    ///
1283    /// # Comparison
1284    ///
1285    /// The comparison is case-insensitive and uses prefix matching.
1286    pub fn is_file_banned(&self, path: &str) -> bool {
1287        let path = path.to_lowercase();
1288        self.banned_packedfiles.iter().any(|x| path.starts_with(x))
1289    }
1290
1291    /// Retrieves a game-specific tool variable.
1292    ///
1293    /// Tool variables are key-value pairs used to configure tool behavior for specific
1294    /// games. Examples might include special file paths, version numbers, or feature flags.
1295    ///
1296    /// # Arguments
1297    ///
1298    /// * `var` - The variable name to look up
1299    ///
1300    /// # Returns
1301    ///
1302    /// The variable value if found, or `None` if the variable is not defined for this game.
1303    pub fn tool_var(&self, var: &str) -> Option<&String> {
1304        self.tool_vars.get(var)
1305    }
1306
1307    /// Reads the game's configured language from its configuration file.
1308    ///
1309    /// Attempts to read the game's language setting from its `language.txt` or equivalent
1310    /// configuration file. This determines which localization PackFiles should be loaded.
1311    ///
1312    /// # Language Codes
1313    ///
1314    /// The file typically contains a 2-letter code (e.g., "EN", "ES", "DE") which is
1315    /// mapped to the full language name used in PackFile names.
1316    ///
1317    /// # Arguments
1318    ///
1319    /// * `game_path` - Absolute path to the game's installation directory
1320    ///
1321    /// # Returns
1322    ///
1323    /// - `Ok(Some(language))` - Language successfully read and mapped
1324    /// - `Ok(Some("english"))` - File missing/unreadable, defaulted to English
1325    /// - `Ok(None)` - Game doesn't use a language configuration file
1326    ///
1327    /// # Errors
1328    ///
1329    /// Returns an error if the language file path cannot be determined due to
1330    /// installation type detection failures.
1331    pub fn game_locale_from_file(&self, game_path: &Path) -> Result<Option<String>> {
1332        match self.locale_file_name() {
1333            Some(locale_file) => {
1334                let language_path = self.language_path(game_path)?;
1335                let locale_path = language_path.join(locale_file);
1336                let mut language = String::new();
1337                if let Ok(mut file) = File::open(locale_path) {
1338                    file.read_to_string(&mut language)?;
1339
1340                    let language = match &*language {
1341                        "BR" => BRAZILIAN.to_owned(),
1342                        "CN" => SIMPLIFIED_CHINESE.to_owned(),
1343                        "CZ" => CZECH.to_owned(),
1344                        "EN" => ENGLISH.to_owned(),
1345                        "FR" => FRENCH.to_owned(),
1346                        "DE" => GERMAN.to_owned(),
1347                        "IT" => ITALIAN.to_owned(),
1348                        "KR" => KOREAN.to_owned(),
1349                        "PO" => POLISH.to_owned(),
1350                        "RU" => RUSSIAN.to_owned(),
1351                        "ES" => SPANISH.to_owned(),
1352                        "TR" => TURKISH.to_owned(),
1353                        "ZH" => TRADITIONAL_CHINESE.to_owned(),
1354
1355                        // Default to english if we can't find the proper one.
1356                        _ => ENGLISH.to_owned(),
1357                    };
1358                    info!("Language file found, using {language} language.");
1359                    Ok(Some(language))
1360                } else {
1361                    warn!("Missing or unreadable language file under {}. Using english language.", game_path.to_string_lossy());
1362                    Ok(Some(ENGLISH.to_owned()))
1363                }
1364            }
1365            None => Ok(None),
1366        }
1367    }
1368
1369    /// Extracts the version number from the game's executable.
1370    ///
1371    /// Reads version information embedded in the game's executable file. Currently only
1372    /// implemented for Troy; returns `None` for other games.
1373    ///
1374    /// # Version Encoding
1375    ///
1376    /// The version is encoded as a 32-bit integer:
1377    /// - Bits 24-31: Major version
1378    /// - Bits 16-23: Minor version
1379    /// - Bits 8-15: Patch version
1380    /// - Bits 0-7: Build number
1381    ///
1382    /// For example, version 1.3.0.5 would be encoded as `0x01030005`.
1383    ///
1384    /// # Arguments
1385    ///
1386    /// * `game_path` - Absolute path to the game's installation directory
1387    ///
1388    /// # Returns
1389    ///
1390    /// The encoded version number, or `None` if:
1391    /// - Version extraction is not implemented for this game
1392    /// - The executable doesn't exist
1393    /// - The executable version info cannot be read
1394    pub fn game_version_number(&self, game_path: &Path) -> Option<u32> {
1395        match self.key() {
1396            KEY_TROY => {
1397                let exe_path = self.executable_path(game_path)?;
1398                if exe_path.is_file() {
1399                    let mut data = vec![];
1400                    let mut file = BufReader::new(File::open(exe_path).ok()?);
1401                    file.read_to_end(&mut data).ok()?;
1402
1403                    let version_info = pe_version_info(&data).ok()?;
1404                    let version_info = version_info.fixed()?;
1405                    let mut version: u32 = 0;
1406
1407                    // The CA format is limited so these can only be u8 when encoded, so we can safetly convert them.
1408                    let major = version_info.dwFileVersion.Major as u32;
1409                    let minor = version_info.dwFileVersion.Minor as u32;
1410                    let patch = version_info.dwFileVersion.Patch as u32;
1411                    let build = version_info.dwFileVersion.Build as u32;
1412
1413                    version += major << 24;
1414                    version += minor << 16;
1415                    version += patch << 8;
1416                    version += build;
1417                    Some(version)
1418                }
1419
1420                // If we have no exe, return a default value.
1421                else {
1422                    None
1423                }
1424
1425            }
1426
1427            _ => None,
1428        }
1429    }
1430
1431    /// Automatically discovers the game's installation directory.
1432    ///
1433    /// Searches for the game installation using platform-specific methods. Currently only
1434    /// supports Steam installations via the Steam library folders system.
1435    ///
1436    /// # Platform Support
1437    ///
1438    /// - **Windows Steam**: Searches via Steam library folders
1439    /// - **Linux Steam**: Searches via Steam library folders
1440    /// - **Other platforms**: Not supported (returns `Ok(None)`)
1441    ///
1442    /// # Arguments
1443    ///
1444    /// None - uses the game's configured Steam App ID
1445    ///
1446    /// # Returns
1447    ///
1448    /// - `Ok(Some(path))` - Game installation found at the returned path
1449    /// - `Ok(None)` - Game not found or platform not supported
1450    ///
1451    /// # Errors
1452    ///
1453    /// Returns an error if Steam library folder parsing fails.
1454    pub fn find_game_install_location(&self) -> Result<Option<PathBuf>> {
1455
1456        // Steam install data. We don't care if it's windows or linux, as the data we want is the same in both.
1457        let install_data = if let Some(install_data) = self.install_data.get(&InstallType::WinSteam) {
1458            install_data
1459        } else if let Some(install_data) = self.install_data.get(&InstallType::LnxSteam) {
1460            install_data
1461        } else {
1462            return Ok(None);
1463        };
1464
1465        if install_data.store_id() > &0 {
1466            if let Ok(steamdir) = SteamDir::locate() {
1467                return match steamdir.find_app(*install_data.store_id() as u32) {
1468                    Ok(Some((app, lib))) => {
1469                        let app_path = lib.resolve_app_dir(&app);
1470                        if app_path.is_dir() {
1471                            Ok(Some(app_path.to_path_buf()))
1472                        } else {
1473                            Ok(None)
1474                        }
1475                    }
1476                    _ => Ok(None)
1477                }
1478            }
1479        }
1480
1481        Ok(None)
1482    }
1483
1484    /// Automatically discovers the Assembly Kit installation directory.
1485    ///
1486    /// Assembly Kits are official modding tools distributed separately from the games.
1487    /// This method searches for the Assembly Kit using platform-specific methods,
1488    /// currently only supporting Steam installations.
1489    ///
1490    /// # Platform Support
1491    ///
1492    /// - **Windows Steam**: Searches via Steam library folders
1493    /// - **Linux Steam**: Searches via Steam library folders
1494    /// - **Other platforms**: Not supported (returns `Ok(None)`)
1495    ///
1496    /// # Arguments
1497    ///
1498    /// None - uses the game's configured Assembly Kit Steam App ID
1499    ///
1500    /// # Returns
1501    ///
1502    /// - `Ok(Some(path))` - Assembly Kit found at the returned path
1503    /// - `Ok(None)` - Assembly Kit not found, not available for this game, or platform not supported
1504    ///
1505    /// # Errors
1506    ///
1507    /// Returns an error if Steam library folder parsing fails.
1508    pub fn find_assembly_kit_install_location(&self) -> Result<Option<PathBuf>> {
1509
1510        // Steam install data. We don't care if it's windows or linux, as the data we want is the same in both.
1511        let install_data = if let Some(install_data) = self.install_data.get(&InstallType::WinSteam) {
1512            install_data
1513        } else if let Some(install_data) = self.install_data.get(&InstallType::LnxSteam) {
1514            install_data
1515        } else {
1516            return Ok(None);
1517        };
1518
1519        if install_data.store_id_ak() > &0 {
1520            if let Ok(steamdir) = SteamDir::locate() {
1521                return match steamdir.find_app(*install_data.store_id_ak() as u32) {
1522                    Ok(Some((app, lib))) => {
1523                        let app_path = lib.resolve_app_dir(&app);
1524                        if app_path.is_dir() {
1525                            Ok(Some(app_path.to_path_buf()))
1526                        } else {
1527                            Ok(None)
1528                        }
1529                    }
1530                    _ => Ok(None)
1531                }
1532            }
1533        }
1534
1535        Ok(None)
1536    }
1537
1538    /// Returns the list of Steam Workshop tags available for this game.
1539    ///
1540    /// Steam Workshop allows mod creators to tag their mods with categories like "graphical",
1541    /// "campaign", "units", etc. This method returns the official list of tags recognized
1542    /// by the Steam Workshop for this specific game.
1543    ///
1544    /// # Tag Categories
1545    ///
1546    /// Common tags across games include:
1547    /// - Content types: "graphical", "campaign", "units", "battle"
1548    /// - Scope: "overhaul", "ui", "maps"
1549    /// - Collections: "compilation", "mod manager"
1550    /// - Languages: "English", "Spanish", etc. (in some games)
1551    ///
1552    /// # Returns
1553    ///
1554    /// A vector of tag strings recognized by Steam Workshop for this game.
1555    ///
1556    /// # Errors
1557    ///
1558    /// Returns an error if the game doesn't support Steam Workshop.
1559    pub fn steam_workshop_tags(&self) -> Result<Vec<String>> {
1560        Ok(match self.key() {
1561            KEY_PHARAOH_DYNASTIES => vec![
1562                String::from("mod"),
1563                String::from("graphical"),
1564                String::from("campaign"),
1565                String::from("ui"),
1566                String::from("battle"),
1567                String::from("overhaul"),
1568                String::from("units"),
1569            ],
1570            KEY_PHARAOH => vec![
1571                String::from("mod"),
1572                String::from("graphical"),
1573                String::from("campaign"),
1574                String::from("ui"),
1575                String::from("battle"),
1576                String::from("overhaul"),
1577                String::from("units"),
1578            ],
1579            KEY_WARHAMMER_3 => vec![
1580                String::from("graphical"),
1581                String::from("campaign"),
1582                String::from("units"),
1583                String::from("battle"),
1584                String::from("ui"),
1585                String::from("maps"),
1586                String::from("overhaul"),
1587                String::from("compilation"),
1588                String::from("cheat"),
1589            ],
1590            KEY_TROY => vec![
1591                String::from("mod"),
1592                String::from("ui"),
1593                String::from("graphical"),
1594                String::from("units"),
1595                String::from("battle"),
1596                String::from("campaign"),
1597                String::from("overhaul"),
1598                String::from("compilation"),
1599            ],
1600            KEY_THREE_KINGDOMS => vec![
1601                String::from("mod"),
1602                String::from("graphical"),
1603                String::from("overhaul"),
1604                String::from("ui"),
1605                String::from("battle"),
1606                String::from("campaign"),
1607                String::from("maps"),
1608                String::from("units"),
1609                String::from("compilation"),
1610            ],
1611            KEY_WARHAMMER_2 => vec![
1612                String::from("mod"),
1613                String::from("Units"),
1614                String::from("Battle"),
1615                String::from("Graphical"),
1616                String::from("UI"),
1617                String::from("Campaign"),
1618                String::from("Maps"),
1619                String::from("Overhaul"),
1620                String::from("Compilation"),
1621                String::from("Mod Manager"),
1622                String::from("Skills"),
1623                String::from("map"),
1624            ],
1625            KEY_WARHAMMER => vec![
1626                String::from("mod"),
1627                String::from("UI"),
1628                String::from("Graphical"),
1629                String::from("Overhaul"),
1630                String::from("Battle"),
1631                String::from("Campaign"),
1632                String::from("Compilation"),
1633                String::from("Units"),
1634                String::from("Maps"),
1635                String::from("Spanish"),
1636                String::from("English"),
1637                String::from("undefined"),
1638                String::from("map"),
1639            ],
1640            KEY_THRONES_OF_BRITANNIA => vec![
1641                String::from("mod"),
1642                String::from("ui"),
1643                String::from("battle"),
1644                String::from("campaign"),
1645                String::from("units"),
1646                String::from("compilation"),
1647                String::from("graphical"),
1648                String::from("overhaul"),
1649                String::from("maps"),
1650            ],
1651            KEY_ATTILA => vec![
1652                String::from("mod"),
1653                String::from("UI"),
1654                String::from("Graphical"),
1655                String::from("Battle"),
1656                String::from("Campaign"),
1657                String::from("Units"),
1658                String::from("Overhaul"),
1659                String::from("Compilation"),
1660                String::from("Maps"),
1661                String::from("version_2"),
1662                String::from("Czech"),
1663                String::from("Danish"),
1664                String::from("English"),
1665                String::from("Finnish"),
1666                String::from("French"),
1667                String::from("German"),
1668                String::from("Hungarian"),
1669                String::from("Italian"),
1670                String::from("Japanese"),
1671                String::from("Korean"),
1672                String::from("Norwegian"),
1673                String::from("Romanian"),
1674                String::from("Russian"),
1675                String::from("Spanish"),
1676                String::from("Swedish"),
1677                String::from("Thai"),
1678                String::from("Turkish"),
1679            ],
1680            KEY_ROME_2 => vec![
1681                String::from("mod"),
1682                String::from("Units"),
1683                String::from("Battle"),
1684                String::from("Overhaul"),
1685                String::from("Compilation"),
1686                String::from("Campaign"),
1687                String::from("Graphical"),
1688                String::from("UI"),
1689                String::from("Maps"),
1690                String::from("version_2"),
1691                String::from("English"),
1692                String::from("gribble"),
1693                String::from("tribble"),
1694            ],
1695            KEY_SHOGUN_2 => vec![
1696                String::from("map"),
1697                String::from("historical"),
1698                String::from("multiplayer"),
1699                String::from("mod"),
1700                String::from("version_2"),
1701                String::from("English"),
1702                String::from("ui"),
1703                String::from("graphical"),
1704                String::from("overhaul"),
1705                String::from("units"),
1706                String::from("campaign"),
1707                String::from("battle"),
1708            ],
1709            _ => return Err(RLibError::GameDoesntSupportWorkshop(self.key().to_owned()))
1710        })
1711    }
1712
1713    /// Looks up a game by its Steam App ID.
1714    ///
1715    /// Given a Steam App ID, searches through all supported games to find the matching game.
1716    /// This is useful when you have a Steam App ID (e.g., from Steam library or launch parameters)
1717    /// and need to identify which Total War game it corresponds to.
1718    ///
1719    /// # Arguments
1720    ///
1721    /// * `steam_id` - The Steam App ID to search for
1722    ///
1723    /// # Returns
1724    ///
1725    /// The [`GameInfo`] for the matching game.
1726    ///
1727    /// # Errors
1728    ///
1729    /// Returns an error if no known game matches the provided Steam App ID.
1730    ///
1731    /// # Example
1732    ///
1733    /// ```
1734    /// use rpfm_lib::games::GameInfo;
1735    ///
1736    /// // Look up Warhammer 3 by its Steam App ID
1737    /// let game_info = GameInfo::game_by_steam_id(1142710).unwrap();
1738    /// assert_eq!(game_info.key(), "warhammer_3");
1739    /// ```
1740    pub fn game_by_steam_id(steam_id: u64) -> Result<Self> {
1741        let games = SupportedGames::default();
1742        for game in games.games() {
1743
1744            // No need to check LnxSteam, as they share the same id.
1745            match game.install_data.get(&InstallType::WinSteam) {
1746                Some(install_data) => if install_data.store_id == steam_id {
1747                    return Ok(game.clone());
1748                } else {
1749                    continue;
1750                }
1751                None => continue,
1752            }
1753        }
1754
1755        Err(RLibError::SteamIDDoesntBelongToKnownGame(steam_id))
1756    }
1757}