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