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}