rpfm_lib/games/
mod.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2024 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//! Module that contains the GameInfo definition and stuff related with it.
12
13use directories::ProjectDirs;
14use getset::*;
15#[cfg(feature = "integration_log")] use log::{info, warn};
16use steamlocate::SteamDir;
17
18use std::collections::HashMap;
19use std::{fmt, fmt::Display};
20use std::fs::{DirBuilder, File};
21use std::io::{BufReader, Read};
22use std::path::{Path, PathBuf};
23use std::sync::{Arc, RwLock};
24
25use crate::compression::CompressionFormat;
26use crate::error::{RLibError, Result};
27use crate::utils::*;
28
29use self::supported_games::*;
30use self::manifest::Manifest;
31use self::pfh_file_type::PFHFileType;
32use self::pfh_version::PFHVersion;
33
34pub mod supported_games;
35pub mod manifest;
36pub mod pfh_file_type;
37pub mod pfh_version;
38
39pub const BRAZILIAN: &str = "br";
40pub const SIMPLIFIED_CHINESE: &str = "cn";
41pub const CZECH: &str = "cz";
42pub const ENGLISH: &str = "en";
43pub const FRENCH: &str = "fr";
44pub const GERMAN: &str = "ge";
45pub const ITALIAN: &str = "it";
46pub const KOREAN: &str = "kr";
47pub const POLISH: &str = "pl";
48pub const RUSSIAN: &str = "ru";
49pub const SPANISH: &str = "sp";
50pub const TURKISH: &str = "tr";
51pub const TRADITIONAL_CHINESE: &str = "zh";
52
53pub const LUA_AUTOGEN_FOLDER: &str = "tw_autogen";
54pub const LUA_REPO: &str = "https://github.com/chadvandy/tw_autogen";
55pub const LUA_REMOTE: &str = "origin";
56pub const LUA_BRANCH: &str = "main";
57
58pub const OLD_AK_REPO: &str = "https://github.com/Frodo45127/total_war_ak_files_pre_shogun_2";
59pub const OLD_AK_REMOTE: &str = "origin";
60pub const OLD_AK_BRANCH: &str = "master";
61
62pub const TRANSLATIONS_REPO: &str = "https://github.com/Frodo45127/total_war_translation_hub";
63pub const TRANSLATIONS_REMOTE: &str = "origin";
64pub const TRANSLATIONS_BRANCH: &str = "master";
65
66//-------------------------------------------------------------------------------//
67//                              Enums & Structs
68//-------------------------------------------------------------------------------//
69
70/// This struct holds all the info needed for a game to be "supported" by RPFM.
71#[derive(Getters, Clone, Debug)]
72#[getset(get = "pub")]
73pub struct GameInfo {
74
75    /// This is the internal key of the game.
76    #[getset(skip)]
77    key: &'static str,
78
79    /// This is the name it'll show up for the user. The *pretty name*. For example, in a dropdown (Warhammer 2).
80    display_name: &'static str,
81
82    /// This is the PFHVersion used at the start of every PackFile for that game.
83    /// It's in a hashmap of PFHFileType => PFHVersion, so we can have different PackFile versions depending on their type.
84    pfh_versions: HashMap<PFHFileType, PFHVersion>,
85
86    /// This is the full name of the schema file used for the game. For example: `schema_wh2.ron`.
87    schema_file_name: String,
88
89    /// This is the name of the file containing the dependencies cache for this game.
90    dependencies_cache_file_name: String,
91
92    /// This is the **type** of raw files the game uses. -1 is "Don't have Assembly Kit". 0 is Empire/Nappy. 1 is Shogun 2. 2 is anything newer than Shogun 2.
93    raw_db_version: i16,
94
95    /// This is the version used when generating PortraitSettings files for each game.
96    portrait_settings_version: Option<u32>,
97
98    /// If we can save `PackFile` files for the game.
99    supports_editing: bool,
100
101    /// If the db tables should have a GUID in their headers.
102    db_tables_have_guid: bool,
103
104    /// If the game has locales for all languages, and we only need to load our own locales. Contains the name of the locale file.
105    locale_file_name: Option<String>,
106
107    /// List of tables (table_name) which the program should NOT EDIT UNDER ANY CIRCUnSTANCE.
108    banned_packedfiles: Vec<String>,
109
110    /// Name of the icon used to display the game as `Game Selected`, in an UI.
111    icon_small: String,
112
113    /// Name of the big icon used to display the game as `Game Selected`, in an UI.
114    icon_big: String,
115
116    /// Logic used to name vanilla tables.
117    vanilla_db_table_name_logic: VanillaDBTableNameLogic,
118
119    /// Installation-dependant data.
120    #[getset(skip)]
121    install_data: HashMap<InstallType, InstallData>,
122
123    /// Tool-specific vars for each game.
124    tool_vars: HashMap<String, String>,
125
126    /// Subfolder under Lua Autogen's folder where the files for this game are, if it's supported.
127    lua_autogen_folder: Option<String>,
128
129    /// Table/fields ignored on the assembly kit integration for this game. These are fields that are "lost" when exporting the tables from Dave.
130    ak_lost_fields: Vec<String>,
131
132    /// Internal cache to speedup operations related with the install type.
133    #[getset(skip)]
134    install_type_cache: Arc<RwLock<HashMap<PathBuf, InstallType>>>,
135
136    /// List of compression formats supported by the game, sorted from newer to older.
137    compression_formats_supported: Vec<CompressionFormat>
138}
139
140/// This enum holds the info about each game approach at naming db tables.
141#[derive(Clone, Debug)]
142pub enum VanillaDBTableNameLogic {
143
144    /// This variant is for games where the table name is their folder's name.
145    FolderName,
146
147    /// This variant is for games where all tables are called the same.
148    DefaultName(String),
149}
150
151/// This enum represents the different installations of games the game support.
152#[derive(Clone, Debug, Hash, PartialEq, Eq)]
153pub enum InstallType {
154
155    /// Windows - Steam variant.
156    WinSteam,
157
158    /// Linux - Steam variant.
159    LnxSteam,
160
161    /// Windows - Epic Store Variant.
162    WinEpic,
163
164    /// Windows - Wargaming Variant.
165    WinWargaming,
166}
167
168/// This struct contains installation-dependant data about each game.
169///
170/// NOTE: All PackFile paths contained in this struct are RELATIVE, either to the data folder, or to the game's folder.
171#[derive(Getters, Clone, Debug)]
172#[getset(get = "pub")]
173pub struct InstallData {
174
175    /// List of vanilla packs, to be use as reference for knowing what PackFiles are vanilla in games without a manifest file.
176    /// Currently only used for Empire and Napoleon. Relative to data_path.
177    vanilla_packs: Vec<String>,
178
179    /// If the manifest of the game should be used to get the vanilla PackFile list, or should we use the hardcoded list.
180    use_manifest: bool,
181
182    /// StoreID of the game.
183    store_id: u64,
184
185    /// StoreID of the AK.
186    store_id_ak: u64,
187
188    /// Name of the executable of the game, including extension if it has it.
189    executable: String,
190
191    /// /data path of the game, or equivalent. Relative to the game's path.
192    data_path: String,
193
194    /// Path where the language.txt file of the game is expected to be. Usually /data, but it's different on linux builds. Relative to the game's path.
195    language_path: String,
196
197    /// Folder where local (your own) mods are stored. Relative to the game's path.
198    local_mods_path: String,
199
200    /// Folder where downloaded (other peoples's) mods are stored. Relative to the game's path.
201    downloaded_mods_path: String,
202
203    /// Name of the folder where the config for this specific game installation are stored.
204    config_folder: Option<String>,
205}
206
207//-------------------------------------------------------------------------------//
208//                             Implementations
209//-------------------------------------------------------------------------------//
210
211impl Display for InstallType {
212    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
213        Display::fmt(match self {
214            Self::WinSteam => "Windows - Steam",
215            Self::LnxSteam => "Linux - Steam",
216            Self::WinEpic => "Windows - Epic",
217            Self::WinWargaming => "Windows - Wargaming",
218        }, f)
219    }
220}
221
222/// Implementation of GameInfo.
223impl GameInfo {
224
225    //---------------------------------------------------------------------------//
226    // Getters.
227    //---------------------------------------------------------------------------//
228
229    /// This function returns the "Key" name of the Game, meaning in lowercase and without spaces.
230    pub fn key(&self) -> &str {
231        self.key
232    }
233
234    /// This function returns the PFHVersion corresponding to the provided PackFile type. If it's not found, it defaults to the one used by mods.
235    pub fn pfh_version_by_file_type(&self, pfh_file_type: PFHFileType) -> PFHVersion {
236        match self.pfh_versions.get(&pfh_file_type) {
237            Some(pfh_version) => *pfh_version,
238            None => *self.pfh_versions.get(&PFHFileType::Mod).unwrap(),
239        }
240    }
241
242    //---------------------------------------------------------------------------//
243    // Advanced getters.
244    //---------------------------------------------------------------------------//
245
246    /// This function tries to get the correct InstallType for the currently configured installation of the game.
247    pub fn install_type(&self, game_path: &Path) -> Result<InstallType> {
248
249        // This function takes 10ms to execute. In a few places, it's executed 2-5 times, and quickly adds up.
250        // So before executing it, check the cache to see if it has been executed before.
251        if let Some(install_type) = self.install_type_cache.read().unwrap().get(game_path) {
252            return Ok(install_type.clone());
253        }
254
255        // Checks to guess what kind of installation we have.
256        let base_path_files = files_from_subdir(game_path, false)?;
257        let install_type_by_exe = self.install_data.iter().filter_map(|(install_type, install_data)|
258            if base_path_files.iter().filter_map(|path| if path.is_file() { path.file_name() } else { None }).any(|filename| filename == &**install_data.executable()) {
259                Some(install_type)
260            } else { None }
261        ).collect::<Vec<&InstallType>>();
262
263        // If no compatible install data was found, use the first one we have.
264        if install_type_by_exe.is_empty() {
265            let install_type = self.install_data.keys().next().unwrap();
266            self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), install_type.clone());
267            Ok(install_type.clone())
268        }
269
270        // If we only have one install type compatible with the executable we have, return it.
271        else if install_type_by_exe.len() == 1 {
272            self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), install_type_by_exe[0].clone());
273            Ok(install_type_by_exe[0].clone())
274        }
275
276        // If we have multiple install data compatible, it gets more complex.
277        else {
278
279            // First, identify if we have a windows or linux build (mac only exists in your dreams.....).
280            // Can't be both because they have different exe names. Unless you're retarded and you merge both, in which case, fuck you.
281            let is_windows = install_type_by_exe.iter().any(|install_type| install_type == &&InstallType::WinSteam || install_type == &&InstallType::WinEpic || install_type == &&InstallType::WinWargaming);
282            if is_windows {
283
284                // Steam versions of the game have a "steam_api.dll" or "steam_api64.dll" file. Epic has "EOSSDK-Win64-Shipping.dll".
285                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");
286                let has_eos_sdk_dll = base_path_files.iter().filter_map(|path| path.file_name()).any(|filename| filename == "EOSSDK-Win64-Shipping.dll");
287                if has_steam_api_dll && install_type_by_exe.contains(&&InstallType::WinSteam) {
288                    self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), InstallType::WinSteam);
289                    Ok(InstallType::WinSteam)
290                }
291
292                // If not, check wether we have epic libs.
293                else if has_eos_sdk_dll && install_type_by_exe.contains(&&InstallType::WinEpic) {
294                    self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), InstallType::WinEpic);
295                    Ok(InstallType::WinEpic)
296                }
297
298                // If neither of those are true, assume it's wargaming/netease (arena?).
299                else {
300                    self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), InstallType::WinWargaming);
301                    Ok(InstallType::WinWargaming)
302                }
303            }
304
305            // Otherwise, assume it's linux
306            else {
307                self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), InstallType::LnxSteam);
308                Ok(InstallType::LnxSteam)
309            }
310        }
311    }
312
313    /// This function gets the install data for the game, if it's a supported installation.
314    pub fn install_data(&self, game_path: &Path) -> Result<&InstallData> {
315        let install_type = self.install_type(game_path)?;
316        let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
317        Ok(install_data)
318    }
319
320    /// This function gets the `/data` path or equivalent of the game selected, if said game it's configured in the settings.
321    pub fn data_path(&self, game_path: &Path) -> Result<PathBuf> {
322        let install_type = self.install_type(game_path)?;
323        let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
324        Ok(game_path.join(install_data.data_path()))
325    }
326
327    /// This function gets the `/contents` path or equivalent of the game selected, if said game it's configured in the settings.
328    pub fn content_path(&self, game_path: &Path) -> Result<PathBuf> {
329        let install_type = self.install_type(game_path)?;
330        let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
331        Ok(game_path.join(install_data.downloaded_mods_path()))
332    }
333
334    /// This function gets the `language.txt` path of the game selected, if said game uses it and it's configured in the settings.
335    pub fn language_path(&self, game_path: &Path) -> Result<PathBuf> {
336
337        // For games that don't support
338        let language_file_name = self.locale_file_name().clone().unwrap_or_else(|| "language.txt".to_owned());
339
340        let install_type = self.install_type(game_path)?;
341        let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
342        let base_path = game_path.join(install_data.language_path());
343
344        // The language files are either in this folder, or in a folder with the locale value inside this folder.
345        let path_with_file = base_path.join(language_file_name);
346        if path_with_file.is_file() {
347            Ok(base_path)
348        } else {
349
350            // 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.
351            let path = base_path.join(BRAZILIAN);
352            if path.is_dir() {
353                return Ok(path);
354            }
355            let path = base_path.join(SIMPLIFIED_CHINESE);
356            if path.is_dir() {
357                return Ok(path);
358            }
359            let path = base_path.join(CZECH);
360            if path.is_dir() {
361                return Ok(path);
362            }
363            let path = base_path.join(ENGLISH);
364            if path.is_dir() {
365                return Ok(path);
366            }
367            let path = base_path.join(FRENCH);
368            if path.is_dir() {
369                return Ok(path);
370            }
371            let path = base_path.join(GERMAN);
372            if path.is_dir() {
373                return Ok(path);
374            }
375            let path = base_path.join(ITALIAN);
376            if path.is_dir() {
377                return Ok(path);
378            }
379            let path = base_path.join(KOREAN);
380            if path.is_dir() {
381                return Ok(path);
382            }
383            let path = base_path.join(POLISH);
384            if path.is_dir() {
385                return Ok(path);
386            }
387            let path = base_path.join(RUSSIAN);
388            if path.is_dir() {
389                return Ok(path);
390            }
391            let path = base_path.join(SPANISH);
392            if path.is_dir() {
393                return Ok(path);
394            }
395            let path = base_path.join(TURKISH);
396            if path.is_dir() {
397                return Ok(path);
398            }
399            let path = base_path.join(TRADITIONAL_CHINESE);
400            if path.is_dir() {
401                return Ok(path);
402            }
403
404            // If no path exists, we just return the base path.
405            Ok(base_path)
406        }
407    }
408
409    /// This function gets the `/data` path or equivalent (the folder local mods are installed during development) of the game selected, if said game it's configured in the settings
410    pub fn local_mods_path(&self, game_path: &Path) -> Result<PathBuf> {
411        let install_type = self.install_type(game_path)?;
412        let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
413        Ok(game_path.join(install_data.local_mods_path()))
414    }
415
416    /// This function gets the `/mods` path or equivalent of the game selected, if said game it's configured in the settings.
417    pub fn content_packs_paths(&self, game_path: &Path) -> Option<Vec<PathBuf>> {
418        let install_type = self.install_type(game_path).ok()?;
419        let install_data = self.install_data.get(&install_type)?;
420        let downloaded_mods_path = install_data.downloaded_mods_path();
421
422        // If the path is empty, it means this game does not support downloaded mods.
423        if downloaded_mods_path.is_empty() {
424            return None;
425        }
426
427        let path = std::fs::canonicalize(game_path.join(downloaded_mods_path)).ok()?;
428        let mut paths = vec![];
429
430        for path in files_from_subdir(&path, true).ok()?.iter() {
431            match path.extension() {
432                Some(extension) => if extension == "pack" || extension == "bin" { paths.push(path.to_path_buf()); }
433                None => continue,
434            }
435        }
436
437        paths.sort();
438        Some(paths)
439    }
440
441    /// This function gets the paths of the Packs from the `/secondary` path or equivalent of the game selected, if it's configured in the settings.
442    ///
443    /// Secondary path must be absolute.
444    pub fn secondary_packs_paths(&self, secondary_path: &Path) -> Option<Vec<PathBuf>> {
445        if !secondary_path.is_dir() || !secondary_path.exists() || !secondary_path.is_absolute() {
446            return None;
447        }
448
449        let game_path = secondary_path.join(self.key());
450        if !game_path.is_dir() || !game_path.exists() {
451            return None;
452        }
453
454        let mut paths = vec![];
455
456        for path in files_from_subdir(&game_path, false).ok()?.iter() {
457            match path.extension() {
458                Some(extension) => if extension == "pack" {
459                    paths.push(path.to_path_buf());
460                }
461                None => continue,
462            }
463        }
464
465        paths.sort();
466        Some(paths)
467    }
468
469    /// This function gets the `/data` path or equivalent of the game selected, if said game it's configured in the settings.
470    pub fn data_packs_paths(&self, game_path: &Path) -> Option<Vec<PathBuf>> {
471        let game_path = self.data_path(game_path).ok()?;
472        let mut paths = vec![];
473
474        for path in files_from_subdir(&game_path, false).ok()?.iter() {
475            match path.extension() {
476                Some(extension) => if extension == "pack" { paths.push(path.to_path_buf()); }
477                None => continue,
478            }
479        }
480
481        paths.sort();
482        Some(paths)
483    }
484
485
486    /// This function gets the destination folder for MyMod packs.
487    pub fn mymod_install_path(&self, game_path: &Path) -> Option<PathBuf> {
488        let install_type = self.install_type(game_path).ok()?;
489        let install_data = self.install_data.get(&install_type)?;
490        let path = game_path.join(PathBuf::from(install_data.local_mods_path()));
491
492        // Make sure the folder exists.
493        DirBuilder::new().recursive(true).create(&path).ok()?;
494
495        Some(path)
496    }
497
498    /// This function returns if we should use the manifest of the game (if found) to get the vanilla PackFiles, or if we should get them from out hardcoded list.
499    pub fn use_manifest(&self, game_path: &Path) -> Result<bool> {
500        let install_type = self.install_type(game_path)?;
501        let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
502
503        // If the install_type is linux, or we actually have a hardcoded list, ignore all Manifests.
504        Ok(*install_data.use_manifest())
505    }
506
507    /// This function returns the steam id for a specific game installation.
508    pub fn steam_id(&self, game_path: &Path) -> Result<u64> {
509        let install_type = self.install_type(game_path)?;
510        let install_data = match install_type {
511            InstallType::WinSteam |
512            InstallType::LnxSteam => self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?,
513            _ => return Err(RLibError::ReservedFiles)
514        };
515
516        Ok(*install_data.store_id())
517    }
518
519    /// This function is used to get the paths of all CA PackFiles on the data folder of the game selected.
520    ///
521    /// If it fails to find a manifest, it falls back to all non-mod files!
522    ///
523    /// NOTE: For WH3, this is language-sensitive. Meaning, if you have the game on spanish, it'll try to load the spanish localization files ONLY.
524    pub fn ca_packs_paths(&self, game_path: &Path) -> Result<Vec<PathBuf>> {
525
526        // Check if we have to filter by language, to avoid overwriting our language with another one.
527        let language = self.game_locale_from_file(game_path)?;
528
529        // Check if we can use the manifest for this.
530        if !self.use_manifest(game_path)? {
531            self.ca_packs_paths_no_manifest(game_path, &language)
532        } else {
533
534            // Try to get the manifest, if exists.
535            match Manifest::read_from_game_path(self, game_path) {
536                Ok(manifest) => {
537                    let data_path = self.data_path(game_path)?;
538                    let mut paths = manifest.0.iter().filter_map(|entry|
539                        if entry.relative_path().ends_with(".pack") {
540
541                            let mut pack_file_path = data_path.to_path_buf();
542                            pack_file_path.push(entry.relative_path());
543                            match &language {
544                                Some(language) => {
545
546                                    // Filter out other language's packfiles.
547                                    if entry.relative_path().contains("local_") {
548                                        let language = "local_".to_owned() + language;
549                                        if entry.relative_path().contains(&language) {
550                                            entry.path_from_manifest_entry(pack_file_path)
551                                        } else {
552                                            None
553                                        }
554                                    } else {
555                                        entry.path_from_manifest_entry(pack_file_path)
556                                    }
557                                }
558                                None => entry.path_from_manifest_entry(pack_file_path),
559                            }
560                        } else { None }
561                        ).collect::<Vec<PathBuf>>();
562
563                    paths.sort();
564                    Ok(paths)
565                }
566
567                // If there is no manifest, use the hardcoded file list for the game, if it has one.
568                Err(_) => self.ca_packs_paths_no_manifest(game_path, &language)
569            }
570        }
571    }
572
573    /// This function tries to get the ca PackFiles without depending on a Manifest. For internal use only.
574    fn ca_packs_paths_no_manifest(&self, game_path: &Path, language: &Option<String>) -> Result<Vec<PathBuf>> {
575        let data_path = self.data_path(game_path)?;
576        let install_type = self.install_type(game_path)?;
577        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;
578        let language_pack = language.clone().map(|lang| format!("local_{lang}"));
579        if !vanilla_packs.is_empty() {
580            Ok(vanilla_packs.iter().filter_map(|pack_name| {
581
582                let mut pack_file_path = data_path.to_path_buf();
583                pack_file_path.push(pack_name);
584                match language_pack {
585                    Some(ref language_pack) => {
586
587                        // Filter out other language's packfiles.
588                        if !pack_name.is_empty() && pack_name.starts_with("local_") {
589                            if pack_name.starts_with(language_pack) {
590                                std::fs::canonicalize(pack_file_path).ok()
591                            } else {
592                                None
593                            }
594                        } else {
595                            std::fs::canonicalize(pack_file_path).ok()
596                        }
597                    }
598                    None => std::fs::canonicalize(pack_file_path).ok(),
599                }
600            }).collect::<Vec<PathBuf>>())
601        }
602
603        // If there is no hardcoded list, get every path.
604        else {
605            Ok(files_from_subdir(&data_path, false)?.iter()
606                .filter_map(|x| if let Some(extension) = x.extension() {
607                    if extension.to_string_lossy().to_lowercase() == "pack" {
608                        Some(x.to_owned())
609                    } else { None }
610                } else { None }).collect::<Vec<PathBuf>>()
611            )
612        }
613    }
614
615    /// This command returns the "launch" command for executing this game's installation.
616    pub fn game_launch_command(&self, game_path: &Path) -> Result<String> {
617        let install_type = self.install_type(game_path)?;
618
619        match install_type {
620            InstallType::LnxSteam |
621            InstallType::WinSteam => {
622                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();
623                Ok(format!("steam://rungameid/{store_id}"))
624            },
625            _ => Err(RLibError::GameInstallLaunchNotSupported(self.display_name.to_string(), install_type.to_string())),
626        }
627    }
628
629    /// This command returns the "Executable" path for the game's installation.
630    pub fn executable_path(&self, game_path: &Path) -> Option<PathBuf> {
631        let install_type = self.install_type(game_path).ok()?;
632        let install_data = self.install_data.get(&install_type)?;
633        let executable_path = game_path.join(install_data.executable());
634
635        Some(executable_path)
636    }
637
638    /// This command returns the "config" path for the game's installation.
639    pub fn config_path(&self, game_path: &Path) -> Option<PathBuf> {
640        let install_type = self.install_type(game_path).ok()?;
641        let install_data = self.install_data.get(&install_type)?;
642        let config_folder = install_data.config_folder.as_ref()?;
643
644        ProjectDirs::from("com", "The Creative Assembly", config_folder).map(|dir| {
645            let mut dir = dir.config_dir().to_path_buf();
646            dir.pop();
647            dir
648        })
649    }
650
651    /// Check if a specific file is banned.
652    pub fn is_file_banned(&self, path: &str) -> bool {
653        let path = path.to_lowercase();
654        self.banned_packedfiles.iter().any(|x| path.starts_with(x))
655    }
656
657    /// Tries to retrieve a tool var for the game.
658    pub fn tool_var(&self, var: &str) -> Option<&String> {
659        self.tool_vars.get(var)
660    }
661
662    /// This function tries to get the language of the game. Defaults to english if not found.
663    pub fn game_locale_from_file(&self, game_path: &Path) -> Result<Option<String>> {
664        match self.locale_file_name() {
665            Some(locale_file) => {
666                let language_path = self.language_path(game_path)?;
667                let locale_path = language_path.join(locale_file);
668                let mut language = String::new();
669                if let Ok(mut file) = File::open(locale_path) {
670                    file.read_to_string(&mut language)?;
671
672                    let language = match &*language {
673                        "BR" => BRAZILIAN.to_owned(),
674                        "CN" => SIMPLIFIED_CHINESE.to_owned(),
675                        "CZ" => CZECH.to_owned(),
676                        "EN" => ENGLISH.to_owned(),
677                        "FR" => FRENCH.to_owned(),
678                        "DE" => GERMAN.to_owned(),
679                        "IT" => ITALIAN.to_owned(),
680                        "KR" => KOREAN.to_owned(),
681                        "PO" => POLISH.to_owned(),
682                        "RU" => RUSSIAN.to_owned(),
683                        "ES" => SPANISH.to_owned(),
684                        "TR" => TURKISH.to_owned(),
685                        "ZH" => TRADITIONAL_CHINESE.to_owned(),
686
687                        // Default to english if we can't find the proper one.
688                        _ => ENGLISH.to_owned(),
689                    };
690
691                    #[cfg(feature = "integration_log")] {
692                        info!("Language file found, using {language} language.");
693                    }
694
695                    Ok(Some(language))
696                } else {
697                    #[cfg(feature = "integration_log")] {
698                        warn!("Missing or unreadable language file under {}. Using english language.", game_path.to_string_lossy());
699                    }
700                    Ok(Some(ENGLISH.to_owned()))
701                }
702            }
703            None => Ok(None),
704        }
705    }
706
707    /// This function gets the version number of the exe for the current GameSelected, if it exists.
708    pub fn game_version_number(&self, game_path: &Path) -> Option<u32> {
709        match self.key() {
710            KEY_TROY => {
711                let exe_path = self.executable_path(game_path)?;
712                if exe_path.is_file() {
713                    let mut data = vec![];
714                    let mut file = BufReader::new(File::open(exe_path).ok()?);
715                    file.read_to_end(&mut data).ok()?;
716
717                    let version_info = pe_version_info(&data).ok()?;
718                    let version_info = version_info.fixed()?;
719                    let mut version: u32 = 0;
720
721                    // The CA format is limited so these can only be u8 when encoded, so we can safetly convert them.
722                    let major = version_info.dwFileVersion.Major as u32;
723                    let minor = version_info.dwFileVersion.Minor as u32;
724                    let patch = version_info.dwFileVersion.Patch as u32;
725                    let build = version_info.dwFileVersion.Build as u32;
726
727                    version += major << 24;
728                    version += minor << 16;
729                    version += patch << 8;
730                    version += build;
731                    Some(version)
732                }
733
734                // If we have no exe, return a default value.
735                else {
736                    None
737                }
738
739            }
740
741            _ => None,
742        }
743    }
744
745    /// This function searches for installed total war games.
746    ///
747    /// NOTE: Only works for steam-installed games.
748    pub fn find_game_install_location(&self) -> Result<Option<PathBuf>> {
749
750        // Steam install data. We don't care if it's windows or linux, as the data we want is the same in both.
751        let install_data = if let Some(install_data) = self.install_data.get(&InstallType::WinSteam) {
752            install_data
753        } else if let Some(install_data) = self.install_data.get(&InstallType::LnxSteam) {
754            install_data
755        } else {
756            return Ok(None);
757        };
758
759        if install_data.store_id() > &0 {
760            if let Ok(steamdir) = SteamDir::locate() {
761                return match steamdir.find_app(*install_data.store_id() as u32) {
762                    Ok(Some((app, lib))) => {
763                        let app_path = lib.resolve_app_dir(&app);
764                        if app_path.is_dir() {
765                            Ok(Some(app_path.to_path_buf()))
766                        } else {
767                            Ok(None)
768                        }
769                    }
770                    _ => Ok(None)
771                }
772            }
773        }
774
775        Ok(None)
776    }
777
778    /// This function searches for installed total war Assembly Kits.
779    ///
780    /// NOTE: Only works for steam-installed games.
781    pub fn find_assembly_kit_install_location(&self) -> Result<Option<PathBuf>> {
782
783        // Steam install data. We don't care if it's windows or linux, as the data we want is the same in both.
784        let install_data = if let Some(install_data) = self.install_data.get(&InstallType::WinSteam) {
785            install_data
786        } else if let Some(install_data) = self.install_data.get(&InstallType::LnxSteam) {
787            install_data
788        } else {
789            return Ok(None);
790        };
791
792        if install_data.store_id_ak() > &0 {
793            if let Ok(steamdir) = SteamDir::locate() {
794                return match steamdir.find_app(*install_data.store_id_ak() as u32) {
795                    Ok(Some((app, lib))) => {
796                        let app_path = lib.resolve_app_dir(&app);
797                        if app_path.is_dir() {
798                            Ok(Some(app_path.to_path_buf()))
799                        } else {
800                            Ok(None)
801                        }
802                    }
803                    _ => Ok(None)
804                }
805            }
806        }
807
808        Ok(None)
809    }
810
811    /// This function returns the list of public tags available in the workshop for each game.
812    pub fn steam_workshop_tags(&self) -> Result<Vec<String>> {
813        Ok(match self.key() {
814            KEY_PHARAOH_DYNASTIES => vec![
815                String::from("mod"),
816                String::from("graphical"),
817                String::from("campaign"),
818                String::from("ui"),
819                String::from("battle"),
820                String::from("overhaul"),
821                String::from("units"),
822            ],
823            KEY_PHARAOH => vec![
824                String::from("mod"),
825                String::from("graphical"),
826                String::from("campaign"),
827                String::from("ui"),
828                String::from("battle"),
829                String::from("overhaul"),
830                String::from("units"),
831            ],
832            KEY_WARHAMMER_3 => vec![
833                String::from("graphical"),
834                String::from("campaign"),
835                String::from("units"),
836                String::from("battle"),
837                String::from("ui"),
838                String::from("maps"),
839                String::from("overhaul"),
840                String::from("compilation"),
841                String::from("cheat"),
842            ],
843            KEY_TROY => vec![
844                String::from("mod"),
845                String::from("ui"),
846                String::from("graphical"),
847                String::from("units"),
848                String::from("battle"),
849                String::from("campaign"),
850                String::from("overhaul"),
851                String::from("compilation"),
852            ],
853            KEY_THREE_KINGDOMS => vec![
854                String::from("mod"),
855                String::from("graphical"),
856                String::from("overhaul"),
857                String::from("ui"),
858                String::from("battle"),
859                String::from("campaign"),
860                String::from("maps"),
861                String::from("units"),
862                String::from("compilation"),
863            ],
864            KEY_WARHAMMER_2 => vec![
865                String::from("mod"),
866                String::from("Units"),
867                String::from("Battle"),
868                String::from("Graphical"),
869                String::from("UI"),
870                String::from("Campaign"),
871                String::from("Maps"),
872                String::from("Overhaul"),
873                String::from("Compilation"),
874                String::from("Mod Manager"),
875                String::from("Skills"),
876                String::from("map"),
877            ],
878            KEY_WARHAMMER => vec![
879                String::from("mod"),
880                String::from("UI"),
881                String::from("Graphical"),
882                String::from("Overhaul"),
883                String::from("Battle"),
884                String::from("Campaign"),
885                String::from("Compilation"),
886                String::from("Units"),
887                String::from("Maps"),
888                String::from("Spanish"),
889                String::from("English"),
890                String::from("undefined"),
891                String::from("map"),
892            ],
893            KEY_THRONES_OF_BRITANNIA => vec![
894                String::from("mod"),
895                String::from("ui"),
896                String::from("battle"),
897                String::from("campaign"),
898                String::from("units"),
899                String::from("compilation"),
900                String::from("graphical"),
901                String::from("overhaul"),
902                String::from("maps"),
903            ],
904            KEY_ATTILA => vec![
905                String::from("mod"),
906                String::from("UI"),
907                String::from("Graphical"),
908                String::from("Battle"),
909                String::from("Campaign"),
910                String::from("Units"),
911                String::from("Overhaul"),
912                String::from("Compilation"),
913                String::from("Maps"),
914                String::from("version_2"),
915                String::from("Czech"),
916                String::from("Danish"),
917                String::from("English"),
918                String::from("Finnish"),
919                String::from("French"),
920                String::from("German"),
921                String::from("Hungarian"),
922                String::from("Italian"),
923                String::from("Japanese"),
924                String::from("Korean"),
925                String::from("Norwegian"),
926                String::from("Romanian"),
927                String::from("Russian"),
928                String::from("Spanish"),
929                String::from("Swedish"),
930                String::from("Thai"),
931                String::from("Turkish"),
932            ],
933            KEY_ROME_2 => vec![
934                String::from("mod"),
935                String::from("Units"),
936                String::from("Battle"),
937                String::from("Overhaul"),
938                String::from("Compilation"),
939                String::from("Campaign"),
940                String::from("Graphical"),
941                String::from("UI"),
942                String::from("Maps"),
943                String::from("version_2"),
944                String::from("English"),
945                String::from("gribble"),
946                String::from("tribble"),
947            ],
948            KEY_SHOGUN_2 => vec![
949                String::from("map"),
950                String::from("historical"),
951                String::from("multiplayer"),
952                String::from("mod"),
953                String::from("version_2"),
954                String::from("English"),
955                String::from("ui"),
956                String::from("graphical"),
957                String::from("overhaul"),
958                String::from("units"),
959                String::from("campaign"),
960                String::from("battle"),
961            ],
962            _ => return Err(RLibError::GameDoesntSupportWorkshop(self.key().to_owned()))
963        })
964    }
965
966    /// This function returns the game that corresponds to the provided Steam ID, if any.
967    pub fn game_by_steam_id(steam_id: u64) -> Result<Self> {
968        let games = SupportedGames::default();
969        for game in games.games() {
970
971            // No need to check LnxSteam, as they share the same id.
972            match game.install_data.get(&InstallType::WinSteam) {
973                Some(install_data) => if install_data.store_id == steam_id {
974                    return Ok(game.clone());
975                } else {
976                    continue;
977                }
978                None => continue,
979            }
980        }
981
982        Err(RLibError::SteamIDDoesntBelongToKnownGame(steam_id))
983    }
984}