use directories::ProjectDirs;
use getset::*;
#[cfg(feature = "integration_log")] use log::{info, warn};
use steamlocate::SteamDir;
use std::collections::HashMap;
use std::{fmt, fmt::Display};
use std::fs::{DirBuilder, File};
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use crate::compression::CompressionFormat;
use crate::error::{RLibError, Result};
use crate::utils::*;
use self::supported_games::*;
use self::manifest::Manifest;
use self::pfh_file_type::PFHFileType;
use self::pfh_version::PFHVersion;
pub mod supported_games;
pub mod manifest;
pub mod pfh_file_type;
pub mod pfh_version;
pub const BRAZILIAN: &str = "br";
pub const SIMPLIFIED_CHINESE: &str = "cn";
pub const CZECH: &str = "cz";
pub const ENGLISH: &str = "en";
pub const FRENCH: &str = "fr";
pub const GERMAN: &str = "ge";
pub const ITALIAN: &str = "it";
pub const KOREAN: &str = "kr";
pub const POLISH: &str = "pl";
pub const RUSSIAN: &str = "ru";
pub const SPANISH: &str = "sp";
pub const TURKISH: &str = "tr";
pub const TRADITIONAL_CHINESE: &str = "zh";
pub const LUA_AUTOGEN_FOLDER: &str = "tw_autogen";
pub const LUA_REPO: &str = "https://github.com/chadvandy/tw_autogen";
pub const LUA_REMOTE: &str = "origin";
pub const LUA_BRANCH: &str = "main";
pub const OLD_AK_REPO: &str = "https://github.com/Frodo45127/total_war_ak_files_pre_shogun_2";
pub const OLD_AK_REMOTE: &str = "origin";
pub const OLD_AK_BRANCH: &str = "master";
pub const TRANSLATIONS_REPO: &str = "https://github.com/Frodo45127/total_war_translation_hub";
pub const TRANSLATIONS_REMOTE: &str = "origin";
pub const TRANSLATIONS_BRANCH: &str = "master";
#[derive(Getters, Clone, Debug)]
#[getset(get = "pub")]
pub struct GameInfo {
#[getset(skip)]
key: &'static str,
display_name: &'static str,
pfh_versions: HashMap<PFHFileType, PFHVersion>,
schema_file_name: String,
dependencies_cache_file_name: String,
raw_db_version: i16,
portrait_settings_version: Option<u32>,
supports_editing: bool,
db_tables_have_guid: bool,
locale_file_name: Option<String>,
banned_packedfiles: Vec<String>,
icon_small: String,
icon_big: String,
vanilla_db_table_name_logic: VanillaDBTableNameLogic,
#[getset(skip)]
install_data: HashMap<InstallType, InstallData>,
tool_vars: HashMap<String, String>,
lua_autogen_folder: Option<String>,
ak_lost_fields: Vec<String>,
#[getset(skip)]
install_type_cache: Arc<RwLock<HashMap<PathBuf, InstallType>>>,
compression_formats_supported: Vec<CompressionFormat>,
max_cs2_parsed_version: u32,
}
#[derive(Clone, Debug)]
pub enum VanillaDBTableNameLogic {
FolderName,
DefaultName(String),
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum InstallType {
WinSteam,
LnxSteam,
WinEpic,
WinWargaming,
}
#[derive(Getters, Clone, Debug)]
#[getset(get = "pub")]
pub struct InstallData {
vanilla_packs: Vec<String>,
use_manifest: bool,
store_id: u64,
store_id_ak: u64,
executable: String,
data_path: String,
language_path: String,
local_mods_path: String,
downloaded_mods_path: String,
config_folder: Option<String>,
}
impl Display for InstallType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Display::fmt(match self {
Self::WinSteam => "Windows - Steam",
Self::LnxSteam => "Linux - Steam",
Self::WinEpic => "Windows - Epic",
Self::WinWargaming => "Windows - Wargaming",
}, f)
}
}
impl GameInfo {
pub fn key(&self) -> &str {
self.key
}
pub fn pfh_version_by_file_type(&self, pfh_file_type: PFHFileType) -> PFHVersion {
match self.pfh_versions.get(&pfh_file_type) {
Some(pfh_version) => *pfh_version,
None => *self.pfh_versions.get(&PFHFileType::Mod).unwrap(),
}
}
pub fn install_type(&self, game_path: &Path) -> Result<InstallType> {
if let Some(install_type) = self.install_type_cache.read().unwrap().get(game_path) {
return Ok(install_type.clone());
}
let base_path_files = files_from_subdir(game_path, false)?;
let install_type_by_exe = self.install_data.iter().filter_map(|(install_type, install_data)|
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()) {
Some(install_type)
} else { None }
).collect::<Vec<&InstallType>>();
if install_type_by_exe.is_empty() {
let install_type = self.install_data.keys().next().unwrap();
self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), install_type.clone());
Ok(install_type.clone())
}
else if install_type_by_exe.len() == 1 {
self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), install_type_by_exe[0].clone());
Ok(install_type_by_exe[0].clone())
}
else {
let is_windows = install_type_by_exe.iter().any(|install_type| install_type == &&InstallType::WinSteam || install_type == &&InstallType::WinEpic || install_type == &&InstallType::WinWargaming);
if is_windows {
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");
let has_eos_sdk_dll = base_path_files.iter().filter_map(|path| path.file_name()).any(|filename| filename == "EOSSDK-Win64-Shipping.dll");
if has_steam_api_dll && install_type_by_exe.contains(&&InstallType::WinSteam) {
self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), InstallType::WinSteam);
Ok(InstallType::WinSteam)
}
else if has_eos_sdk_dll && install_type_by_exe.contains(&&InstallType::WinEpic) {
self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), InstallType::WinEpic);
Ok(InstallType::WinEpic)
}
else {
self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), InstallType::WinWargaming);
Ok(InstallType::WinWargaming)
}
}
else {
self.install_type_cache.write().unwrap().insert(game_path.to_path_buf(), InstallType::LnxSteam);
Ok(InstallType::LnxSteam)
}
}
}
pub fn install_data(&self, game_path: &Path) -> Result<&InstallData> {
let install_type = self.install_type(game_path)?;
let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
Ok(install_data)
}
pub fn data_path(&self, game_path: &Path) -> Result<PathBuf> {
let install_type = self.install_type(game_path)?;
let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
Ok(game_path.join(install_data.data_path()))
}
pub fn content_path(&self, game_path: &Path) -> Result<PathBuf> {
let install_type = self.install_type(game_path)?;
let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
Ok(game_path.join(install_data.downloaded_mods_path()))
}
pub fn language_path(&self, game_path: &Path) -> Result<PathBuf> {
let language_file_name = self.locale_file_name().clone().unwrap_or_else(|| "language.txt".to_owned());
let install_type = self.install_type(game_path)?;
let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
let base_path = game_path.join(install_data.language_path());
let path_with_file = base_path.join(language_file_name);
if path_with_file.is_file() {
Ok(base_path)
} else {
let path = base_path.join(BRAZILIAN);
if path.is_dir() {
return Ok(path);
}
let path = base_path.join(SIMPLIFIED_CHINESE);
if path.is_dir() {
return Ok(path);
}
let path = base_path.join(CZECH);
if path.is_dir() {
return Ok(path);
}
let path = base_path.join(ENGLISH);
if path.is_dir() {
return Ok(path);
}
let path = base_path.join(FRENCH);
if path.is_dir() {
return Ok(path);
}
let path = base_path.join(GERMAN);
if path.is_dir() {
return Ok(path);
}
let path = base_path.join(ITALIAN);
if path.is_dir() {
return Ok(path);
}
let path = base_path.join(KOREAN);
if path.is_dir() {
return Ok(path);
}
let path = base_path.join(POLISH);
if path.is_dir() {
return Ok(path);
}
let path = base_path.join(RUSSIAN);
if path.is_dir() {
return Ok(path);
}
let path = base_path.join(SPANISH);
if path.is_dir() {
return Ok(path);
}
let path = base_path.join(TURKISH);
if path.is_dir() {
return Ok(path);
}
let path = base_path.join(TRADITIONAL_CHINESE);
if path.is_dir() {
return Ok(path);
}
Ok(base_path)
}
}
pub fn local_mods_path(&self, game_path: &Path) -> Result<PathBuf> {
let install_type = self.install_type(game_path)?;
let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
Ok(game_path.join(install_data.local_mods_path()))
}
pub fn content_packs_paths(&self, game_path: &Path) -> Option<Vec<PathBuf>> {
let install_type = self.install_type(game_path).ok()?;
let install_data = self.install_data.get(&install_type)?;
let downloaded_mods_path = install_data.downloaded_mods_path();
if downloaded_mods_path.is_empty() {
return None;
}
let path = std::fs::canonicalize(game_path.join(downloaded_mods_path)).ok()?;
let mut paths = vec![];
for path in files_from_subdir(&path, true).ok()?.iter() {
match path.extension() {
Some(extension) => if extension == "pack" || extension == "bin" { paths.push(path.to_path_buf()); }
None => continue,
}
}
paths.sort();
Some(paths)
}
pub fn secondary_packs_paths(&self, secondary_path: &Path) -> Option<Vec<PathBuf>> {
if !secondary_path.is_dir() || !secondary_path.exists() || !secondary_path.is_absolute() {
return None;
}
let game_path = secondary_path.join(self.key());
if !game_path.is_dir() || !game_path.exists() {
return None;
}
let mut paths = vec![];
for path in files_from_subdir(&game_path, false).ok()?.iter() {
match path.extension() {
Some(extension) => if extension == "pack" {
paths.push(path.to_path_buf());
}
None => continue,
}
}
paths.sort();
Some(paths)
}
pub fn data_packs_paths(&self, game_path: &Path) -> Option<Vec<PathBuf>> {
let game_path = self.data_path(game_path).ok()?;
let mut paths = vec![];
for path in files_from_subdir(&game_path, false).ok()?.iter() {
match path.extension() {
Some(extension) => if extension == "pack" { paths.push(path.to_path_buf()); }
None => continue,
}
}
paths.sort();
Some(paths)
}
pub fn mymod_install_path(&self, game_path: &Path) -> Option<PathBuf> {
let install_type = self.install_type(game_path).ok()?;
let install_data = self.install_data.get(&install_type)?;
let path = game_path.join(PathBuf::from(install_data.local_mods_path()));
DirBuilder::new().recursive(true).create(&path).ok()?;
Some(path)
}
pub fn use_manifest(&self, game_path: &Path) -> Result<bool> {
let install_type = self.install_type(game_path)?;
let install_data = self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?;
Ok(*install_data.use_manifest())
}
pub fn steam_id(&self, game_path: &Path) -> Result<u64> {
let install_type = self.install_type(game_path)?;
let install_data = match install_type {
InstallType::WinSteam |
InstallType::LnxSteam => self.install_data.get(&install_type).ok_or_else(|| RLibError::GameInstallTypeNotSupported(self.display_name.to_string(), install_type.to_string()))?,
_ => return Err(RLibError::ReservedFiles)
};
Ok(*install_data.store_id())
}
pub fn ca_packs_paths(&self, game_path: &Path) -> Result<Vec<PathBuf>> {
let language = self.game_locale_from_file(game_path)?;
if !self.use_manifest(game_path)? {
self.ca_packs_paths_no_manifest(game_path, &language)
} else {
match Manifest::read_from_game_path(self, game_path) {
Ok(manifest) => {
let data_path = self.data_path(game_path)?;
let mut paths = manifest.0.iter().filter_map(|entry|
if entry.relative_path().ends_with(".pack") {
let mut pack_file_path = data_path.to_path_buf();
pack_file_path.push(entry.relative_path());
match &language {
Some(language) => {
if entry.relative_path().contains("local_") {
let language = "local_".to_owned() + language;
if entry.relative_path().contains(&language) {
entry.path_from_manifest_entry(pack_file_path)
} else {
None
}
} else {
entry.path_from_manifest_entry(pack_file_path)
}
}
None => entry.path_from_manifest_entry(pack_file_path),
}
} else { None }
).collect::<Vec<PathBuf>>();
paths.sort();
Ok(paths)
}
Err(_) => self.ca_packs_paths_no_manifest(game_path, &language)
}
}
}
fn ca_packs_paths_no_manifest(&self, game_path: &Path, language: &Option<String>) -> Result<Vec<PathBuf>> {
let data_path = self.data_path(game_path)?;
let install_type = self.install_type(game_path)?;
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;
let language_pack = language.clone().map(|lang| format!("local_{lang}"));
if !vanilla_packs.is_empty() {
Ok(vanilla_packs.iter().filter_map(|pack_name| {
let mut pack_file_path = data_path.to_path_buf();
pack_file_path.push(pack_name);
match language_pack {
Some(ref language_pack) => {
if !pack_name.is_empty() && pack_name.starts_with("local_") {
if pack_name.starts_with(language_pack) {
std::fs::canonicalize(pack_file_path).ok()
} else {
None
}
} else {
std::fs::canonicalize(pack_file_path).ok()
}
}
None => std::fs::canonicalize(pack_file_path).ok(),
}
}).collect::<Vec<PathBuf>>())
}
else {
Ok(files_from_subdir(&data_path, false)?.iter()
.filter_map(|x| if let Some(extension) = x.extension() {
if extension.to_string_lossy().to_lowercase() == "pack" {
Some(x.to_owned())
} else { None }
} else { None }).collect::<Vec<PathBuf>>()
)
}
}
pub fn game_launch_command(&self, game_path: &Path) -> Result<String> {
let install_type = self.install_type(game_path)?;
match install_type {
InstallType::LnxSteam |
InstallType::WinSteam => {
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();
Ok(format!("steam://rungameid/{store_id}"))
},
_ => Err(RLibError::GameInstallLaunchNotSupported(self.display_name.to_string(), install_type.to_string())),
}
}
pub fn executable_path(&self, game_path: &Path) -> Option<PathBuf> {
let install_type = self.install_type(game_path).ok()?;
let install_data = self.install_data.get(&install_type)?;
let executable_path = game_path.join(install_data.executable());
Some(executable_path)
}
pub fn config_path(&self, game_path: &Path) -> Option<PathBuf> {
let install_type = self.install_type(game_path).ok()?;
let install_data = self.install_data.get(&install_type)?;
let config_folder = install_data.config_folder.as_ref()?;
ProjectDirs::from("com", "The Creative Assembly", config_folder).map(|dir| {
let mut dir = dir.config_dir().to_path_buf();
dir.pop();
dir
})
}
pub fn is_file_banned(&self, path: &str) -> bool {
let path = path.to_lowercase();
self.banned_packedfiles.iter().any(|x| path.starts_with(x))
}
pub fn tool_var(&self, var: &str) -> Option<&String> {
self.tool_vars.get(var)
}
pub fn game_locale_from_file(&self, game_path: &Path) -> Result<Option<String>> {
match self.locale_file_name() {
Some(locale_file) => {
let language_path = self.language_path(game_path)?;
let locale_path = language_path.join(locale_file);
let mut language = String::new();
if let Ok(mut file) = File::open(locale_path) {
file.read_to_string(&mut language)?;
let language = match &*language {
"BR" => BRAZILIAN.to_owned(),
"CN" => SIMPLIFIED_CHINESE.to_owned(),
"CZ" => CZECH.to_owned(),
"EN" => ENGLISH.to_owned(),
"FR" => FRENCH.to_owned(),
"DE" => GERMAN.to_owned(),
"IT" => ITALIAN.to_owned(),
"KR" => KOREAN.to_owned(),
"PO" => POLISH.to_owned(),
"RU" => RUSSIAN.to_owned(),
"ES" => SPANISH.to_owned(),
"TR" => TURKISH.to_owned(),
"ZH" => TRADITIONAL_CHINESE.to_owned(),
_ => ENGLISH.to_owned(),
};
#[cfg(feature = "integration_log")] {
info!("Language file found, using {language} language.");
}
Ok(Some(language))
} else {
#[cfg(feature = "integration_log")] {
warn!("Missing or unreadable language file under {}. Using english language.", game_path.to_string_lossy());
}
Ok(Some(ENGLISH.to_owned()))
}
}
None => Ok(None),
}
}
pub fn game_version_number(&self, game_path: &Path) -> Option<u32> {
match self.key() {
KEY_TROY => {
let exe_path = self.executable_path(game_path)?;
if exe_path.is_file() {
let mut data = vec![];
let mut file = BufReader::new(File::open(exe_path).ok()?);
file.read_to_end(&mut data).ok()?;
let version_info = pe_version_info(&data).ok()?;
let version_info = version_info.fixed()?;
let mut version: u32 = 0;
let major = version_info.dwFileVersion.Major as u32;
let minor = version_info.dwFileVersion.Minor as u32;
let patch = version_info.dwFileVersion.Patch as u32;
let build = version_info.dwFileVersion.Build as u32;
version += major << 24;
version += minor << 16;
version += patch << 8;
version += build;
Some(version)
}
else {
None
}
}
_ => None,
}
}
pub fn find_game_install_location(&self) -> Result<Option<PathBuf>> {
let install_data = if let Some(install_data) = self.install_data.get(&InstallType::WinSteam) {
install_data
} else if let Some(install_data) = self.install_data.get(&InstallType::LnxSteam) {
install_data
} else {
return Ok(None);
};
if install_data.store_id() > &0 {
if let Ok(steamdir) = SteamDir::locate() {
return match steamdir.find_app(*install_data.store_id() as u32) {
Ok(Some((app, lib))) => {
let app_path = lib.resolve_app_dir(&app);
if app_path.is_dir() {
Ok(Some(app_path.to_path_buf()))
} else {
Ok(None)
}
}
_ => Ok(None)
}
}
}
Ok(None)
}
pub fn find_assembly_kit_install_location(&self) -> Result<Option<PathBuf>> {
let install_data = if let Some(install_data) = self.install_data.get(&InstallType::WinSteam) {
install_data
} else if let Some(install_data) = self.install_data.get(&InstallType::LnxSteam) {
install_data
} else {
return Ok(None);
};
if install_data.store_id_ak() > &0 {
if let Ok(steamdir) = SteamDir::locate() {
return match steamdir.find_app(*install_data.store_id_ak() as u32) {
Ok(Some((app, lib))) => {
let app_path = lib.resolve_app_dir(&app);
if app_path.is_dir() {
Ok(Some(app_path.to_path_buf()))
} else {
Ok(None)
}
}
_ => Ok(None)
}
}
}
Ok(None)
}
pub fn steam_workshop_tags(&self) -> Result<Vec<String>> {
Ok(match self.key() {
KEY_PHARAOH_DYNASTIES => vec![
String::from("mod"),
String::from("graphical"),
String::from("campaign"),
String::from("ui"),
String::from("battle"),
String::from("overhaul"),
String::from("units"),
],
KEY_PHARAOH => vec![
String::from("mod"),
String::from("graphical"),
String::from("campaign"),
String::from("ui"),
String::from("battle"),
String::from("overhaul"),
String::from("units"),
],
KEY_WARHAMMER_3 => vec![
String::from("graphical"),
String::from("campaign"),
String::from("units"),
String::from("battle"),
String::from("ui"),
String::from("maps"),
String::from("overhaul"),
String::from("compilation"),
String::from("cheat"),
],
KEY_TROY => vec![
String::from("mod"),
String::from("ui"),
String::from("graphical"),
String::from("units"),
String::from("battle"),
String::from("campaign"),
String::from("overhaul"),
String::from("compilation"),
],
KEY_THREE_KINGDOMS => vec![
String::from("mod"),
String::from("graphical"),
String::from("overhaul"),
String::from("ui"),
String::from("battle"),
String::from("campaign"),
String::from("maps"),
String::from("units"),
String::from("compilation"),
],
KEY_WARHAMMER_2 => vec![
String::from("mod"),
String::from("Units"),
String::from("Battle"),
String::from("Graphical"),
String::from("UI"),
String::from("Campaign"),
String::from("Maps"),
String::from("Overhaul"),
String::from("Compilation"),
String::from("Mod Manager"),
String::from("Skills"),
String::from("map"),
],
KEY_WARHAMMER => vec![
String::from("mod"),
String::from("UI"),
String::from("Graphical"),
String::from("Overhaul"),
String::from("Battle"),
String::from("Campaign"),
String::from("Compilation"),
String::from("Units"),
String::from("Maps"),
String::from("Spanish"),
String::from("English"),
String::from("undefined"),
String::from("map"),
],
KEY_THRONES_OF_BRITANNIA => vec![
String::from("mod"),
String::from("ui"),
String::from("battle"),
String::from("campaign"),
String::from("units"),
String::from("compilation"),
String::from("graphical"),
String::from("overhaul"),
String::from("maps"),
],
KEY_ATTILA => vec![
String::from("mod"),
String::from("UI"),
String::from("Graphical"),
String::from("Battle"),
String::from("Campaign"),
String::from("Units"),
String::from("Overhaul"),
String::from("Compilation"),
String::from("Maps"),
String::from("version_2"),
String::from("Czech"),
String::from("Danish"),
String::from("English"),
String::from("Finnish"),
String::from("French"),
String::from("German"),
String::from("Hungarian"),
String::from("Italian"),
String::from("Japanese"),
String::from("Korean"),
String::from("Norwegian"),
String::from("Romanian"),
String::from("Russian"),
String::from("Spanish"),
String::from("Swedish"),
String::from("Thai"),
String::from("Turkish"),
],
KEY_ROME_2 => vec![
String::from("mod"),
String::from("Units"),
String::from("Battle"),
String::from("Overhaul"),
String::from("Compilation"),
String::from("Campaign"),
String::from("Graphical"),
String::from("UI"),
String::from("Maps"),
String::from("version_2"),
String::from("English"),
String::from("gribble"),
String::from("tribble"),
],
KEY_SHOGUN_2 => vec![
String::from("map"),
String::from("historical"),
String::from("multiplayer"),
String::from("mod"),
String::from("version_2"),
String::from("English"),
String::from("ui"),
String::from("graphical"),
String::from("overhaul"),
String::from("units"),
String::from("campaign"),
String::from("battle"),
],
_ => return Err(RLibError::GameDoesntSupportWorkshop(self.key().to_owned()))
})
}
pub fn game_by_steam_id(steam_id: u64) -> Result<Self> {
let games = SupportedGames::default();
for game in games.games() {
match game.install_data.get(&InstallType::WinSteam) {
Some(install_data) => if install_data.store_id == steam_id {
return Ok(game.clone());
} else {
continue;
}
None => continue,
}
}
Err(RLibError::SteamIDDoesntBelongToKnownGame(steam_id))
}
}