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