diff --git a/Readme.md b/Readme.md index 21e5d85..91c312a 100644 --- a/Readme.md +++ b/Readme.md @@ -28,7 +28,7 @@ Optionally you can set BoilR up to automatically download artwork from [SteamGri - [x] [Lutris](https://github.com/lutris/lutris) - [x] [Legendary](https://github.com/derrod/legendary) - [x] [Rare](https://github.com/Dummerle/Rare/releases) -- [x] [Heroic Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher) (Only Linux & Epic Games for now) +- [x] [Heroic Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher) (Linux Only) - [ ] XBox/Microsoft Store integration diff --git a/src/gog/gog_game.rs b/src/gog/gog_game.rs index 8bbfd24..49e3bc8 100644 --- a/src/gog/gog_game.rs +++ b/src/gog/gog_game.rs @@ -23,15 +23,18 @@ pub(crate) struct PlayTask { pub task_type: String, #[serde(alias = "workingDir")] pub working_dir: Option, + pub arguments: Option, + } #[derive(Clone)] -pub(crate) struct GogShortcut { +pub struct GogShortcut { pub name: String, pub game_folder: String, pub path: String, pub working_dir: String, pub game_id: String, + pub arguments: String, } impl From for ShortcutOwned { @@ -44,14 +47,24 @@ impl From for ShortcutOwned { } else { exe.to_str().unwrap_or("").to_string() }; + let mut exe_string = exe.to_string_lossy().to_string(); + if exe_string.contains(" ") && !exe_string.starts_with("\""){ + exe_string = format!("\"{}\"",exe_string); + } + + let mut working_dir_string = gogs.working_dir; + if working_dir_string.contains(" ") && !working_dir_string.starts_with("\""){ + working_dir_string = format!("\"{}\"",working_dir_string); + } + let shortcut = Shortcut::new( "0", gogs.name.as_str(), - exe.to_str().unwrap(), - gogs.working_dir.as_str(), + &exe_string, + &working_dir_string, icon.as_str(), "", - "", + &gogs.arguments, ); let mut owned_shortcut = shortcut.to_owned(); owned_shortcut.tags.push("Gog".to_owned()); diff --git a/src/gog/gog_platform.rs b/src/gog/gog_platform.rs index 46abc5e..af31c49 100644 --- a/src/gog/gog_platform.rs +++ b/src/gog/gog_platform.rs @@ -12,6 +12,128 @@ pub struct GogPlatform { pub settings: GogSettings, } + + +pub fn get_shortcuts_from_config(wine_c_drive : Option, config_path: PathBuf) -> Result, GogErrors> { + let install_locations = get_install_locations(config_path)?; + #[cfg(target_family = "unix")] + let install_locations = if let Some(wine_c_drive) = &wine_c_drive { + fix_paths(wine_c_drive, install_locations) + } else { + install_locations + }; + let mut game_folders = vec![]; + for install_location in install_locations { + let path = Path::new(&install_location); + if path.exists() { + let dirs = path.read_dir(); + if let Ok(dirs) = dirs { + for dir in dirs.flatten() { + if let Ok(file_type) = dir.file_type() { + if file_type.is_dir() { + game_folders.push(dir.path()); + } + } + } + } + } + } + let shortcuts = get_shortcuts_from_game_folders(game_folders); + Ok(shortcuts) +} + +pub fn get_shortcuts_from_game_folders(game_folders: Vec) -> Vec { + let games = get_games_from_game_folders(game_folders); + let shortcuts = get_shortcuts_from_games(games); + shortcuts +} + +fn get_shortcuts_from_games(games: Vec<(GogGame, PathBuf)>) -> Vec { + let mut shortcuts = vec![]; + for (game, game_folder) in games { + + if let Some(folder_path) = game_folder.to_str() { + if let Some(tasks) = &game.play_tasks { + if let Some(primary_task) = tasks.iter().find(|t| { + t.is_primary.unwrap_or_default() + && t.task_type == "FileTask" + && t.category.as_ref().unwrap_or(&String::from("")) == "game" + }) { + if let Some(task_path) = &primary_task.path { + let full_path = game_folder.join(&task_path); + if let Some(full_path) = full_path.to_str() { + let folder_path = folder_path.to_string(); + + let working_dir = match &primary_task.working_dir { + Some(working_dir) => game_folder + .join(working_dir) + .to_str() + .unwrap_or_else(|| folder_path.as_str()) + .to_string(), + None => folder_path.to_string(), + }; + + #[cfg(target_family = "unix")] + let working_dir = working_dir.replace("\\", "/"); + + let full_path_string = full_path.to_string(); + + #[cfg(target_family = "unix")] + let full_path_string = full_path_string.replace("\\", "/"); + let arguments = primary_task.arguments.as_ref().unwrap_or(&"".to_string()).clone(); + let shortcut = GogShortcut { + name: game.name, + game_folder: folder_path, + working_dir, + game_id: game.game_id, + path: full_path_string, + arguments, + }; + shortcuts.push(shortcut); + } + } + } + } + } + } + shortcuts +} + +fn get_games_from_game_folders(game_folders: Vec) -> Vec<(GogGame, PathBuf)> { + let mut games = vec![]; + for game_folder in &game_folders { + let mut game_folder = game_folder; + let deep_game_folder = Path::new(&game_folder).join("game").to_path_buf(); + if deep_game_folder.exists() { + game_folder = &deep_game_folder; + } + if let Ok(files) = game_folder.read_dir() { + for file in files.flatten() { + if let Some(file_name) = file.file_name().to_str() { + if file_name.starts_with("goggame-") { + if let Some(extension) = file.path().extension() { + if let Some(extension) = extension.to_str() { + if extension == "info" { + // Finally we know we can parse this as a game + if let Ok(content) = std::fs::read_to_string(file.path()) { + if let Ok(gog_game) = + serde_json::from_str::(&content) + { + games.push((gog_game, game_folder.clone())); + } + } + } + } + } + } + } + } + } + } + games +} + + impl Platform for GogPlatform { fn enabled(&self) -> bool { self.settings.enabled @@ -40,103 +162,7 @@ impl Platform for GogPlatform { if !config_path.exists() { return Err(GogErrors::ConfigFileNotFound { path: config_path }); } - let install_locations = get_install_locations(config_path)?; - #[cfg(target_family = "unix")] - let install_locations = if let Some(wine_c_drive) = &self.settings.wine_c_drive { - fix_paths(wine_c_drive, install_locations) - } else { - install_locations - }; - - let mut game_folders = vec![]; - for install_location in install_locations { - let path = Path::new(&install_location); - if path.exists() { - let dirs = path.read_dir(); - if let Ok(dirs) = dirs { - for dir in dirs.flatten() { - if let Ok(file_type) = dir.file_type() { - if file_type.is_dir() { - game_folders.push(dir.path()); - } - } - } - } - } - } - let mut games = vec![]; - for game_folder in &game_folders { - if let Ok(files) = game_folder.read_dir() { - for file in files.flatten() { - if let Some(file_name) = file.file_name().to_str() { - if file_name.starts_with("goggame-") { - if let Some(extension) = file.path().extension() { - if let Some(extension) = extension.to_str() { - if extension == "info" { - // Finally we know we can parse this as a game - if let Ok(content) = std::fs::read_to_string(file.path()) { - if let Ok(gog_game) = - serde_json::from_str::(&content) - { - games.push((gog_game, game_folder)); - } - } - } - } - } - } - } - } - } - } - - let mut shortcuts = vec![]; - for (game, game_folder) in games { - if let Some(folder_path) = game_folder.to_str() { - if let Some(tasks) = &game.play_tasks { - if let Some(primary_task) = tasks.iter().find(|t| { - t.is_primary.unwrap_or_default() - && t.task_type == "FileTask" - && t.category.as_ref().unwrap_or(&String::from("")) == "game" - }) { - if let Some(task_path) = &primary_task.path { - let full_path = game_folder.join(&task_path); - if let Some(full_path) = full_path.to_str() { - let folder_path = folder_path.to_string(); - - let working_dir = match &primary_task.working_dir { - Some(working_dir) => game_folder - .join(working_dir) - .to_str() - .unwrap_or_else(|| folder_path.as_str()) - .to_string(), - None => folder_path.to_string(), - }; - - #[cfg(target_family = "unix")] - let working_dir = working_dir.replace("\\", "/"); - - let full_path_string = full_path.to_string(); - - #[cfg(target_family = "unix")] - let full_path_string = full_path_string.replace("\\", "/"); - - let shortcut = GogShortcut { - name: game.name, - game_folder: folder_path, - working_dir, - game_id: game.game_id, - path: full_path_string, - }; - shortcuts.push(shortcut); - } - } - } - } - } - } - - Ok(shortcuts) + get_shortcuts_from_config(self.settings.wine_c_drive.clone(),config_path) } fn settings_valid(&self) -> crate::platform::SettingsValidity { diff --git a/src/gog/mod.rs b/src/gog/mod.rs index 7fd4b36..f7b2980 100644 --- a/src/gog/mod.rs +++ b/src/gog/mod.rs @@ -5,3 +5,4 @@ mod gog_settings; pub use gog_platform::*; pub use gog_settings::*; +pub use gog_game::GogShortcut; diff --git a/src/heroic/heroic_game.rs b/src/heroic/heroic_game.rs index b441a4b..f4b55f6 100644 --- a/src/heroic/heroic_game.rs +++ b/src/heroic/heroic_game.rs @@ -13,6 +13,8 @@ pub struct HeroicGame { pub launch_parameters: String, } + + impl HeroicGame{ pub fn is_installed(&self) -> bool{ Path::new(&self.install_path).join(&self.executable).exists() diff --git a/src/heroic/heroic_game_type.rs b/src/heroic/heroic_game_type.rs new file mode 100644 index 0000000..d7668e5 --- /dev/null +++ b/src/heroic/heroic_game_type.rs @@ -0,0 +1,22 @@ +use steam_shortcuts_util::shortcut::ShortcutOwned; + +use crate::gog::GogShortcut; +use super::HeroicGame; + + +#[derive(Clone)] +pub enum HeroicGameType{ + Epic(HeroicGame), + //The bool is if it is windows (true) or not (false) + Gog(GogShortcut,bool) +} + + +impl From for ShortcutOwned { + fn from(heroic_game_type: HeroicGameType) -> Self { + match heroic_game_type{ + HeroicGameType::Epic(epic) => epic.into(), + HeroicGameType::Gog(gog,_) => gog.into(), + } + } +} diff --git a/src/heroic/heroic_platform.rs b/src/heroic/heroic_platform.rs index 492f1ec..a298537 100644 --- a/src/heroic/heroic_platform.rs +++ b/src/heroic/heroic_platform.rs @@ -1,4 +1,7 @@ -use super::{HeroicGame, HeroicSettings}; +use serde::{Deserialize}; + +use super::{HeroicGame, HeroicGameType, HeroicSettings}; +use crate::gog::{ get_shortcuts_from_game_folders}; use crate::platform::{Platform, SettingsValidity}; use std::collections::HashMap; use std::error::Error; @@ -12,7 +15,20 @@ pub struct HeroicPlatform { enum InstallationMode { FlatPak, - UserBin, + UserBin, +} + +#[derive(Deserialize)] +struct HeroicGogConfig { + installed: Vec, +} + +#[derive(Deserialize)] +struct HeroicGogPath { + platform: String, + #[serde(alias = "appName")] + app_name: String, + install_path: String, } fn get_installed_json_location(install_mode: &InstallationMode) -> PathBuf { @@ -25,7 +41,17 @@ fn get_installed_json_location(install_mode: &InstallationMode) -> PathBuf { .to_path_buf() } - +fn get_gog_installed_location(install_mode: &InstallationMode) -> PathBuf { + let home_dir = std::env::var("HOME").unwrap_or("".to_string()); + match install_mode { + InstallationMode::FlatPak => Path::new(&home_dir) + .join(".var/app/com.heroicgameslauncher.hgl/config/heroic/gog_store/installed.json"), + InstallationMode::UserBin => { + Path::new(&home_dir).join(".config/heroic/gog_store/installed.json") + } + } + .to_path_buf() +} fn get_shortcuts_from_install_mode( install_mode: &InstallationMode, @@ -48,7 +74,7 @@ fn get_shortcuts_from_location>(path: P) -> Result> for HeroicPlatform { +impl Platform> for HeroicPlatform { fn enabled(&self) -> bool { self.settings.enabled } @@ -56,19 +82,16 @@ impl Platform> for HeroicPlatform { fn name(&self) -> &str { "Heroic" } - fn get_shortcuts(&self) -> Result, Box> { + fn get_shortcuts(&self) -> Result, Box> { let install_modes = vec![InstallationMode::FlatPak, InstallationMode::UserBin]; - let mut shortcuts :Vec = install_modes - .iter() - .filter_map(|install_mode| get_shortcuts_from_install_mode(install_mode).ok()) - .flatten() - .filter(|s| s.is_installed()) - .collect(); - shortcuts.sort_by_key(|m| format!("{}-{}-{}",m.launch_parameters,m.executable,&m.app_name)); - shortcuts.dedup_by_key(|m| format!("{}-{}-{}",m.launch_parameters,m.executable,&m.app_name)); - - Ok(shortcuts) + let mut heroic_games = get_epic_games(&install_modes)?; + if let Ok(gog_games) = get_gog_games(&install_modes) { + heroic_games.extend(gog_games); + } else { + println!("Did not find any GOG games in heroic") + } + Ok(heroic_games) } #[cfg(target_family = "unix")] @@ -86,7 +109,75 @@ impl Platform> for HeroicPlatform { } } - fn needs_proton(&self, _input: &HeroicGame) -> bool { - return true; + fn needs_proton(&self, input: &HeroicGameType) -> bool { + match input{ + HeroicGameType::Epic(_) => true, + HeroicGameType::Gog(_, is_windows) => *is_windows, + } } } + +fn get_epic_games( + install_modes: &[InstallationMode], +) -> Result, Box> { + let mut shortcuts: Vec = install_modes + .iter() + .filter_map(|install_mode| get_shortcuts_from_install_mode(install_mode).ok()) + .flatten() + .filter(|s| s.is_installed()) + .collect(); + + shortcuts.sort_by_key(|m| format!("{}-{}-{}", m.launch_parameters, m.executable, &m.app_name)); + shortcuts.dedup_by_key(|m| format!("{}-{}-{}", m.launch_parameters, m.executable, &m.app_name)); + let mut epic_shortcuts = vec![]; + for shortcut in shortcuts { + epic_shortcuts.push(HeroicGameType::Epic(shortcut)); + } + Ok(epic_shortcuts) +} + +fn get_gog_games( + install_modes: &[InstallationMode], +) -> Result, Box> { + let gog_paths: Vec = install_modes + .iter() + .filter_map(|install_mode| { + let config = get_gog_installed_location(install_mode); + if config.exists() { + Some(config) + } else { + None + } + }) + .filter_map(|config_path| std::fs::read_to_string(config_path).ok()) + .filter_map(|config_string| serde_json::from_str::(&config_string).ok()) + .map(|config| config.installed) + .flatten() + .collect(); + + let mut is_windows_map = HashMap::new(); + + for path in gog_paths.iter() { + is_windows_map.insert(path.app_name.clone(), path.platform == "windows"); + } + + let game_folders = gog_paths + .iter() + .filter_map(|p| { + let path = Path::new(&p.install_path); + if path.exists() { + Some(path.to_path_buf()) + } else { + None + } + }) + .collect(); + let shortcuts = get_shortcuts_from_game_folders(game_folders); + let mut gog_shortcuts = vec![]; + for shortcut in shortcuts { + let is_windows = is_windows_map.get(&shortcut.game_id).unwrap_or(&false); + gog_shortcuts.push(HeroicGameType::Gog(shortcut, *is_windows)); + } + + Ok(gog_shortcuts) +} diff --git a/src/heroic/mod.rs b/src/heroic/mod.rs index e9d8eb2..7b94b10 100644 --- a/src/heroic/mod.rs +++ b/src/heroic/mod.rs @@ -1,8 +1,9 @@ mod heroic_game; mod heroic_platform; mod settings; - +mod heroic_game_type; pub use heroic_game::*; pub use heroic_platform::*; pub use settings::*; +pub use heroic_game_type::*; diff --git a/src/sync/synchronization.rs b/src/sync/synchronization.rs index 359d883..ae17755 100644 --- a/src/sync/synchronization.rs +++ b/src/sync/synchronization.rs @@ -106,9 +106,9 @@ fn fix_shortcut_icons( for shortcut in shortcuts { #[cfg(not(target_family = "unix"))] - let replace_icon = shortcut.icon.trim().eq(""); + let replace_icon = shortcut.icon.trim().eq("") || !Path::new(shortcut.icon.trim()).exists(); #[cfg(target_family = "unix")] - let replace_icon = shortcut.icon.trim().eq("") || shortcut.icon.eq(&shortcut.exe); + let replace_icon = shortcut.icon.trim().eq("") || !Path::new(shortcut.icon.trim()).exists() ||shortcut.icon.eq(&shortcut.exe); if replace_icon { let app_id = steam_shortcuts_util::app_id_generator::calculate_app_id( &shortcut.exe,