From c092ffba88761ac916d70af2d9d30eae4957f976 Mon Sep 17 00:00:00 2001 From: Ofacy Date: Mon, 29 May 2023 07:19:54 +0200 Subject: [PATCH] Add Ubisoft connect (Linux Proton) support (#346) * Added Ubisoft connect (Linux Proton) support * Update src/platforms/uplay/platform.rs Co-authored-by: Philip Kristoffersen * Update src/platforms/uplay/platform.rs Co-authored-by: Philip Kristoffersen * Fixed needing "find_subsequence". * Update platform.rs * Update platform.rs * Fix trying to use 'None' as a function. --------- Co-authored-by: Ofacy Co-authored-by: Philip Kristoffersen --- src/platforms/uplay/game.rs | 9 +- src/platforms/uplay/platform.rs | 188 ++++++++++++++++++++++++++++++-- 2 files changed, 189 insertions(+), 8 deletions(-) diff --git a/src/platforms/uplay/game.rs b/src/platforms/uplay/game.rs index bb88e61..e02a6bb 100644 --- a/src/platforms/uplay/game.rs +++ b/src/platforms/uplay/game.rs @@ -8,11 +8,18 @@ pub(crate) struct UplayGame { pub(crate) icon: String, pub(crate) id: String, pub(crate) launcher: PathBuf, + pub(crate) launcher_compat_folder: Option, + pub(crate) launch_id: u8, } impl From for ShortcutOwned { fn from(game: UplayGame) -> Self { - let launch = format!("\"uplay://launch/{}/0\"", game.id); + let launch = match game.launcher_compat_folder{ + Some(compat_folder) => format! + ("STEAM_COMPAT_DATA_PATH=\"{}\" %command% \"uplay://launch/{}/{}\"", + compat_folder.to_string_lossy(), game.id, game.launch_id), + None => format!("\"uplay://launch/{}/{}\"", game.id, game.launch_id) + }; let start_dir = game .launcher .parent() diff --git a/src/platforms/uplay/platform.rs b/src/platforms/uplay/platform.rs index 89e8e7b..19c6e1d 100644 --- a/src/platforms/uplay/platform.rs +++ b/src/platforms/uplay/platform.rs @@ -1,13 +1,18 @@ -#[cfg(target_os = "windows")] +//All of this is technically related to Ubisoft Connnect, not Ubisoft Play. + use std::path::Path; -#[cfg(target_os = "windows")] use std::path::PathBuf; +use std::io::Read; +use std::io::BufReader; +use std::fs::File; + use crate::platforms::load_settings; use crate::platforms::to_shortcuts_simple; use crate::platforms::FromSettingsString; use crate::platforms::GamesPlatform; use crate::platforms::ShortcutToImport; +use crate::platforms::NeedsPorton; use super::{game::UplayGame, settings::UplaySettings}; @@ -16,10 +21,27 @@ pub struct UplayPlatform { pub settings: UplaySettings, } +impl NeedsPorton for UplayGame { + #[cfg(target_os = "windows")] + fn needs_proton(&self, _platform: &UplayPlatform) -> bool { + false + } + + #[cfg(target_family = "unix")] + fn needs_proton(&self, _platform: &UplayPlatform) -> bool { + true + } + + fn create_symlinks(&self, _platform: &UplayPlatform) -> bool { + false + } +} + + fn get_uplay_games() -> eyre::Result> { #[cfg(target_family = "unix")] { - Err(eyre::format_err!("Uplay is not supported on Linux")) + get_games_from_proton() } #[cfg(target_os = "windows")] { @@ -27,8 +49,64 @@ fn get_uplay_games() -> eyre::Result> { } } +#[derive(Default)] +struct UplayPathData { + //~/.steam/steam/steamapps/compatdata/X/pfx/drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher/upc.exe + exe_path: PathBuf, + //~/.steam/steam/steamapps/compatdata/X/pfx/drive_c/Program Files (x86)/Ubisoft/Ubisoft Game Launcher/games/ + games_path: PathBuf, + //~/.steam/steam/steamapps/compatdata/X + compat_folder: Option, +} + + +#[cfg(target_family = "unix")] +fn get_launcher_path() -> eyre::Result { + let mut res = UplayPathData::default(); + if let Ok(home) = std::env::var("HOME") { + let compat_folder_path = Path::new(&home) + .join(".steam") + .join("steam") + .join("steamapps") + .join("compatdata"); + + if let Ok(compat_folder) = std::fs::read_dir(compat_folder_path) { + for dir in compat_folder.flatten() { + let uplay_exe_path = dir + .path() + .join("pfx") + .join("drive_c") + .join("Program Files (x86)") + .join("Ubisoft") + .join("Ubisoft Game Launcher") + .join("upc.exe"); + + let uplay_games = dir + .path() + .join("pfx") + .join("drive_c") + .join("Program Files (x86)") + .join("Ubisoft") + .join("Ubisoft Game Launcher") + .join("games"); + + if uplay_exe_path.exists() && uplay_games.exists() { + res.exe_path = uplay_exe_path; + res.games_path = uplay_games; + res.compat_folder = Some(dir.path()); + return Ok(res); + } + } + } + } + Err(eyre::eyre!( + "Could not find uplay launcher")) +} + + #[cfg(target_os = "windows")] -fn get_launcher_path() -> eyre::Result { +fn get_launcher_path() -> eyre::Result { + let mut res = UplayPathData::default(); use winreg::enums::*; use winreg::RegKey; @@ -37,7 +115,8 @@ fn get_launcher_path() -> eyre::Result { let launcher_dir: String = launcher_key.get_value("InstallDir")?; let path = Path::new(&launcher_dir).join("upc.exe"); if path.exists() { - Ok(path) + res.exe_path = path; + Ok(res) } else { Err(eyre::eyre!( "Could not find uplay launcher at path {:?}", @@ -54,7 +133,7 @@ fn get_games_from_winreg() -> eyre::Result> { let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); let mut games = vec![]; let mut installed_ids = vec![]; - let launcher_path = get_launcher_path()?; + let launcher_path = get_launcher_path()?.exe_path; if let Ok(installs) = hklm.open_subkey("SOFTWARE\\WOW6432Node\\Ubisoft\\Launcher\\Installs") { for i in installs.enum_keys().filter_map(|i| i.ok()) { @@ -82,6 +161,8 @@ fn get_games_from_winreg() -> eyre::Result> { icon, id, launcher: launcher_path.clone(), + launcher_compat_folder: None, + launch_id: 0 }) } } @@ -90,6 +171,99 @@ fn get_games_from_winreg() -> eyre::Result> { Ok(games) } +#[cfg(target_family = "unix")] +fn get_games_from_proton() -> eyre::Result> { + let mut games = vec![]; + + let launcher_path = get_launcher_path()?; + let parent = launcher_path.exe_path.parent().unwrap_or_else(|| Path::new("/")); + let file = File::open(parent + .join("cache") + .join("configuration") + .join("configurations"))?; + let mut reader = BufReader::new(file); + let mut buffer = Vec::new(); + + // Read file into vector. + reader.read_to_end(&mut buffer)?; + + let mut splits: Vec = Vec::new(); + + while !buffer.is_empty() { + let game_header = b"version: 2.0"; + let foundindex: usize = match buffer.windows(game_header.len()).position(|window| window == game_header) { + Some(index) => {index}, + None => {break;}, + }; + let (mut first, second) = buffer.split_at(foundindex); + if first.len() >= 14usize { + first = first.split_at(first.len()-14).0; + } + splits.push(unsafe {std::str::from_utf8_unchecked(first).to_string()}); + buffer = second.split_at("version: 2.0".len()).1.to_vec(); + } + + + + + for gameconfig in splits { + if !gameconfig.contains("executables:") {continue}; + if !gameconfig.contains("online:") {continue}; + if !gameconfig.contains("shortcut_name:") {continue}; + if !gameconfig.contains("register:") {continue}; + + let mut inonline = false; + let mut shortcut_name: String = "".to_string(); + let mut game_id: String = "".to_string(); + let mut icon_image: PathBuf = "".into(); + let mut launch_id = 0; + for line in gameconfig.split('\n') { + let trimed = line.trim(); + if trimed.starts_with("online:") { + inonline = true; + continue; + } + if trimed.starts_with("offline:") { + break; + } + if trimed.starts_with("icon_image: ") { + let split = trimed.split_at("icon_image: ".len()).1; + if split.is_empty() {break}; // invalid config. + icon_image = parent.join("data").join("games").join(split); + } + if !inonline {continue}; + if trimed.starts_with("- shortcut_name:") { + let split = trimed.split_at("- shortcut_name:".len()).1; + if split.is_empty() {break}; // invalid config. + shortcut_name = split.to_string(); + continue; + } + + if trimed.starts_with("register: ") { + let split = trimed.split_at("register: ".len()).1; + if split.is_empty() {break}; // invalid config. + game_id = split + .strip_prefix("HKEY_LOCAL_MACHINE\\SOFTWARE\\Ubisoft\\Launcher\\Installs\\").unwrap_or_default() + .strip_suffix("\\InstallDir").unwrap_or_default().to_string(); + continue; + } + + if trimed == "denuvo: yes" { + games.push(UplayGame { + name: shortcut_name.clone(), + icon: icon_image.to_string_lossy().to_string(), + id: game_id.clone(), + launcher: launcher_path.exe_path.clone(), + launcher_compat_folder: launcher_path.compat_folder.clone(), + launch_id, + }); + launch_id += 1; + } + } + } + Ok(games) +} + impl FromSettingsString for UplayPlatform { fn from_settings_string>(s: S) -> Self { UplayPlatform { @@ -123,4 +297,4 @@ impl GamesPlatform for UplayPlatform { fn code_name(&self) -> &str { "uplay" } -} +} \ No newline at end of file