From d0b76b664c8e8756826cea0ca55b76c66f6db9b1 Mon Sep 17 00:00:00 2001 From: Philip Kristoffersen Date: Thu, 17 Mar 2022 16:51:38 +0100 Subject: [PATCH] Add Heroic support (#46) --- gui/mainview.fl | 14 ++- src/defaultconfig.toml | 3 + src/heroic/heroic_game.rs | 49 ++++++++ src/heroic/heroic_platform.rs | 168 ++++++++++++++++++++++++++++ src/heroic/mod.rs | 7 ++ src/heroic/settings.rs | 6 + src/legendary/legendary_platform.rs | 24 ++-- src/main.rs | 1 + src/platform.rs | 2 +- src/settings.rs | 3 +- src/sync/synchronization.rs | 8 +- src/ui/ui.rs | 5 +- 12 files changed, 275 insertions(+), 15 deletions(-) create mode 100644 src/heroic/heroic_game.rs create mode 100644 src/heroic/heroic_platform.rs create mode 100644 src/heroic/mod.rs create mode 100644 src/heroic/settings.rs diff --git a/gui/mainview.fl b/gui/mainview.fl index c10d815..6fccf90 100644 --- a/gui/mainview.fl +++ b/gui/mainview.fl @@ -8,7 +8,7 @@ class UserInterface {open } { Fl_Window win { label {BoilR} open - xywh {1 1 500 525} type Double visible + xywh {1 1 500 575} type Double visible } { # ----- Steam ----- @@ -43,7 +43,7 @@ class UserInterface {open # ----- Legendary ----- Fl_Check_Button enable_legendary_checkbox { - label Legendary + label {Legendary} xywh {25 175 155 25} down_box DOWN_BOX } Fl_Input legendary_executable_input { @@ -106,16 +106,22 @@ class UserInterface {open xywh {175 425 300 25} align 5 } + # ----- Heroic ----- + Fl_Check_Button enable_heroic_checkbox { + label Heroic + xywh {25 475 155 25} down_box DOWN_BOX + } + # ----- Buttons ----- Fl_Button save_settings_button { label {Save Settings} - xywh {25 475 125 25} + xywh {25 525 125 25} } Fl_Button synchronize_button { label {Synchronize Games} - xywh {175 475 300 25} + xywh {175 525 300 25} } } } diff --git a/src/defaultconfig.toml b/src/defaultconfig.toml index 4c73c14..af119b8 100644 --- a/src/defaultconfig.toml +++ b/src/defaultconfig.toml @@ -29,4 +29,7 @@ create_symlinks = true [lutris] enabled = true +[heroic] +enabled = true + [steam] \ No newline at end of file diff --git a/src/heroic/heroic_game.rs b/src/heroic/heroic_game.rs new file mode 100644 index 0000000..9ed4223 --- /dev/null +++ b/src/heroic/heroic_game.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; +use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct HeroicGame { + pub app_name: String, + pub can_run_offline: bool, + pub title: String, + pub is_dlc: bool, + pub install_path: String, + pub executable: String, + pub config_folder: Option, + pub legendary_location: Option +} + +impl From for ShortcutOwned { + fn from(game: HeroicGame) -> Self { + + let legendary = game.legendary_location.unwrap_or("legendary".to_string()); + + let icon = format!("\"{}\\{}\"", game.install_path, game.executable); + let launch = match game.config_folder{ + Some(config_folder) => { + format!("env XDG_CONFIG_HOME={} {}", config_folder, legendary) + }, + None => { + format!("{}", legendary) + }, + }; + + let launch_options = format!("launch {}",game.app_name); + + let shortcut = Shortcut::new( + 0, + game.title.as_str(), + launch.as_str(), + "", + icon.as_str(), + "", + &launch_options.as_str() + ); + let mut owned_shortcut = shortcut.to_owned(); + owned_shortcut.tags.push("Heroic".to_owned()); + owned_shortcut.tags.push("Ready TO Play".to_owned()); + owned_shortcut.tags.push("Installed".to_owned()); + + owned_shortcut + } +} diff --git a/src/heroic/heroic_platform.rs b/src/heroic/heroic_platform.rs new file mode 100644 index 0000000..e1bd9de --- /dev/null +++ b/src/heroic/heroic_platform.rs @@ -0,0 +1,168 @@ +use super::{HeroicGame, HeroicSettings}; +use crate::platform::{Platform, SettingsValidity}; +use serde_json::from_str; +use std::error::Error; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; + +pub struct HeroicPlatform { + pub settings: HeroicSettings, +} + +#[cfg(target_family = "unix")] +enum InstallationMode { + FlatPak, + UserBin, +} + +#[cfg(target_family = "unix")] +fn get_legendary_location(install_mode: &InstallationMode) -> &'static str { + match install_mode{ + InstallationMode::FlatPak => "/var/lib/flatpak/app/com.heroicgameslauncher.hgl/current/active/files/bin/heroic/resources/app.asar.unpacked/build/bin/linux/legendary", + InstallationMode::UserBin => "/opt/Heroic/resources/app.asar.unpacked/build/bin/linux/legendary" + } +} + +#[cfg(target_family = "unix")] +fn get_config_folder(install_mode: &InstallationMode) -> Option { + match install_mode { + InstallationMode::FlatPak => { + let home_dir = std::env::var("HOME").unwrap_or("".to_string()); + Some( + Path::new(&home_dir) + .join(".var/app/com.heroicgameslauncher.hgl/config") + .to_string_lossy() + .to_string(), + ) + } + InstallationMode::UserBin => None, + } +} + +#[cfg(target_os = "windows")] +fn find_legendary_location() -> Option { + match heroic_folder_from_registry().or_else(heroic_folder_from_appdata) { + Some(heroic_folder) => { + let legendary_path = heroic_folder + .join("resources\\app.asar.unpacked\\build\\bin\\win32\\legendary.exe"); + if legendary_path.exists() { + Some(legendary_path.to_path_buf().to_string_lossy().to_string()) + } else { + None + } + } + None => None, + } +} + +#[cfg(target_os = "windows")] +fn heroic_folder_from_registry() -> Option { + use winreg::enums::*; + use winreg::RegKey; + + let hklm = RegKey::predef(HKEY_CURRENT_USER); + if let Ok(launcher) = hklm.open_subkey("Software\\035fb1f9-7381-565b-92bb-ed6b2a3b99ba") { + let path_string: Result = launcher.get_value("InstallLocation"); + if let Ok(path_string) = path_string { + //.join("resources/app.asar.unpacked/build/bin/win32/legendary.exe") + let path = Path::new(&path_string); + if path.exists() { + return Some(path.to_path_buf()); + } + } + } + None +} + +#[cfg(target_os = "windows")] +fn heroic_folder_from_appdata() -> Option { + let key = "APPDATA"; + match std::env::var(key) { + Ok(program_data) => { + let path = Path::new(&program_data).join("heroic"); + if path.exists() { + Some(path.to_path_buf()) + } else { + None + } + } + Err(_err) => None, + } +} + +#[cfg(target_family = "unix")] +fn get_shortcuts_from_install_mode( + install_mode: &InstallationMode, +) -> Result, Box> { + let legendary = get_legendary_location(install_mode); + let config_folder = get_config_folder(install_mode); + get_shortcuts_from_location(config_folder, legendary.to_string()) +} + +fn get_shortcuts_from_location( + config_folder: Option, + legendary: String, +) -> Result, Box> { + let output = if let Some(config_folder) = config_folder.clone() { + Command::new(&legendary) + .env("XDG_CONFIG_HOME", config_folder) + .arg("list-installed") + .arg("--json") + .output()? + } else { + Command::new(&legendary) + .arg("list-installed") + .arg("--json") + .output()? + }; + let json = String::from_utf8_lossy(&output.stdout); + let mut legendary_ouput: Vec = from_str(&json)?; + legendary_ouput.iter_mut().for_each(|mut game| { + game.config_folder = config_folder.clone(); + game.legendary_location = Some(legendary.to_string()); + }); + Ok(legendary_ouput) +} + +impl Platform> for HeroicPlatform { + fn enabled(&self) -> bool { + self.settings.enabled + } + + fn name(&self) -> &str { + "Heroic" + } + #[cfg(target_family = "unix")] + fn get_shortcuts(&self) -> Result, Box> { + let install_modes = vec![InstallationMode::FlatPak, InstallationMode::UserBin]; + let first_working_instal = install_modes + .iter() + .find_map(|install_mode| get_shortcuts_from_install_mode(install_mode).ok()); + match first_working_instal { + Some(res) => return Ok(res), + None => get_shortcuts_from_install_mode(&install_modes[0]), + } + } + + #[cfg(target_os = "windows")] + fn get_shortcuts(&self) -> Result, Box> { + let legendary = find_legendary_location().unwrap_or("legendary".to_string()); + get_shortcuts_from_location(None, legendary) + } + + #[cfg(target_family = "unix")] + fn create_symlinks(&self) -> bool { + false + } + + fn settings_valid(&self) -> crate::platform::SettingsValidity { + let shortcuts_res = self.get_shortcuts(); + match shortcuts_res { + Ok(_) => SettingsValidity::Valid, + Err(err) => SettingsValidity::Invalid { + reason: format!("{}", err), + }, + } + } +} diff --git a/src/heroic/mod.rs b/src/heroic/mod.rs new file mode 100644 index 0000000..cf69564 --- /dev/null +++ b/src/heroic/mod.rs @@ -0,0 +1,7 @@ +mod heroic_game; +mod heroic_platform; +mod settings; + +pub use heroic_game::*; +pub use heroic_platform::*; +pub use settings::*; diff --git a/src/heroic/settings.rs b/src/heroic/settings.rs new file mode 100644 index 0000000..2aa00d5 --- /dev/null +++ b/src/heroic/settings.rs @@ -0,0 +1,6 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct HeroicSettings { + pub enabled: bool, +} diff --git a/src/legendary/legendary_platform.rs b/src/legendary/legendary_platform.rs index 5e025a2..f0e3cdd 100644 --- a/src/legendary/legendary_platform.rs +++ b/src/legendary/legendary_platform.rs @@ -24,13 +24,13 @@ impl Platform> for LegendaryPlatform { } fn get_shortcuts(&self) -> Result, Box> { - let legendary_command = Command::new("legendary") - .arg("list-installed") - .arg("--json") - .output()?; - let json = String::from_utf8_lossy(&legendary_command.stdout); - let legendary_ouput = from_str(&json)?; - Ok(legendary_ouput) + let legendary_string = self + .settings + .executable + .clone() + .unwrap_or("legendary".to_string()); + let legendary = legendary_string.as_str(); + execute_legendary_command(legendary) } #[cfg(target_family = "unix")] @@ -48,3 +48,13 @@ impl Platform> for LegendaryPlatform { } } } + +fn execute_legendary_command(program: &str) -> Result, Box> { + let legendary_command = Command::new(program) + .arg("list-installed") + .arg("--json") + .output()?; + let json = String::from_utf8_lossy(&legendary_command.stdout); + let legendary_ouput = from_str(&json)?; + Ok(legendary_ouput) +} diff --git a/src/main.rs b/src/main.rs index 4503fc2..773b0e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod steam; mod steamgriddb; mod sync; mod uplay; +mod heroic; #[cfg(feature = "ui")] mod ui; diff --git a/src/platform.rs b/src/platform.rs index a92e50c..7211db4 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -1,4 +1,4 @@ -use steam_shortcuts_util::shortcut::ShortcutOwned; + use steam_shortcuts_util::shortcut::ShortcutOwned; pub trait Platform where diff --git a/src/settings.rs b/src/settings.rs index 8875fe3..a8669b4 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,7 +1,7 @@ use crate::{ egs::EpicGamesLauncherSettings, gog::GogSettings, itch::ItchSettings, legendary::LegendarySettings, lutris::settings::LutrisSettings, origin::OriginSettings, - steam::SteamSettings, steamgriddb::SteamGridDbSettings, uplay::UplaySettings, + steam::SteamSettings, steamgriddb::SteamGridDbSettings, uplay::UplaySettings, heroic::HeroicSettings, }; use config::{Config, ConfigError, Environment, File}; @@ -20,6 +20,7 @@ pub struct Settings { pub gog: GogSettings, pub uplay: UplaySettings, pub lutris: LutrisSettings, + pub heroic: HeroicSettings, } impl Settings { diff --git a/src/sync/synchronization.rs b/src/sync/synchronization.rs index efa47c4..5f53599 100644 --- a/src/sync/synchronization.rs +++ b/src/sync/synchronization.rs @@ -8,7 +8,7 @@ use crate::{ settings::Settings, steam::{get_shortcuts_for_user, get_shortcuts_paths, ShortcutInfo, SteamUsersInfo}, steamgriddb::download_images_for_users, - uplay::Uplay, + uplay::Uplay, heroic::HeroicPlatform, }; use std::error::Error; @@ -119,6 +119,12 @@ fn update_platforms(settings: &Settings, new_user_shortcuts: &mut Vec