From ed246f079b8a635c97a7096997a5678752589402 Mon Sep 17 00:00:00 2001 From: Observer KRypt0n_ Date: Sat, 22 Apr 2023 23:31:00 +0200 Subject: [PATCH] feat(star-rail): added initial HSR support --- Cargo.toml | 7 +- src/lib.rs | 3 + src/star_rail/config/mod.rs | 58 +++++ src/star_rail/config/schema/components.rs | 60 +++++ .../config/schema/game/enhancements.rs | 40 ++++ src/star_rail/config/schema/game/mod.rs | 111 +++++++++ .../config/schema/launcher/discord_rpc.rs | 75 ++++++ src/star_rail/config/schema/launcher/mod.rs | 101 ++++++++ src/star_rail/config/schema/mod.rs | 132 +++++++++++ src/star_rail/config/schema/patch.rs | 78 +++++++ src/star_rail/consts.rs | 19 ++ src/star_rail/game.rs | 221 ++++++++++++++++++ src/star_rail/mod.rs | 10 + src/star_rail/states.rs | 134 +++++++++++ 14 files changed, 1046 insertions(+), 3 deletions(-) create mode 100644 src/star_rail/config/mod.rs create mode 100644 src/star_rail/config/schema/components.rs create mode 100644 src/star_rail/config/schema/game/enhancements.rs create mode 100644 src/star_rail/config/schema/game/mod.rs create mode 100644 src/star_rail/config/schema/launcher/discord_rpc.rs create mode 100644 src/star_rail/config/schema/launcher/mod.rs create mode 100644 src/star_rail/config/schema/mod.rs create mode 100644 src/star_rail/config/schema/patch.rs create mode 100644 src/star_rail/consts.rs create mode 100644 src/star_rail/game.rs create mode 100644 src/star_rail/mod.rs create mode 100644 src/star_rail/states.rs diff --git a/Cargo.toml b/Cargo.toml index 03ad0c3..1348d5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,11 +7,11 @@ readme = "README.md" edition = "2021" [dependencies.anime-game-core] -git = "https://github.com/an-anime-team/anime-game-core" -tag = "1.7.4" +# git = "https://github.com/an-anime-team/anime-game-core" +# tag = "1.7.4" features = ["all"] -# path = "../anime-game-core" # ! for dev purposes only +path = "../anime-game-core" # ! for dev purposes only [dependencies] anyhow = { version = "1.0", features = ["backtrace"] } @@ -31,6 +31,7 @@ discord-rich-presence = { version = "0.2.3", optional = true } [features] genshin = ["anime-game-core/genshin"] honkai = ["anime-game-core/honkai"] +star-rail = ["anime-game-core/star-rail"] # Common features states = [] diff --git a/src/lib.rs b/src/lib.rs index 6c8d79b..e863d3f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,9 @@ pub mod genshin; #[cfg(feature = "honkai")] pub mod honkai; +#[cfg(feature = "star-rail")] +pub mod star_rail; + #[cfg(feature = "config")] pub mod config; diff --git a/src/star_rail/config/mod.rs b/src/star_rail/config/mod.rs new file mode 100644 index 0000000..5057ffd --- /dev/null +++ b/src/star_rail/config/mod.rs @@ -0,0 +1,58 @@ +use std::path::PathBuf; + +pub mod schema; + +pub use schema::Schema; + +use crate::config::ConfigExt; +use crate::star_rail::consts::config_file; + +static mut CONFIG: Option = None; + +pub struct Config; + +impl ConfigExt for Config { + type Schema = schema::Schema; + + #[inline] + fn config_file() -> PathBuf { + config_file().expect("Failed to resolve config file path") + } + + #[inline] + fn default_schema() -> Self::Schema { + Self::Schema::default() + } + + #[inline] + fn serialize_schema(schema: Self::Schema) -> anyhow::Result { + Ok(serde_json::to_string_pretty(&schema)?) + } + + #[inline] + fn deserialize_schema>(schema: T) -> anyhow::Result { + Ok(Self::Schema::from(&serde_json::from_str(schema.as_ref())?)) + } + + #[inline] + fn clone_schema(schema: &Self::Schema) -> Self::Schema { + schema.clone() + } + + #[inline] + fn get() -> anyhow::Result { + unsafe { + match &CONFIG { + Some(config) => Ok(config.clone()), + None => Self::get_raw() + } + } + } + + #[inline] + fn update(schema: Self::Schema) { + unsafe { + CONFIG = Some(schema); + } + } +} diff --git a/src/star_rail/config/schema/components.rs b/src/star_rail/config/schema/components.rs new file mode 100644 index 0000000..d90b587 --- /dev/null +++ b/src/star_rail/config/schema/components.rs @@ -0,0 +1,60 @@ +use std::path::PathBuf; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use crate::star_rail::consts::launcher_dir; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Components { + pub path: PathBuf, + pub servers: Vec +} + +impl Default for Components { + #[inline] + fn default() -> Self { + let launcher_dir = launcher_dir().expect("Failed to get launcher dir"); + + Self { + path: launcher_dir.join("components"), + servers: vec![ + String::from("https://github.com/an-anime-team/components") + ] + } + } +} + +impl From<&JsonValue> for Components { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + path: match value.get("path") { + Some(value) => match value.as_str() { + Some(value) => PathBuf::from(value), + None => default.path + }, + None => default.path + }, + + servers: match value.get("servers") { + Some(value) => match value.as_array() { + Some(values) => { + let mut servers = Vec::new(); + + for value in values { + if let Some(server) = value.as_str() { + servers.push(server.to_string()); + } + } + + servers + }, + None => default.servers + }, + None => default.servers + } + } + } +} diff --git a/src/star_rail/config/schema/game/enhancements.rs b/src/star_rail/config/schema/game/enhancements.rs new file mode 100644 index 0000000..b2c7824 --- /dev/null +++ b/src/star_rail/config/schema/game/enhancements.rs @@ -0,0 +1,40 @@ +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use crate::config::schema_blanks::prelude::*; + +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Enhancements { + pub fsr: Fsr, + pub gamemode: bool, + pub hud: HUD, + pub gamescope: Gamescope +} + +impl From<&JsonValue> for Enhancements { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + fsr: match value.get("fsr") { + Some(value) => Fsr::from(value), + None => default.fsr + }, + + gamemode: match value.get("gamemode") { + Some(value) => value.as_bool().unwrap_or(default.gamemode), + None => default.gamemode + }, + + hud: match value.get("hud") { + Some(value) => HUD::from(value), + None => default.hud + }, + + gamescope: match value.get("gamescope") { + Some(value) => Gamescope::from(value), + None => default.gamescope + } + } + } +} diff --git a/src/star_rail/config/schema/game/mod.rs b/src/star_rail/config/schema/game/mod.rs new file mode 100644 index 0000000..2323804 --- /dev/null +++ b/src/star_rail/config/schema/game/mod.rs @@ -0,0 +1,111 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use crate::config::schema_blanks::prelude::*; +use crate::star_rail::consts::launcher_dir; + +crate::config_impl_wine_schema!(launcher_dir); +crate::config_impl_dxvk_schema!(launcher_dir); + +pub mod enhancements; + +pub mod prelude { + pub use super::Wine; + pub use super::Dxvk; + + pub use super::enhancements::Enhancements; +} + +use prelude::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Game { + pub path: PathBuf, + pub wine: Wine, + pub dxvk: Dxvk, + pub enhancements: Enhancements, + pub environment: HashMap, + pub command: Option +} + +impl Default for Game { + #[inline] + fn default() -> Self { + let launcher_dir = launcher_dir().expect("Failed to get launcher dir"); + + Self { + path: launcher_dir.join(concat!("Hon", "kai Sta", "r Rail")), + wine: Wine::default(), + dxvk: Dxvk::default(), + enhancements: Enhancements::default(), + environment: HashMap::new(), + command: None + } + } +} + +impl From<&JsonValue> for Game { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + path: match value.get("path") { + Some(value) => match value.as_str() { + Some(value) => PathBuf::from(value), + None => default.path + }, + None => default.path + }, + + wine: match value.get("wine") { + Some(value) => Wine::from(value), + None => default.wine + }, + + dxvk: match value.get("dxvk") { + Some(value) => Dxvk::from(value), + None => default.dxvk + }, + + enhancements: match value.get("enhancements") { + Some(value) => Enhancements::from(value), + None => default.enhancements + }, + + environment: match value.get("environment") { + Some(value) => match value.as_object() { + Some(values) => { + let mut vars = HashMap::new(); + + for (name, value) in values { + if let Some(value) = value.as_str() { + vars.insert(name.clone(), value.to_string()); + } + } + + vars + }, + None => default.environment + }, + None => default.environment + }, + + command: match value.get("command") { + Some(value) => { + if value.is_null() { + None + } else { + match value.as_str() { + Some(value) => Some(value.to_string()), + None => default.command + } + } + }, + None => default.command + } + } + } +} diff --git a/src/star_rail/config/schema/launcher/discord_rpc.rs b/src/star_rail/config/schema/launcher/discord_rpc.rs new file mode 100644 index 0000000..7e787e4 --- /dev/null +++ b/src/star_rail/config/schema/launcher/discord_rpc.rs @@ -0,0 +1,75 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; + +use crate::discord_rpc::DiscordRpcParams; + +#[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 From for DiscordRpcParams { + #[inline] + fn from(config: DiscordRpc) -> Self { + Self { + app_id: config.app_id, + enabled: config.enabled, + title: config.title, + subtitle: config.subtitle, + icon: config.icon + } + } +} + +// TODO: add honkers-specific discord rpc + +impl Default for DiscordRpc { + #[inline] + 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/star_rail/config/schema/launcher/mod.rs b/src/star_rail/config/schema/launcher/mod.rs new file mode 100644 index 0000000..a50dd56 --- /dev/null +++ b/src/star_rail/config/schema/launcher/mod.rs @@ -0,0 +1,101 @@ +use std::path::PathBuf; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use enum_ordinalize::Ordinalize; + +use crate::config::schema_blanks::prelude::*; +use crate::star_rail::consts::launcher_dir; + +#[cfg(feature = "discord-rpc")] +pub mod discord_rpc; + +pub mod prelude { + #[cfg(feature = "discord-rpc")] + pub use super::discord_rpc::DiscordRpc; +} + +use prelude::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Ordinalize, Serialize, Deserialize)] +pub enum LauncherStyle { + Modern, + Classic +} + +impl Default for LauncherStyle { + #[inline] + fn default() -> Self { + Self::Modern + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Launcher { + pub language: String, + pub style: LauncherStyle, + pub temp: Option, + pub repairer: Repairer, + + #[cfg(feature = "discord-rpc")] + pub discord_rpc: DiscordRpc +} + +impl Default for Launcher { + #[inline] + fn default() -> Self { + Self { + language: String::from("en-us"), + style: LauncherStyle::default(), + temp: launcher_dir().ok(), + repairer: Repairer::default(), + + #[cfg(feature = "discord-rpc")] + discord_rpc: DiscordRpc::default() + } + } +} + +impl From<&JsonValue> for Launcher { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + language: match value.get("language") { + Some(value) => value.as_str().unwrap_or(&default.language).to_string(), + None => default.language + }, + + style: match value.get("style") { + Some(value) => serde_json::from_value(value.to_owned()).unwrap_or_default(), + None => default.style + }, + + temp: match value.get("temp") { + Some(value) => { + if value.is_null() { + None + } else { + match value.as_str() { + Some(value) => Some(PathBuf::from(value)), + None => default.temp + } + } + }, + None => default.temp + }, + + repairer: match value.get("repairer") { + Some(value) => Repairer::from(value), + None => default.repairer + }, + + #[cfg(feature = "discord-rpc")] + discord_rpc: match value.get("discord_rpc") { + Some(value) => DiscordRpc::from(value), + None => default.discord_rpc + } + } + } +} diff --git a/src/star_rail/config/schema/mod.rs b/src/star_rail/config/schema/mod.rs new file mode 100644 index 0000000..a5877be --- /dev/null +++ b/src/star_rail/config/schema/mod.rs @@ -0,0 +1,132 @@ +use std::path::PathBuf; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use wincompatlib::prelude::*; + +#[cfg(feature = "sandbox")] +use crate::config::schema_blanks::sandbox::Sandbox; + +#[cfg(feature = "components")] +use crate::components::wine::{ + WincompatlibWine, + Version as WineVersion +}; + +#[cfg(feature = "components")] +use crate::components::dxvk::Version as DxvkVersion; + +pub mod launcher; +pub mod game; +pub mod patch; + +#[cfg(feature = "components")] +pub mod components; + +pub mod prelude { + pub use super::launcher::prelude::*; + pub use super::game::prelude::*; + + pub use super::launcher::*; + pub use super::game::*; + pub use super::patch::*; + + #[cfg(feature = "components")] + pub use super::components::*; +} + +use prelude::*; + +#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Schema { + pub launcher: Launcher, + pub game: Game, + + #[cfg(feature = "sandbox")] + pub sandbox: Sandbox, + + #[cfg(feature = "components")] + pub components: Components, + + pub patch: Patch +} + +impl From<&JsonValue> for Schema { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + launcher: match value.get("launcher") { + Some(value) => Launcher::from(value), + None => default.launcher + }, + + game: match value.get("game") { + Some(value) => Game::from(value), + None => default.game + }, + + #[cfg(feature = "sandbox")] + sandbox: match value.get("sandbox") { + Some(value) => Sandbox::from(value), + None => default.sandbox + }, + + #[cfg(feature = "components")] + components: match value.get("components") { + Some(value) => Components::from(value), + None => default.components + }, + + patch: match value.get("patch") { + Some(value) => Patch::from(value), + None => default.patch + }, + } + } +} + +impl Schema { + #[cfg(feature = "components")] + /// Get selected wine version + pub fn get_selected_wine(&self) -> anyhow::Result> { + match &self.game.wine.selected { + Some(selected) => WineVersion::find_in(&self.components.path, selected), + None => Ok(None) + } + } + + #[cfg(feature = "components")] + /// Get selected dxvk version + pub fn get_selected_dxvk(&self) -> anyhow::Result> { + match wincompatlib::dxvk::Dxvk::get_version(&self.game.wine.prefix)? { + Some(version) => DxvkVersion::find_in(&self.components.path, version), + None => Ok(None) + } + } + + #[cfg(feature = "components")] + /// Resolve real wine prefix path using wincompatlib + /// + /// - For general wine build returns `game.wine.prefix` + /// - For proton-like builds return `game.wine.prefix`/`pfx` + pub fn get_wine_prefix_path(&self) -> PathBuf { + if let Ok(Some(wine)) = self.get_selected_wine() { + let wine = wine + .to_wine(&self.components.path, Some(&self.game.wine.builds.join(&wine.name))) + .with_prefix(&self.game.wine.prefix); + + let prefix = match wine { + WincompatlibWine::Default(wine) => wine.prefix, + WincompatlibWine::Proton(proton) => proton.wine().prefix.clone() + }; + + if let Some(prefix) = prefix { + return prefix; + } + } + + self.game.wine.prefix.clone() + } +} diff --git a/src/star_rail/config/schema/patch.rs b/src/star_rail/config/schema/patch.rs new file mode 100644 index 0000000..9a20a99 --- /dev/null +++ b/src/star_rail/config/schema/patch.rs @@ -0,0 +1,78 @@ +use std::path::PathBuf; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use crate::star_rail::consts::launcher_dir; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Patch { + pub path: PathBuf, + pub servers: Vec, + pub apply_xlua: bool, + pub root: bool +} + +impl Default for Patch { + #[inline] + fn default() -> Self { + let launcher_dir = launcher_dir().expect("Failed to get launcher dir"); + + Self { + path: launcher_dir.join("patch"), + + servers: vec![ + String::from("https://notabug.org/Krock/dawn") + ], + + apply_xlua: true, + + // Disable root requirement for patching if we're running launcher in flatpak + root: !PathBuf::from("/.flatpak-info").exists() + } + } +} + +impl From<&JsonValue> for Patch { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + path: match value.get("path") { + Some(value) => match value.as_str() { + Some(value) => PathBuf::from(value), + None => default.path + }, + None => default.path + }, + + servers: match value.get("servers") { + Some(value) => match value.as_array() { + Some(values) => { + let mut servers = Vec::new(); + + for value in values { + if let Some(server) = value.as_str() { + servers.push(server.to_string()); + } + } + + servers + }, + None => default.servers + }, + None => default.servers + }, + + apply_xlua: match value.get("apply_xlua") { + Some(value) => value.as_bool().unwrap_or(default.apply_xlua), + None => default.apply_xlua + }, + + root: match value.get("root") { + Some(value) => value.as_bool().unwrap_or(default.root), + None => default.root + } + } + } +} diff --git a/src/star_rail/consts.rs b/src/star_rail/consts.rs new file mode 100644 index 0000000..ed40555 --- /dev/null +++ b/src/star_rail/consts.rs @@ -0,0 +1,19 @@ +use std::path::PathBuf; + +/// Get default launcher dir path +/// +/// `$HOME/.local/share/honkers-railway-launcher` +#[inline] +pub fn launcher_dir() -> anyhow::Result { + Ok(std::env::var("XDG_DATA_HOME") + .or_else(|_| std::env::var("HOME").map(|home| home + "/.local/share")) + .map(|home| PathBuf::from(home).join("honkers-railway-launcher"))?) +} + +/// Get default config file path +/// +/// `$HOME/.local/share/honkers-railway-launcher/config.json` +#[inline] +pub fn config_file() -> anyhow::Result { + launcher_dir().map(|dir| dir.join("config.json")) +} diff --git a/src/star_rail/game.rs b/src/star_rail/game.rs new file mode 100644 index 0000000..a0a30ba --- /dev/null +++ b/src/star_rail/game.rs @@ -0,0 +1,221 @@ +use std::process::{Command, Stdio}; +use std::path::PathBuf; + +use anime_game_core::star_rail::telemetry; + +use crate::config::ConfigExt; +use crate::star_rail::config::Config; + +use crate::star_rail::consts; + +#[cfg(feature = "discord-rpc")] +use crate::discord_rpc::*; + +#[derive(Debug, Clone)] +struct Folders { + pub wine: PathBuf, + pub prefix: PathBuf, + pub game: PathBuf, + pub temp: PathBuf +} + +fn replace_keywords(command: impl ToString, folders: &Folders) -> String { + command.to_string() + .replace("%build%", folders.wine.to_str().unwrap()) + .replace("%prefix%", folders.prefix.to_str().unwrap()) + .replace("%temp%", folders.game.to_str().unwrap()) + .replace("%launcher%", &consts::launcher_dir().unwrap().to_string_lossy()) + .replace("%game%", folders.temp.to_str().unwrap()) +} + +/// Try to run the game +/// +/// This function will freeze thread it was called from while the game is running +#[tracing::instrument(level = "info", ret)] +pub fn run() -> anyhow::Result<()> { + tracing::info!("Preparing to run the game"); + + let config = Config::get()?; + + if !config.game.path.exists() { + return Err(anyhow::anyhow!("Game is not installed")); + } + + let Some(wine) = config.get_selected_wine()? else { + anyhow::bail!("Couldn't find wine executable"); + }; + + let features = wine.features(&config.components.path)?.unwrap_or_default(); + + let mut folders = Folders { + wine: config.game.wine.builds.join(&wine.name), + prefix: config.game.wine.prefix.clone(), + game: config.game.path.clone(), + temp: config.launcher.temp.clone().unwrap_or(std::env::temp_dir()) + }; + + // Check telemetry servers + + tracing::info!("Checking telemetry"); + + if let Ok(Some(server)) = telemetry::is_disabled() { + return Err(anyhow::anyhow!("Telemetry server is not disabled: {server}")); + } + + // Prepare bash -c '' + + let mut bash_command = String::new(); + let mut windows_command = String::new(); + + if config.game.enhancements.gamemode { + bash_command += "gamemoderun "; + } + + let run_command = features.command + .map(|command| replace_keywords(command, &folders)) + .unwrap_or(format!("'{}'", folders.wine.join(wine.files.wine64.unwrap_or(wine.files.wine)).to_string_lossy())); + + bash_command += &run_command; + bash_command += " "; + + if let Some(virtual_desktop) = config.game.wine.virtual_desktop.get_command("an_anime_game") { + windows_command += &virtual_desktop; + windows_command += " "; + } + + windows_command += "launcher.bat "; + + if config.game.wine.borderless { + windows_command += "-screen-fullscreen 0 -popupwindow "; + } + + // https://notabug.org/Krock/dawn/src/master/TWEAKS.md + if config.game.enhancements.fsr.enabled { + windows_command += "-window-mode exclusive "; + } + + // gamescope -- + if let Some(gamescope) = config.game.enhancements.gamescope.get_command() { + bash_command = format!("{gamescope} -- {bash_command}"); + } + + // bwrap -- + #[cfg(feature = "sandbox")] + if config.sandbox.enabled { + let bwrap = config.sandbox.get_command( + folders.wine.to_str().unwrap(), + folders.prefix.to_str().unwrap(), + folders.game.to_str().unwrap() + ); + + let sandboxed_folders = Folders { + wine: PathBuf::from("/tmp/sandbox/wine"), + prefix: PathBuf::from("/tmp/sandbox/prefix"), + game: PathBuf::from("/tmp/sandbox/game"), + temp: PathBuf::from("/tmp") + }; + + bash_command = bash_command + .replace(folders.wine.to_str().unwrap(), sandboxed_folders.wine.to_str().unwrap()) + .replace(folders.prefix.to_str().unwrap(), sandboxed_folders.prefix.to_str().unwrap()) + .replace(folders.game.to_str().unwrap(), sandboxed_folders.game.to_str().unwrap()) + .replace(folders.temp.to_str().unwrap(), sandboxed_folders.temp.to_str().unwrap()); + + bash_command = format!("{bwrap} --chdir /tmp/sandbox/game -- {bash_command}"); + folders = sandboxed_folders; + } + + // Bundle all windows arguments used to run the game into a single file + if features.compact_launch { + std::fs::write(folders.game.join("compact_launch.bat"), format!("start {windows_command}\nexit"))?; + + windows_command = String::from("compact_launch.bat"); + } + + // Finalize launching command + bash_command = match &config.game.command { + // Use user-given launch command + Some(command) => replace_keywords(command, &folders) + .replace("%command%", &format!("{bash_command} {windows_command}")) + .replace("%bash_command%", &bash_command) + .replace("%windows_command%", &windows_command), + + // Combine bash and windows parts of the command + None => format!("{bash_command} {windows_command}") + }; + + let mut command = Command::new("bash"); + + command.arg("-c"); + command.arg(&bash_command); + + // Setup environment + + command.env("WINEARCH", "win64"); + command.env("WINEPREFIX", &folders.prefix); + + // Add environment flags for selected wine + for (key, value) in features.env.into_iter() { + command.env(key, replace_keywords(value, &folders)); + } + + // Add environment flags for selected dxvk + if let Ok(Some(dxvk )) = config.get_selected_dxvk() { + if let Ok(Some(features)) = dxvk.features(&config.components.path) { + for (key, value) in features.env.iter() { + command.env(key, replace_keywords(value, &folders)); + } + } + } + + command.envs(config.game.wine.sync.get_env_vars()); + command.envs(config.game.enhancements.hud.get_env_vars(config.game.enhancements.gamescope.enabled)); + command.envs(config.game.enhancements.fsr.get_env_vars()); + command.envs(config.game.wine.language.get_env_vars()); + + command.envs(config.game.environment); + + // Run command + + let variables = command + .get_envs() + .map(|(key, value)| format!("{}=\"{}\"", key.to_string_lossy(), value.unwrap_or_default().to_string_lossy())) + .fold(String::new(), |acc, env| acc + " " + &env); + + tracing::info!("Running the game with command: {variables} bash -c \"{bash_command}\""); + + // We use real current dir here because sandboxed one + // obviously doesn't exist + command.current_dir(&config.game.path) + .spawn()?.wait_with_output()?; + + #[cfg(feature = "discord-rpc")] + let rpc = if config.launcher.discord_rpc.enabled { + Some(DiscordRpc::new(config.launcher.discord_rpc.into())) + } else { + None + }; + + #[cfg(feature = "discord-rpc")] + if let Some(rpc) = &rpc { + rpc.update(RpcUpdates::Connect)?; + } + + loop { + std::thread::sleep(std::time::Duration::from_secs(3)); + + let 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; + } + } + + #[cfg(feature = "discord-rpc")] + if let Some(rpc) = &rpc { + rpc.update(RpcUpdates::Disconnect)?; + } + + Ok(()) +} diff --git a/src/star_rail/mod.rs b/src/star_rail/mod.rs new file mode 100644 index 0000000..6138c07 --- /dev/null +++ b/src/star_rail/mod.rs @@ -0,0 +1,10 @@ +pub mod consts; + +#[cfg(feature = "config")] +pub mod config; + +#[cfg(feature = "states")] +pub mod states; + +#[cfg(feature = "game")] +pub mod game; diff --git a/src/star_rail/states.rs b/src/star_rail/states.rs new file mode 100644 index 0000000..777626d --- /dev/null +++ b/src/star_rail/states.rs @@ -0,0 +1,134 @@ +use std::path::PathBuf; + +use serde::{Serialize, Deserialize}; + +use anime_game_core::prelude::*; +use anime_game_core::star_rail::prelude::*; + +use crate::config::ConfigExt; +use crate::star_rail::config::Config; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum LauncherState { + Launch, + + /// Always contains `VersionDiff::Predownload` + PredownloadAvailable(VersionDiff), + + MainPatchAvailable(MainPatch), + + #[cfg(feature = "components")] + WineNotInstalled, + + PrefixNotExists, + + // Always contains `VersionDiff::Diff` + GameUpdateAvailable(VersionDiff), + + /// Always contains `VersionDiff::Outdated` + GameOutdated(VersionDiff), + + /// Always contains `VersionDiff::NotInstalled` + GameNotInstalled(VersionDiff) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum StateUpdating { + Game, + Patch +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LauncherStateParams { + pub wine_prefix: PathBuf, + pub game_path: PathBuf, + + pub patch_servers: Vec, + pub patch_folder: PathBuf, + + pub status_updater: F +} + +impl LauncherState { + pub fn get(params: LauncherStateParams) -> anyhow::Result { + tracing::debug!("Trying to get launcher state"); + + // Check prefix existence + if !params.wine_prefix.join("drive_c").exists() { + return Ok(Self::PrefixNotExists); + } + + // Check game installation status + (params.status_updater)(StateUpdating::Game); + + let game = Game::new(¶ms.game_path); + let diff = game.try_get_diff()?; + + match diff { + VersionDiff::Latest(_) | VersionDiff::Predownload { .. } => { + // Check game patch status + (params.status_updater)(StateUpdating::Patch); + + let patch = Patch::new(¶ms.patch_folder); + + // Sync local patch folder with remote if needed + // TODO: maybe I shouldn't do it here? + if patch.is_sync(¶ms.patch_servers)?.is_none() { + for server in ¶ms.patch_servers { + if patch.sync(server).is_ok() { + break; + } + } + } + + // Check the main patch + let main_patch = patch.main_patch()?; + + if !main_patch.is_applied(¶ms.game_path)? { + return Ok(Self::MainPatchAvailable(main_patch)); + } + + // Check if update predownload available + if let VersionDiff::Predownload { .. } = diff { + Ok(Self::PredownloadAvailable(diff)) + } + + // Otherwise we can launch the game + else { + Ok(Self::Launch) + } + } + + VersionDiff::Diff { .. } => Ok(Self::GameUpdateAvailable(diff)), + VersionDiff::Outdated { .. } => Ok(Self::GameOutdated(diff)), + VersionDiff::NotInstalled { .. } => Ok(Self::GameNotInstalled(diff)) + } + } + + #[cfg(feature = "config")] + #[tracing::instrument(level = "debug", skip(status_updater), ret)] + pub fn get_from_config(status_updater: T) -> anyhow::Result { + tracing::debug!("Trying to get launcher state"); + + let config = Config::get()?; + + match &config.game.wine.selected { + #[cfg(feature = "components")] + Some(selected) if !config.game.wine.builds.join(selected).exists() => return Ok(Self::WineNotInstalled), + + None => return Ok(Self::WineNotInstalled), + + _ => () + } + + Self::get(LauncherStateParams { + wine_prefix: config.get_wine_prefix_path(), + game_path: config.game.path, + + patch_servers: config.patch.servers, + patch_folder: config.patch.path, + + status_updater + }) + } +}