diff --git a/src/game/import/ea.rs b/src/game/import/ea.rs index a106220..4fa66c3 100644 --- a/src/game/import/ea.rs +++ b/src/game/import/ea.rs @@ -1,53 +1,36 @@ use std::path::PathBuf; -use super::GameImporter; +use super::ImportOverrides; use crate::error::Error; use crate::game::error::GameError; -use crate::game::import::ImportBase; -use crate::game::registry::{ActiveDistribution, GameData}; -use crate::ts::v1::models::ecosystem::GameDefPlatform; +use crate::game::registry::ActiveDistribution; +use crate::ts::v1::models::ecosystem::{GameDef, GamePlatform}; use crate::util::reg::{self, HKey}; -pub struct EaImporter { - ident: String, -} - -impl EaImporter { - pub fn new(ident: &str) -> EaImporter { - EaImporter { - ident: ident.into(), - } - } -} - -impl GameImporter for EaImporter { - fn construct(self: Box, base: ImportBase) -> Result { - let subkey = format!("Software\\{}\\", self.ident.replace('.', "\\")); - let value = reg::get_value_at(HKey::LocalMachine, &subkey, "Install Dir")?; +pub fn get_gamedist(ident: &str, game_def: &GameDef, overrides: &ImportOverrides) -> Result, Error> { + let subkey = format!("Software\\{}\\", ident.replace('.', "\\")); + let value = reg::get_value_at(HKey::LocalMachine, &subkey, "Install Dir")?; - let game_dir = PathBuf::from(value); - let r2mm = base.game_def.r2modman.as_ref().expect( - "Expected a valid r2mm field in the ecosystem schema, got nothing. This is a bug.", - ); + let game_dir = PathBuf::from(value); + let r2mm = game_def.r2modman.as_ref().expect( + "Expected a valid r2mm field in the ecosystem schema, got nothing. This is a bug.", + ).first().unwrap(); - let exe_path = base - .overrides - .custom_exe - .clone() - .or_else(|| super::find_game_exe(&r2mm.exe_names, &game_dir)) - .ok_or_else(|| GameError::ExeNotFound { - possible_names: r2mm.exe_names.clone(), - base_path: game_dir.clone(), - })?; - let dist = ActiveDistribution { - dist: GameDefPlatform::Origin { - identifier: self.ident.to_string(), - }, - game_dir: game_dir.to_path_buf(), - data_dir: game_dir.join(&r2mm.data_folder_name), - exe_path, - }; + let exe_path = overrides + .custom_exe + .clone() + .or_else(|| super::find_game_exe(&r2mm.exe_names, &game_dir)) + .ok_or_else(|| GameError::ExeNotFound { + possible_names: r2mm.exe_names.clone(), + base_path: game_dir.clone(), + })?; - Ok(super::construct_data(base, dist)) - } + Ok(Some(ActiveDistribution { + dist: GamePlatform::Origin { + identifier: ident.to_string(), + }, + game_dir: game_dir.to_path_buf(), + data_dir: game_dir.join(&r2mm.data_folder_name), + exe_path, + })) } diff --git a/src/game/import/egs.rs b/src/game/import/egs.rs index cd0be24..6d2c5d4 100644 --- a/src/game/import/egs.rs +++ b/src/game/import/egs.rs @@ -3,11 +3,11 @@ use std::path::PathBuf; use serde::{Deserialize, Serialize}; -use super::{GameImporter, ImportBase}; +use super::ImportOverrides; use crate::error::{Error, IoError}; use crate::game::error::GameError; -use crate::game::registry::{ActiveDistribution, GameData}; -use crate::ts::v1::models::ecosystem::GameDefPlatform; +use crate::game::registry::ActiveDistribution; +use crate::ts::v1::models::ecosystem::{GameDef, GamePlatform}; use crate::util::reg::{self, HKey}; #[derive(Serialize, Deserialize, Debug)] @@ -17,81 +17,65 @@ struct PartialInstallManifest { app_name: String, } -pub struct EgsImporter { - ident: String, -} - -impl EgsImporter { - pub fn new(ident: &str) -> EgsImporter { - EgsImporter { - ident: ident.into(), - } - } -} +pub fn get_gamedist(ident: &str, game_def: &GameDef, overrides: &ImportOverrides) -> Result, Error> { + let game_label = game_def.label.clone(); -impl GameImporter for EgsImporter { - fn construct(self: Box, base: ImportBase) -> Result { - let game_label = base.game_def.label.clone(); + // There's a couple ways that we can retrieve the path of a game installed via EGS. + // 1. Parse LauncherInstalled.dat in C:/ProgramData/Epic/UnrealEngineLauncher/ + // 2. Parse game manifest files in C:/ProgramData/Epic/EpicGamesLauncher/Data/Manifests + // I'm going to go for the second option. - // There's a couple ways that we can retrieve the path of a game installed via EGS. - // 1. Parse LauncherInstalled.dat in C:/ProgramData/Epic/UnrealEngineLauncher/ - // 2. Parse game manifest files in C:/ProgramData/Epic/EpicGamesLauncher/Data/Manifests - // I'm going to go for the second option. + // Attempt to get the path of the EGS /Data directory from the registry. + let subkey = r#"Software\WOW64Node\Epic Games\EpicGamesLauncher"#; + let value = reg::get_value_at(HKey::LocalMachine, subkey, "AppDataPath")?; + let manifests_dir = PathBuf::from(value).join("Manifests"); - // Attempt to get the path of the EGS /Data directory from the registry. - let subkey = r#"Software\WOW64Node\Epic Games\EpicGamesLauncher"#; - let value = reg::get_value_at(HKey::LocalMachine, subkey, "AppDataPath")?; - let manifests_dir = PathBuf::from(value).join("Manifests"); - - if !manifests_dir.exists() { - Err(IoError::DirNotFound(manifests_dir.clone()))?; - } + if !manifests_dir.exists() { + Err(IoError::DirNotFound(manifests_dir.clone()))?; + } - // Manifest files are JSON files with .item extensions. - let manifest_files = fs::read_dir(manifests_dir) - .unwrap() - .filter_map(|x| x.ok()) - .map(|x| x.path()) - .filter(|x| x.is_file() && x.extension().is_some()) - .filter(|x| x.extension().unwrap() == "item") - .collect::>(); + // Manifest files are JSON files with .item extensions. + let manifest_files = fs::read_dir(manifests_dir) + .unwrap() + .filter_map(|x| x.ok()) + .map(|x| x.path()) + .filter(|x| x.is_file() && x.extension().is_some()) + .filter(|x| x.extension().unwrap() == "item") + .collect::>(); - // Search for the manifest which contains the correct game AppName. - let game_dir = manifest_files - .into_iter() - .find_map(|x| { - let file_contents = fs::read_to_string(x).unwrap(); - let manifest: PartialInstallManifest = - serde_json::from_str(&file_contents).unwrap(); + // Search for the manifest which contains the correct game AppName. + let game_dir = manifest_files + .into_iter() + .find_map(|x| { + let file_contents = fs::read_to_string(x).unwrap(); + let manifest: PartialInstallManifest = + serde_json::from_str(&file_contents).unwrap(); - if manifest.app_name == self.ident { - Some(manifest.install_location) - } else { - None - } - }) - .ok_or_else(|| GameError::NotFound(game_label.clone(), "EGS".to_string()))?; + if manifest.app_name == ident { + Some(manifest.install_location) + } else { + None + } + }) + .ok_or_else(|| GameError::NotFound(game_label.clone(), "EGS".to_string()))?; - let r2mm = base.game_def.r2modman.as_ref().expect( - "Expected a valid r2mm field in the ecosystem schema, got nothing. This is a bug.", - ); + let r2mm = game_def.r2modman.as_ref().expect( + "Expected a valid r2mm field in the ecosystem schema, got nothing. This is a bug.", + ).first().unwrap(); - let exe_path = base - .overrides - .custom_exe - .clone() - .or_else(|| super::find_game_exe(&r2mm.exe_names, &game_dir)) - .ok_or_else(|| GameError::ExeNotFound { - possible_names: r2mm.exe_names.clone(), - base_path: game_dir.clone(), - })?; - let dist = ActiveDistribution { - dist: GameDefPlatform::Other, - game_dir: game_dir.to_path_buf(), - data_dir: game_dir.join(&r2mm.data_folder_name), - exe_path, - }; + let exe_path = overrides + .custom_exe + .clone() + .or_else(|| super::find_game_exe(&r2mm.exe_names, &game_dir)) + .ok_or_else(|| GameError::ExeNotFound { + possible_names: r2mm.exe_names.clone(), + base_path: game_dir.clone(), + })?; - Ok(super::construct_data(base, dist)) - } + Ok(Some(ActiveDistribution { + dist: GamePlatform::Other, + game_dir: game_dir.to_path_buf(), + data_dir: game_dir.join(&r2mm.data_folder_name), + exe_path, + })) } diff --git a/src/game/import/gamepass.rs b/src/game/import/gamepass.rs index b27015b..94b4297 100644 --- a/src/game/import/gamepass.rs +++ b/src/game/import/gamepass.rs @@ -1,67 +1,50 @@ use std::path::PathBuf; -use super::{GameImporter, ImportBase}; +use super::ImportOverrides; use crate::error::Error; use crate::game::error::GameError; -use crate::game::registry::{ActiveDistribution, GameData}; -use crate::ts::v1::models::ecosystem::GameDefPlatform; +use crate::game::registry::ActiveDistribution; +use crate::ts::v1::models::ecosystem::{GameDef, GamePlatform}; use crate::util::reg::{self, HKey}; -pub struct GamepassImporter { - ident: String, -} - -impl GamepassImporter { - pub fn new(ident: &str) -> GamepassImporter { - GamepassImporter { - ident: ident.into(), - } - } -} - -impl GameImporter for GamepassImporter { - fn construct(self: Box, base: ImportBase) -> Result { - let root = r#"Software\Microsoft\GamingServices\PackageRepository"#; - - let uuid = reg::get_values_at(HKey::LocalMachine, &format!("{root}\\Package\\"))? - .into_iter() - .find(|x| x.key.starts_with(&self.ident)) - .ok_or_else(|| { - GameError::NotFound(base.game_def.label.clone(), "Gamepass".to_string()) - })? - .val - .replace('\"', ""); - - let game_root = reg::get_keys_at(HKey::LocalMachine, &format!("Root\\{}\\", uuid))? - .into_iter() - .next() - .ok_or_else(|| { - GameError::NotFound(base.game_def.label.clone(), "Gamepass".to_string()) - })?; - let game_dir = PathBuf::from(reg::get_value_at(HKey::LocalMachine, &game_root, "Root")?); - - let r2mm = base.game_def.r2modman.as_ref().expect( - "Expected a valid r2mm field in the ecosystem schema, got nothing. This is a bug.", - ); - - let exe_path = base - .overrides - .custom_exe - .clone() - .or_else(|| super::find_game_exe(&r2mm.exe_names, &game_dir)) - .ok_or_else(|| GameError::ExeNotFound { - possible_names: r2mm.exe_names.clone(), - base_path: game_dir.clone(), - })?; - let dist = ActiveDistribution { - dist: GameDefPlatform::GamePass { - identifier: self.ident.to_string(), - }, - game_dir: game_dir.to_path_buf(), - data_dir: game_dir.join(&r2mm.data_folder_name), - exe_path, - }; - - Ok(super::construct_data(base, dist)) - } +pub fn get_gamedist(ident: &str, game_def: &GameDef, overrides: &ImportOverrides) -> Result, Error> { + let root = r#"Software\Microsoft\GamingServices\PackageRepository"#; + let r2mm = game_def.r2modman.as_ref().expect( + "Expected a valid r2mm field in the ecosystem schema, got nothing. This is a bug." + ).first().unwrap(); + + let uuid = reg::get_values_at(HKey::LocalMachine, &format!("{root}\\Package\\"))? + .into_iter() + .find(|x| x.key.starts_with(ident)) + .ok_or_else(|| { + GameError::NotFound(ident.into(), "Gamepass".to_string()) + })? + .val + .replace('\"', ""); + + let game_root = reg::get_keys_at(HKey::LocalMachine, &format!("Root\\{}\\", uuid))? + .into_iter() + .next() + .ok_or_else(|| { + GameError::NotFound(ident.into(), "Gamepass".to_string()) + })?; + let game_dir = PathBuf::from(reg::get_value_at(HKey::LocalMachine, &game_root, "Root")?); + + let exe_path = overrides + .custom_exe + .clone() + .or_else(|| super::find_game_exe(&r2mm.exe_names, &game_dir)) + .ok_or_else(|| GameError::ExeNotFound { + possible_names: r2mm.exe_names.clone(), + base_path: game_dir.clone(), + })?; + + Ok(Some(ActiveDistribution { + dist: GamePlatform::XboxGamePass { + identifier: ident.to_string(), + }, + game_dir: game_dir.to_path_buf(), + data_dir: game_dir.join(&r2mm.data_folder_name), + exe_path, + })) } diff --git a/src/game/import/mod.rs b/src/game/import/mod.rs index a5dcf73..1766aea 100644 --- a/src/game/import/mod.rs +++ b/src/game/import/mod.rs @@ -10,11 +10,7 @@ use super::ecosystem; use super::error::GameError; use super::registry::{ActiveDistribution, GameData}; use crate::error::Error; -use crate::game::import::ea::EaImporter; -use crate::game::import::egs::EgsImporter; -use crate::game::import::gamepass::GamepassImporter; -use crate::game::import::steam::SteamImporter; -use crate::ts::v1::models::ecosystem::{GameDef, GameDefPlatform}; +use crate::ts::v1::models::ecosystem::{GameDef, GamePlatform}; pub trait GameImporter { fn construct(self: Box, base: ImportBase) -> Result; @@ -25,6 +21,7 @@ pub struct ImportOverrides { pub custom_name: Option, pub custom_id: Option, pub custom_exe: Option, + pub steam_dir: Option, pub game_dir: Option, } @@ -38,8 +35,7 @@ pub struct ImportBase { impl ImportBase { pub async fn new(game_id: &str) -> Result { let game_def = ecosystem::get_schema() - .await - .unwrap() + .await? .games .get(game_id) .ok_or_else(|| GameError::BadGameId(game_id.into()))? @@ -63,31 +59,63 @@ impl ImportBase { ..self } } -} -pub fn select_importer(base: &ImportBase) -> Result, Error> { - base.game_def - .distributions - .iter() - .find_map(|dist| match dist { - GameDefPlatform::Origin { identifier } => { - Some(Box::new(EaImporter::new(identifier)) as _) - } - GameDefPlatform::EpicGames { identifier } => { - Some(Box::new(EgsImporter::new(identifier)) as _) - } - GameDefPlatform::GamePass { identifier } => { - Some(Box::new(GamepassImporter::new(identifier)) as _) - } - GameDefPlatform::Steam { identifier } => { - Some(Box::new(SteamImporter::new(identifier)) as _) - } - GameDefPlatform::SteamDirect { identifier } => { - Some(Box::new(SteamImporter::new(identifier)) as _) - } - _ => None, - }) - .ok_or_else(|| GameError::NotSupported(base.game_id.clone(), "".into()).into()) + /// Search the system for valid installs of the specified game id. + pub fn get_active_dists(&self) -> Result, Error> { + Ok(self.game_def + .distributions + .iter() + .filter_map(|x| self.get_active_dist(x).ok()) + .filter_map(|x| x) + .collect::>()) + } + + pub fn get_active_dist(&self, platform: &GamePlatform) -> Result, Error> { + match platform { + GamePlatform::EpicGamesStore { identifier } => { + egs::get_gamedist(identifier, &self.game_def, &self.overrides) + }, + GamePlatform::XboxGamePass { identifier } => { + gamepass::get_gamedist(identifier, &self.game_def, &self.overrides) + }, + GamePlatform::Origin { identifier } => { + ea::get_gamedist(identifier, &self.game_def, &self.overrides) + }, + GamePlatform::Steam { identifier } => { + steam::get_gamedist( + identifier.parse().unwrap(), + self.overrides.steam_dir.as_deref(), + &self.game_def, + &self.overrides, + ) + }, + GamePlatform::SteamDirect { identifier } => { + steam::get_gamedist( + identifier.parse().unwrap(), + self.overrides.steam_dir.as_deref(), + &self.game_def, + &self.overrides, + ) + }, + _ => panic!() + } + } + + pub fn make_gamedata(self, dist: ActiveDistribution) -> GameData { + GameData { + identifier: self + .overrides + .custom_id + .unwrap_or(self.game_def.label.clone()), + ecosystem_label: self.game_def.label, + display_name: self + .overrides + .custom_name + .unwrap_or(self.game_def.meta.display_name), + active_distribution: dist, + possible_distributions: self.game_def.distributions, + } + } } pub fn find_game_exe(possible: &[String], base_path: &Path) -> Option { @@ -97,6 +125,21 @@ pub fn find_game_exe(possible: &[String], base_path: &Path) -> Option { .find(|x| x.is_file()) } +/// Convert the provided game id and platform name to a full GamePlatform instance, if +/// one can be found in the ecosystem schema. +pub async fn plat_from_name(game_id: &str, plat_name: &str) -> Result { + let ecosystem = ecosystem::get_schema().await?; + let game = ecosystem.games.get(game_id) + .ok_or(GameError::BadGameId(game_id.into()))?; + + game + .distributions + .iter() + .find(|x| x.get_platform_name() == plat_name) + .ok_or(GameError::NotSupported(game_id.into(), plat_name.into()).into()) + .cloned() +} + pub fn construct_data(base: ImportBase, dist: ActiveDistribution) -> GameData { GameData { identifier: base diff --git a/src/game/import/nodrm.rs b/src/game/import/nodrm.rs index b362902..8735437 100644 --- a/src/game/import/nodrm.rs +++ b/src/game/import/nodrm.rs @@ -1,10 +1,10 @@ use std::path::{Path, PathBuf}; -use super::{GameImporter, ImportBase}; +use super::ImportOverrides; use crate::error::{Error, IoError}; use crate::game::error::GameError; -use crate::game::registry::{ActiveDistribution, GameData}; -use crate::ts::v1::models::ecosystem::GameDefPlatform; +use crate::game::registry::ActiveDistribution; +use crate::ts::v1::models::ecosystem::{GameDef, GamePlatform}; pub struct NoDrmImporter { game_dir: PathBuf, @@ -18,32 +18,27 @@ impl NoDrmImporter { } } -impl GameImporter for NoDrmImporter { - fn construct(self: Box, base: ImportBase) -> Result { - if !self.game_dir.exists() { - Err(IoError::DirNotFound(self.game_dir.to_path_buf()))?; - } - - let r2mm = base.game_def.r2modman.as_ref().expect( - "Expected a valid r2mm field in the ecosystem schema, got nothing. This is a bug.", - ); +pub fn get_gamedist(game_dir: &Path, game_def: &GameDef, overrides: &ImportOverrides) -> Result { + if !game_dir.exists() { + Err(IoError::DirNotFound(game_dir.to_path_buf()))?; + } - let exe_path = base - .overrides - .custom_exe - .clone() - .or_else(|| super::find_game_exe(&r2mm.exe_names, &self.game_dir)) - .ok_or_else(|| GameError::ExeNotFound { - possible_names: r2mm.exe_names.clone(), - base_path: self.game_dir.clone(), - })?; - let dist = ActiveDistribution { - dist: GameDefPlatform::Other, - game_dir: self.game_dir.to_path_buf(), - data_dir: self.game_dir.join(&r2mm.data_folder_name), - exe_path, - }; + let r2mm = game_def.r2modman.as_ref().expect( + "Expected a valid r2mm field in the ecosystem schema, got nothing. This is a bug.", + ).first().unwrap(); - Ok(super::construct_data(base, dist)) - } + let exe_path = overrides + .custom_exe + .clone() + .or_else(|| super::find_game_exe(&r2mm.exe_names, game_dir)) + .ok_or_else(|| GameError::ExeNotFound { + possible_names: r2mm.exe_names.clone(), + base_path: game_dir.to_path_buf(), + })?; + Ok(ActiveDistribution { + dist: GamePlatform::Other, + game_dir: game_dir.to_path_buf(), + data_dir: game_dir.join(&r2mm.data_folder_name), + exe_path, + }) } diff --git a/src/game/import/steam.rs b/src/game/import/steam.rs index 0f58691..a79f780 100644 --- a/src/game/import/steam.rs +++ b/src/game/import/steam.rs @@ -1,94 +1,69 @@ -use std::path::PathBuf; +use std::path::Path; use steamlocate::SteamDir; -use super::{GameImporter, ImportBase}; +use super::ImportOverrides; use crate::error::Error; use crate::game::error::GameError; -use crate::game::registry::{ActiveDistribution, GameData}; -use crate::ts::v1::models::ecosystem::GameDefPlatform; +use crate::game::registry::ActiveDistribution; +use crate::ts::v1::models::ecosystem::{GameDef, GamePlatform}; -pub struct SteamImporter { - appid: u32, - steam_dir: Option, -} +pub fn get_gamedist(app_id: u32, steam_dir: Option<&Path>, game_def: &GameDef, overrides: &ImportOverrides) -> Result, Error> { + // If an app_dir is provided then we can skip automatic path resolution. If not, + // attempt to resolve the app's directory from the steam dir, whether provided or otherwise. + let app_dir = match overrides.game_dir { + Some(ref game_dir) => game_dir.clone(), + None => { + let steam = steam_dir + .as_ref() + .map_or_else(SteamDir::locate, |x| SteamDir::from_dir(x)) + .map_err(|e: steamlocate::Error| match e { + steamlocate::Error::InvalidSteamDir(_) => GameError::SteamDirBadPath( + steam_dir.as_ref().unwrap().to_path_buf(), + ), + steamlocate::Error::FailedLocate(_) => GameError::SteamDirNotFound, + _ => unreachable!(), + })?; -impl SteamImporter { - pub fn new(appid: &str) -> Self { - SteamImporter { - steam_dir: None, - appid: appid - .parse::() - .expect("Got a bad appid from the ecosystem schema. This is a bug"), + let (app, lib) = steam + .find_app(app_id) + .unwrap_or_else(|e| { + panic!( + "An error occured while searching for app with id '{}': {e:?}.", + app_id + ) + }) + .ok_or_else(|| { + GameError::SteamAppNotFound(app_id, steam.path().to_path_buf()) + })?; + lib.resolve_app_dir(&app) } - } + }; - pub fn with_steam_dir(self, steam_dir: Option) -> Self { - SteamImporter { steam_dir, ..self } + if !app_dir.is_dir() { + Err(GameError::SteamDirNotFound)?; } -} -impl GameImporter for SteamImporter { - fn construct(self: Box, base: ImportBase) -> Result { - // If an app_dir is provided then we can skip automatic path resolution. If not, - // attempt to resolve the app's directory from the steam dir, whether provided or otherwise. - let app_dir = match base.overrides.game_dir { - Some(ref game_dir) => game_dir.clone(), - None => { - let steam = self - .steam_dir - .as_ref() - .map_or_else(SteamDir::locate, |x| SteamDir::from_dir(x)) - .map_err(|e: steamlocate::Error| match e { - steamlocate::Error::InvalidSteamDir(_) => GameError::SteamDirBadPath( - self.steam_dir.as_ref().unwrap().to_path_buf(), - ), - steamlocate::Error::FailedLocate(_) => GameError::SteamDirNotFound, - _ => unreachable!(), - })?; + let r2mm = game_def.r2modman.as_ref().expect( + "Expected a valid r2mm field in the ecosystem schema, got nothing. This is a bug.", + ).first().unwrap(); - let (app, lib) = steam - .find_app(self.appid) - .unwrap_or_else(|e| { - panic!( - "An error occured while searching for app with id '{}': {e:?}.", - self.appid - ) - }) - .ok_or_else(|| { - GameError::SteamAppNotFound(self.appid, steam.path().to_path_buf()) - })?; - lib.resolve_app_dir(&app) - } - }; - - if !app_dir.is_dir() { - Err(GameError::SteamDirNotFound)?; - } + let exe_path = r2mm + .exe_names + .iter() + .map(|x| app_dir.join(x)) + .find(|x| x.is_file()) + .ok_or_else(|| GameError::ExeNotFound { + possible_names: r2mm.exe_names.clone(), + base_path: app_dir.clone(), + })?; - let r2mm = base.game_def.r2modman.as_ref().expect( - "Expected a valid r2mm field in the ecosystem schema, got nothing. This is a bug.", - ); - - let exe_path = r2mm - .exe_names - .iter() - .map(|x| app_dir.join(x)) - .find(|x| x.is_file()) - .ok_or_else(|| GameError::ExeNotFound { - possible_names: r2mm.exe_names.clone(), - base_path: app_dir.clone(), - })?; - - let dist = ActiveDistribution { - dist: GameDefPlatform::Steam { - identifier: self.appid.to_string(), - }, - data_dir: app_dir.join(&r2mm.data_folder_name), - game_dir: app_dir, - exe_path, - }; - - Ok(super::construct_data(base, dist)) - } + Ok(Some(ActiveDistribution { + dist: GamePlatform::Steam { + identifier: app_id.to_string(), + }, + data_dir: app_dir.join(&r2mm.data_folder_name), + game_dir: app_dir, + exe_path, + })) } diff --git a/src/game/registry.rs b/src/game/registry.rs index 45a9d3c..7dc906e 100644 --- a/src/game/registry.rs +++ b/src/game/registry.rs @@ -5,7 +5,7 @@ use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use crate::error::Error; -use crate::ts::v1::models::ecosystem::GameDefPlatform; +use crate::ts::v1::models::ecosystem::GamePlatform; use crate::util::os::OS; #[derive(Serialize, Deserialize, Debug, PartialEq)] @@ -14,12 +14,12 @@ pub struct GameData { pub identifier: String, pub display_name: String, pub active_distribution: ActiveDistribution, - pub possible_distributions: Vec, + pub possible_distributions: Vec, } -#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct ActiveDistribution { - pub dist: GameDefPlatform, + pub dist: GamePlatform, pub game_dir: PathBuf, pub data_dir: PathBuf, pub exe_path: PathBuf, diff --git a/src/main.rs b/src/main.rs index 50e6df8..b621242 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,19 +5,18 @@ use std::path::PathBuf; use clap::Parser; use cli::{ExternSubcommand, InitSubcommand}; -use colored::Colorize; use directories::BaseDirs; use error::{Error, IoError}; -use game::import::GameImporter; use once_cell::sync::Lazy; use project::error::ProjectError; use project::ProjectKind; use ts::error::ApiError; +use ts::v1::models::ecosystem::GamePlatform; use wildmatch::WildMatch; use crate::cli::{Args, Commands, ListSubcommand}; use crate::config::Vars; -use crate::game::import::{self, ImportBase, ImportOverrides}; +use crate::game::import::{ImportBase, ImportOverrides}; use crate::game::{ecosystem, registry}; use crate::package::resolver::DependencyGraph; use crate::package::Package; @@ -183,40 +182,30 @@ async fn main() -> Result<(), Error> { let overrides = ImportOverrides { custom_name, custom_id, + steam_dir, custom_exe: None, game_dir: game_dir.clone(), }; + let import_base = ImportBase::new(&game_id).await?.with_overrides(overrides); - if platform.is_none() { - let importer = import::select_importer(&import_base)?; - let game_data = importer.construct(import_base)?; + if let Some(platform) = platform { + let platform = game::import::plat_from_name(&game_id, &platform).await?; + let dist = import_base.get_active_dist(&platform)?; + + if dist.is_none() { + panic!("No valid platform found for the provided game id."); + } + + let game_data = import_base.make_gamedata(dist.unwrap()); return project.add_game_data(game_data); } - // Hacky fix for now - let platform = platform.unwrap(); - let dists = &import_base.game_def.distributions; - let ident = dists.iter().find_map(|x| x.ident_from_name(&platform)); - - let importer: Box = match (ident, platform.as_str()) { - (Some(ident), "steam") => { - Box::new(import::steam::SteamImporter::new(ident).with_steam_dir(steam_dir)) - as _ - } - (None, "nodrm") => Box::new(import::nodrm::NoDrmImporter::new( - game_dir.as_ref().unwrap(), - )) as _, - _ => panic!("Manually importing games from '{platform}' is not implemented"), - }; - let game_data = importer.construct(import_base)?; - let res = project.add_game_data(game_data); - println!( - "{} has been imported into the current project", - game_id.green() - ); + // Automatically choose the platform if one is not specified. + let dists = import_base.get_active_dists()?; + let game_data = import_base.make_gamedata(dists[0].clone()); - res + project.add_game_data(game_data) } Commands::Run { @@ -365,11 +354,11 @@ async fn main() -> Result<(), Error> { match command { ExternSubcommand::GameData { game_id } => { let base = ImportBase::new(&game_id).await?; - let game_data = import::select_importer(&base)?.construct(base)?; + let dists = base.get_active_dists()?; println!( "{}", - serde_json::to_string_pretty(&game_data.active_distribution)? + serde_json::to_string_pretty(&dists)? ); } } diff --git a/src/ts/v1/models/ecosystem.rs b/src/ts/v1/models/ecosystem.rs index 07f8d6b..cb12f10 100644 --- a/src/ts/v1/models/ecosystem.rs +++ b/src/ts/v1/models/ecosystem.rs @@ -2,7 +2,11 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; +use crate::error::Error; use crate::ts::version::Version; +use crate::game::ecosystem; +use crate::game::error::GameError; + #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] @@ -11,6 +15,7 @@ pub struct EcosystemSchema { pub games: HashMap, pub communities: HashMap, pub modloader_packages: Vec, + pub package_installers: HashMap, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -19,28 +24,27 @@ pub struct GameDef { pub uuid: String, pub label: String, pub meta: GameDefMeta, - pub distributions: Vec, - pub r2modman: Option, + pub distributions: Vec, + pub r2modman: Option>, pub thunderstore: Option, } #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct GameDefMeta { + #[serde(default)] pub display_name: String, pub icon_url: Option, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, clap::Subcommand)] #[serde(tag = "platform")] #[serde(rename_all = "kebab-case")] -pub enum GameDefPlatform { - #[serde(rename = "epic-games-store")] - EpicGames { +pub enum GamePlatform { + EpicGamesStore { identifier: String, }, - #[serde(rename = "xbox-game-pass")] - GamePass { + XboxGamePass { identifier: String, }, Origin { @@ -52,31 +56,36 @@ pub enum GameDefPlatform { SteamDirect { identifier: String, }, - #[serde(rename = "oculus-store")] - Oculus, + OculusStore, Other, } -impl GameDefPlatform { +impl GamePlatform { /// Hardcoding these for now until we integrate this sorta thing into /// the ecosystem schema, preferably as a compile time check. pub fn ident_from_name<'a>(&'a self, name: &str) -> Option<&'a str> { - match self { - GameDefPlatform::EpicGames { identifier } if name == "epic-games-store" => { - Some(identifier) - } - GameDefPlatform::GamePass { identifier } if name == "gamepass" => Some(identifier), - GameDefPlatform::Origin { identifier } if name == "origin" || name == "ea" => { - Some(identifier) - } - GameDefPlatform::Steam { identifier } if name == "steam" => Some(identifier), - GameDefPlatform::SteamDirect { identifier } if name == "steam-direct" => { - Some(identifier) - } + match (name, self) { + ("epic-games-store", GamePlatform::EpicGamesStore { identifier }) => Some(identifier), + ("gamepass", GamePlatform::XboxGamePass { identifier }) => Some(identifier), + ("origin" | "ea", GamePlatform::Origin { identifier }) => Some(identifier), + ("steam", GamePlatform::Steam { identifier }) => Some(identifier), + ("steam-direct", GamePlatform::SteamDirect { identifier }) => Some(identifier), _ => None, } } + pub fn get_platform_name(&self) -> &'static str { + match self { + GamePlatform::EpicGamesStore { identifier: _ } => "epic-games-store", + GamePlatform::XboxGamePass { identifier: _ } => "gamepass", + GamePlatform::Origin { identifier: _ } => "origin", + GamePlatform::Steam { identifier: _ } => "steam", + GamePlatform::SteamDirect { identifier: _ } => "steam-direct", + GamePlatform::OculusStore => "oculus-store", + GamePlatform::Other => "other", + } + } + pub fn get_platform_names(&self) -> Vec<&'static str> { vec![ "origin", @@ -93,14 +102,18 @@ impl GameDefPlatform { #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct GameDefR2MM { + pub meta: GameDefMeta, pub internal_folder_name: String, pub data_folder_name: String, + pub distributions: Vec, pub settings_identifier: String, pub package_index: String, pub steam_folder_name: String, pub exe_names: Vec, pub game_instance_type: String, pub game_selection_display_mode: String, + pub additional_search_strings: Vec, + pub package_loader: Option, pub install_rules: Vec, pub relative_file_exclusions: Option>, } @@ -113,14 +126,20 @@ pub struct R2MMModLoaderPackage { pub loader: String, } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct PackageInstaller { + pub name: String, + pub description: String, +} + #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct R2MMInstallRule { pub route: String, pub tracking_method: Option, - pub children: Option>, + pub sub_routes: Option>, pub default_file_extensions: Option>, - pub is_default_location: Option, + pub is_default_location: bool, } #[derive(Serialize, Deserialize, Debug, Clone)]