diff --git a/Cargo.toml b/Cargo.toml index dc2a731..e6575b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "anime-launcher-sdk" -version = "0.1.1" +version = "0.2.0" authors = ["Nikita Podvirnyy "] license = "GPL-3.0" readme = "README.md" @@ -20,6 +20,7 @@ enum-ordinalize = { version = "3.1", optional = true } wincompatlib = { version = "0.2", optional = true } lazy_static = { version = "1.4", optional = true } md-5 = { version = "0.10", features = ["asm"], optional = true } +discord-rich-presence = { version = "0.2.3", optional = true } [features] states = [] @@ -27,6 +28,7 @@ config = ["dep:serde", "dep:serde_json", "dep:enum-ordinalize"] components = ["dep:wincompatlib", "dep:lazy_static"] game = ["components", "config"] fps-unlocker = ["dep:md-5"] +discord-rpc = ["dep:discord-rich-presence"] default = ["all"] -all = ["states", "config", "components", "game", "fps-unlocker"] +all = ["states", "config", "components", "game", "fps-unlocker", "discord-rpc"] diff --git a/src/config/game/enhancements/mod.rs b/src/config/game/enhancements/mod.rs index 193fee1..cb8b93c 100644 --- a/src/config/game/enhancements/mod.rs +++ b/src/config/game/enhancements/mod.rs @@ -3,16 +3,22 @@ use serde_json::Value as JsonValue; pub mod fsr; pub mod hud; -pub mod fps_unlocker; pub mod gamescope; +#[cfg(feature = "fps-unlocker")] +pub mod fps_unlocker; + pub mod prelude { pub use super::gamescope::prelude::*; + + #[cfg(feature = "fps-unlocker")] pub use super::fps_unlocker::prelude::*; pub use super::Enhancements; pub use super::fsr::Fsr; pub use super::hud::HUD; + + #[cfg(feature = "fps-unlocker")] pub use super::fps_unlocker::FpsUnlocker; } @@ -23,7 +29,10 @@ pub struct Enhancements { pub fsr: Fsr, pub gamemode: bool, pub hud: HUD, + + #[cfg(feature = "fps-unlocker")] pub fps_unlocker: FpsUnlocker, + pub gamescope: Gamescope } @@ -47,6 +56,7 @@ impl From<&JsonValue> for Enhancements { None => default.hud }, + #[cfg(feature = "fps-unlocker")] fps_unlocker: match value.get("fps_unlocker") { Some(value) => FpsUnlocker::from(value), None => default.fps_unlocker diff --git a/src/config/launcher/discord_rpc.rs b/src/config/launcher/discord_rpc.rs new file mode 100644 index 0000000..59c026f --- /dev/null +++ b/src/config/launcher/discord_rpc.rs @@ -0,0 +1,57 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DiscordRpc { + pub app_id: u64, + pub enabled: bool, + pub title: String, + pub subtitle: String, + pub icon: String +} + +impl Default for DiscordRpc { + fn default() -> Self { + Self { + app_id: 901534333360304168, + enabled: false, + + title: String::from("Researching the world"), + subtitle: String::from("of Teyvat"), + icon: String::from("launcher") + } + } +} + +impl From<&JsonValue> for DiscordRpc { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + app_id: match value.get("app_id") { + Some(value) => value.as_u64().unwrap_or(default.app_id), + None => default.app_id + }, + + enabled: match value.get("enabled") { + Some(value) => value.as_bool().unwrap_or(default.enabled), + None => default.enabled + }, + + title: match value.get("title") { + Some(value) => value.as_str().unwrap_or(&default.title).to_string(), + None => default.title + }, + + subtitle: match value.get("subtitle") { + Some(value) => value.as_str().unwrap_or(&default.subtitle).to_string(), + None => default.subtitle + }, + + icon: match value.get("icon") { + Some(value) => value.as_str().unwrap_or(&default.icon).to_string(), + None => default.icon + } + } + } +} diff --git a/src/config/launcher/mod.rs b/src/config/launcher/mod.rs index 9d114d0..5e6380c 100644 --- a/src/config/launcher/mod.rs +++ b/src/config/launcher/mod.rs @@ -11,9 +11,15 @@ use crate::consts::launcher_dir; pub mod repairer; +#[cfg(feature = "discord-rpc")] +pub mod discord_rpc; + pub mod prelude { pub use super::Launcher; pub use super::repairer::Repairer; + + #[cfg(feature = "discord-rpc")] + pub use super::discord_rpc::DiscordRpc; } use prelude::*; @@ -33,7 +39,7 @@ impl Default for GameEdition { }) }); - if locale.len() > 4 && &locale[..5].to_lowercase() == "zh_cn" { + if locale.len() > 4 && &locale[..5].to_ascii_lowercase() == "zh_cn" { Self::China } else { Self::Global @@ -80,7 +86,10 @@ pub struct Launcher { pub speed_limit: u64, pub repairer: Repairer, pub edition: GameEdition, - pub style: LauncherStyle + pub style: LauncherStyle, + + #[cfg(feature = "discord-rpc")] + pub discord_rpc: DiscordRpc } impl Default for Launcher { @@ -91,7 +100,10 @@ impl Default for Launcher { speed_limit: 0, repairer: Repairer::default(), edition: GameEdition::default(), - style: LauncherStyle::default() + style: LauncherStyle::default(), + + #[cfg(feature = "discord-rpc")] + discord_rpc: DiscordRpc::default() } } } @@ -138,6 +150,12 @@ impl From<&JsonValue> for Launcher { style: match value.get("style") { Some(value) => serde_json::from_value(value.clone()).unwrap_or(default.style), None => default.style + }, + + #[cfg(feature = "discord-rpc")] + discord_rpc: match value.get("discord_rpc") { + Some(value) => DiscordRpc::from(value), + None => default.discord_rpc } } } diff --git a/src/discord_rpc.rs b/src/discord_rpc.rs new file mode 100644 index 0000000..e77b79c --- /dev/null +++ b/src/discord_rpc.rs @@ -0,0 +1,108 @@ +use std::thread::JoinHandle; +use std::sync::mpsc::{self, Sender, SendError}; + +use discord_rich_presence::{ + activity::*, + DiscordIpc, + DiscordIpcClient +}; + +use super::config::prelude::DiscordRpc as DiscordRpcConfig; + +#[derive(Debug, Clone)] +pub enum RpcUpdates { + /// Establish RPC connection + Connect, + + /// Terminate RPC connection. Panics if not connected + Disconnect, + + /// Update RPC activity + UpdateActivity { + title: String, + subtitle: String, + icon: String + }, + + /// Clear RPC activity + ClearActivity +} + +pub struct DiscordRpc { + _thread: JoinHandle<()>, + sender: Sender +} + +impl DiscordRpc { + pub fn new(mut config: DiscordRpcConfig) -> Self { + let (sender, receiver) = mpsc::channel(); + + Self { + _thread: std::thread::spawn(move || { + let mut client = DiscordIpcClient::new(&config.app_id.to_string()) + .expect("Failed to register discord ipc client"); + + let mut connected = false; + + while let Ok(update) = receiver.recv() { + match update { + RpcUpdates::Connect => { + if !connected { + connected = true; + + client.connect().expect("Failed to connect to discord"); + + client.set_activity(Self::get_activity(&config)) + .expect("Failed to update discord rpc activity"); + } + } + + RpcUpdates::Disconnect => { + if connected { + connected = false; + + client.close().expect("Failed to disconnect from discord"); + } + } + + RpcUpdates::UpdateActivity { title, subtitle, icon } => { + config.title = title; + config.subtitle = subtitle; + config.icon = icon; + + if connected { + client.set_activity(Self::get_activity(&config)) + .expect("Failed to update discord rpc activity"); + } + } + + RpcUpdates::ClearActivity => { + if connected { + client.clear_activity().expect("Failed to clear discord rpc activity"); + } + } + } + } + }), + sender + } + } + + pub fn get_activity(config: &DiscordRpcConfig) -> Activity { + Activity::new() + .details(&config.title) + .state(&config.subtitle) + .assets(Assets::new().large_image(&config.icon)) + } + + pub fn update(&self, update: RpcUpdates) -> Result<(), SendError> { + self.sender.send(update) + } +} + +impl Drop for DiscordRpc { + #[allow(unused_must_use)] + fn drop(&mut self) { + self.update(RpcUpdates::Disconnect); + } +} diff --git a/src/game/mod.rs b/src/game.rs similarity index 87% rename from src/game/mod.rs rename to src/game.rs index 51f9698..83b95de 100644 --- a/src/game/mod.rs +++ b/src/game.rs @@ -1,4 +1,4 @@ -use std::process::Command; +use std::process::{Command, Stdio}; use anime_game_core::genshin::telemetry; @@ -8,6 +8,9 @@ use super::config; #[cfg(feature = "fps-unlocker")] use super::fps_unlocker::FpsUnlocker; +#[cfg(feature = "discord-rpc")] +use super::discord_rpc::*; + /// Try to run the game /// /// If `debug = true`, then the game will be run in the new terminal window @@ -63,7 +66,7 @@ pub fn run() -> anyhow::Result<()> { }; // Generate FPS unlocker config file - if let Err(err) = unlocker.update_config(config.game.enhancements.fps_unlocker.config.clone()) { + if let Err(err) = unlocker.update_config(config.game.enhancements.fps_unlocker.config) { return Err(anyhow::anyhow!("Failed to update FPS unlocker config: {err}")); } @@ -150,5 +153,25 @@ pub fn run() -> anyhow::Result<()> { command.current_dir(config.game.path).spawn()?; + #[cfg(feature = "discord-rpc")] + if config.launcher.discord_rpc.enabled { + let rpc = DiscordRpc::new(config.launcher.discord_rpc); + + rpc.update(RpcUpdates::Connect)?; + + #[allow(unused_must_use)] + std::thread::spawn(move || { + while let Ok(output) = Command::new("ps").arg("-A").stdout(Stdio::piped()).output() { + let output = String::from_utf8_lossy(&output.stdout); + + if !output.contains("GenshinImpact.e") && !output.contains("unlocker.exe") { + break; + } + } + + rpc.update(RpcUpdates::Disconnect); + }); + } + Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index c0a080d..8e24cde 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,9 @@ pub mod game; #[cfg(feature = "fps-unlocker")] pub mod fps_unlocker; +#[cfg(feature = "discord-rpc")] +pub mod discord_rpc; + pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Check if specified binary is available