diff --git a/Readme.md b/Readme.md index 1deae2a..c10eda7 100644 --- a/Readme.md +++ b/Readme.md @@ -58,23 +58,24 @@ There is also an [AUR package](https://aur.archlinux.org/packages/steam-boilr-gu BoilR can import games from many platforms, but there are limits based -| Platforms | Windows | Linux (executable) | Linux (flatpak) | -| ------------------------------------------------------------------------------- | ------- | --------------------------- | ------------------------------------------------------------------------- | -| [Epic Games Store](https://www.epicgames.com/) | Yes | Yes, install through proton | Yes, install through proton | -| [Itch.io](https://itch.io/app) (Windows Games) | Yes | No | No | -| [Itch.io](https://itch.io/app) (Linux Games) | No | Yes | Yes | -| [Origin](https://www.origin.com) | Yes | Yes, install through proton | Yes, install through proton | -| [GOG](https://www.gog.com/galaxy) | Yes | No (Use Heroic or MiniGalaxy) | No (Use Heroic or MiniGalaxy) | -| [UPlay](https://ubisoftconnect.com) | Yes | No | No | -| [Lutris](https://github.com/lutris/lutris) (Flatpak) | No | Yes | Yes (make sure Lutris is shut down first) | -| [Lutris](https://github.com/lutris/lutris) (Non-Flatpak) | No | Yes | Yes | -| [Legendary](https://github.com/derrod/legendary) | No | Yes | Yes | -| [Rare](https://github.com/Dummerle/Rare/releases) | No | Yes | Yes | -| [Heroic Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher) | No | Yes | Yes | -| [Amazon Games](https://gaming.amazon.com) | Yes | No | No | -| [Flatpaks](https://flathub.org/) | No | Yes | Yes | -| [Bottles](https://usebottles.com/) | No | Yes | Yes | -| [MiniGalaxy](https://sharkwouter.github.io/minigalaxy/) | No | Yes | Yes | +| Platforms | Windows | Linux (executable) | Linux (flatpak) | +| ------------------------------------------------------------------------------- | ------- | ----------------------------- | ----------------------------------------- | +| [Epic Games Store](https://www.epicgames.com/) | Yes | Yes, install through proton | Yes, install through proton | +| [Itch.io](https://itch.io/app) (Windows Games) | Yes | No | No | +| [Itch.io](https://itch.io/app) (Linux Games) | No | Yes | Yes | +| [Origin](https://www.origin.com) | Yes | Yes, install through proton | Yes, install through proton | +| [GOG](https://www.gog.com/galaxy) | Yes | No (Use Heroic or MiniGalaxy) | No (Use Heroic or MiniGalaxy) | +| [UPlay](https://ubisoftconnect.com) | Yes | No | No | +| [Lutris](https://github.com/lutris/lutris) (Flatpak) | No | Yes | Yes (make sure Lutris is shut down first) | +| [Lutris](https://github.com/lutris/lutris) (Non-Flatpak) | No | Yes | Yes | +| [Legendary](https://github.com/derrod/legendary) | No | Yes | Yes | +| [Rare](https://github.com/Dummerle/Rare/releases) | No | Yes | Yes | +| [Heroic Launcher](https://github.com/Heroic-Games-Launcher/HeroicGamesLauncher) | No | Yes | Yes | +| [Amazon Games](https://gaming.amazon.com) | Yes | No | No | +| [Flatpaks](https://flathub.org/) | No | Yes | Yes | +| [Bottles](https://usebottles.com/) | No | Yes | Yes | +| [MiniGalaxy](https://sharkwouter.github.io/minigalaxy/) | No | Yes | Yes | +| [Playnite](https://playnite.link/) | Yes | No | No | ## Getting cover art for your shortcuts diff --git a/src/platforms/mod.rs b/src/platforms/mod.rs index 6442d13..a536c16 100644 --- a/src/platforms/mod.rs +++ b/src/platforms/mod.rs @@ -14,6 +14,10 @@ mod minigalaxy; #[cfg(not(target_family = "unix"))] mod amazon; +#[cfg(not(target_family = "unix"))] +mod playnite; + + mod gog; mod itch; diff --git a/src/platforms/platforms_load.rs b/src/platforms/platforms_load.rs index 13263f9..18bcf94 100644 --- a/src/platforms/platforms_load.rs +++ b/src/platforms/platforms_load.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use super::GamesPlatform; use crate::settings::load_setting_sections; -const PLATFORM_NAMES: [&str; 12] = [ +const PLATFORM_NAMES: [&str; 13] = [ "amazon", "bottles", "epic_games", @@ -17,6 +17,7 @@ const PLATFORM_NAMES: [&str; 12] = [ "origin", "uplay", "minigalaxy", + "playnite", ]; pub type Platforms = Vec>; @@ -32,8 +33,10 @@ pub fn load_platform, B: AsRef>( { //Windows only platforms use super::amazon::AmazonPlatform; + use super::playnite::PlaynitePlatform; match name { "amazon" => return load::(s), + "playnite" => return load::(s), _ => {} } } @@ -58,7 +61,7 @@ pub fn load_platform, B: AsRef>( } } - //Common platforms + //Common platforms use super::egs::EpicPlatform; use super::gog::GogPlatform; use super::itch::ItchPlatform; diff --git a/src/platforms/playnite/mod.rs b/src/platforms/playnite/mod.rs new file mode 100644 index 0000000..1495373 --- /dev/null +++ b/src/platforms/playnite/mod.rs @@ -0,0 +1,3 @@ +mod playnite_platform; +mod playnite_parser; +pub use playnite_platform::PlaynitePlatform; \ No newline at end of file diff --git a/src/platforms/playnite/playnite_parser.rs b/src/platforms/playnite/playnite_parser.rs new file mode 100644 index 0000000..31ed2ec --- /dev/null +++ b/src/platforms/playnite/playnite_parser.rs @@ -0,0 +1,37 @@ +use nom::{ + bytes::{ + complete::{take_until, take_while}, + streaming::take, + }, + character::is_alphanumeric, + multi::many0, + IResult, +}; + +pub(crate) struct NamesAndId { + pub(crate) name: String, + pub(crate) id: String, +} + +pub(crate) fn parse_db<'a>(content: &'a [u8]) -> nom::IResult<&'a [u8], Vec> { + many0(parse_game)(content) +} + +fn parse_game(i: &[u8]) -> nom::IResult<&[u8], NamesAndId> { + let (i, _taken) = take_until("_id")(i)?; + let (i, _taken) = take_until("Image")(i)?; + let (i, prefix_and_id) = take_until("\\")(i)?; + let id_bytes = prefix_and_id + .split(|b| *b == 0 as u8) + .last() + .unwrap_or_default(); + let id = String::from_utf8_lossy(id_bytes).to_string(); + + let (i, _taken) = take_until("InstallSizeGroup")(i)?; + let (i, _taken) = take_until("Name")(i)?; + let (i, _taken) = take(4usize)(i)?; + let (i, _taken) = take_while(|b| !is_alphanumeric(b))(i)?; + let (i, name_bytes) = take_while(|b| b != 0)(i)?; + let name = String::from_utf8_lossy(name_bytes).to_string(); + IResult::Ok((i, NamesAndId { id, name })) +} diff --git a/src/platforms/playnite/playnite_platform.rs b/src/platforms/playnite/playnite_platform.rs new file mode 100644 index 0000000..770b7ee --- /dev/null +++ b/src/platforms/playnite/playnite_platform.rs @@ -0,0 +1,119 @@ +use serde::{Deserialize, Serialize}; +use std::{ + env, + path::{Path, PathBuf}, +}; +use steam_shortcuts_util::{shortcut::ShortcutOwned, Shortcut}; + +use crate::platforms::{load_settings, to_shortcuts_simple, FromSettingsString, GamesPlatform}; + +use super::playnite_parser::parse_db; + +impl FromSettingsString for PlaynitePlatform { + fn from_settings_string>(s: S) -> Self { + PlaynitePlatform { + settings: load_settings(s), + } + } +} + +#[derive(Clone)] +pub struct PlaynitePlatform { + pub settings: PlayniteSettings, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct PlayniteSettings { + pub enabled: bool, +} + +impl Default for PlayniteSettings { + fn default() -> Self { + Self { enabled: true } + } +} + +impl GamesPlatform for PlaynitePlatform { + fn name(&self) -> &str { + "Playnite" + } + + fn code_name(&self) -> &str { + "playnite" + } + + fn enabled(&self) -> bool { + self.settings.enabled + } + + fn get_shortcut_info(&self) -> eyre::Result> { + to_shortcuts_simple(self.get_playnite_games()) + } + + fn get_settings_serilizable(&self) -> String { + toml::to_string(&self.settings).unwrap_or_default() + } + + fn render_ui(&mut self, ui: &mut egui::Ui) { + ui.heading("Playnite"); + ui.checkbox(&mut self.settings.enabled, "Import from Playnite"); + } +} + +impl PlaynitePlatform { + fn get_playnite_games(&self) -> eyre::Result> { + let mut res = vec![]; + let app_data_path = env::var("APPDATA")?; + let app_data_local_path = env::var("LOCALAPPDATA")?; + let launcher_path = Path::new(&app_data_local_path) + .join("Playnite") + .join("Playnite.DesktopApp.exe"); + if !launcher_path.exists() { + return Err(eyre::eyre!("Did not find Playnite installation")); + } + let launcher_path = launcher_path.to_string_lossy().to_string(); + let playnite_folder = Path::new(&app_data_path).join("Playnite"); + let games_file_path = playnite_folder.join("library").join("games.db"); + if games_file_path.exists() { + let games_bytes = std::fs::read(&games_file_path).unwrap(); + let (_, games) = parse_db(&games_bytes).map_err(|e| eyre::eyre!(e.to_string()))?; + for game in games { + res.push(PlayniteGame { + id: game.id, + launcher_path: launcher_path.clone().into(), + name: game.name, + }); + } + } + Ok(res) + } +} + +impl From for ShortcutOwned { + fn from(game: PlayniteGame) -> Self { + let launch = format!("--hidesplashscreen --start {}", game.id); + let exe = game.launcher_path.to_string_lossy().to_string(); + let start_dir = game + .launcher_path + .parent() + .unwrap_or_else(|| Path::new("")) + .to_string_lossy() + .to_string(); + Shortcut::new( + "0", + game.name.as_str(), + exe.as_str(), + start_dir.as_str(), + "", + "", + launch.as_str(), + ) + .to_owned() + } +} + +pub struct PlayniteGame { + pub name: String, + pub id: String, + pub launcher_path: PathBuf, +}