diff --git a/src/config/mod.rs b/src/config/mod.rs index 65daa35..f7a6ce0 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; /// Workpieces to create your custom config file schema pub mod schema_blanks; -pub trait Config { +pub trait ConfigExt { /// Default associated config schema type Schema; diff --git a/src/genshin/config/mod.rs b/src/genshin/config/mod.rs index 25eac8a..dc95aa2 100644 --- a/src/genshin/config/mod.rs +++ b/src/genshin/config/mod.rs @@ -4,14 +4,14 @@ pub mod schema; pub use schema::Schema; -use crate::config::Config as ConfigTrait; +use crate::config::ConfigExt; use crate::genshin::consts::config_file; static mut CONFIG: Option = None; pub struct Config; -impl ConfigTrait for Config { +impl ConfigExt for Config { type Schema = schema::Schema; #[inline] diff --git a/src/genshin/config/schema/game/fps_unlocker.rs b/src/genshin/config/schema/game/fps_unlocker/config.rs similarity index 94% rename from src/genshin/config/schema/game/fps_unlocker.rs rename to src/genshin/config/schema/game/fps_unlocker/config.rs index 1f8863a..299951d 100644 --- a/src/genshin/config/schema/game/fps_unlocker.rs +++ b/src/genshin/config/schema/game/fps_unlocker/config.rs @@ -4,7 +4,7 @@ use serde_json::Value as JsonValue; use crate::config::schema_blanks::prelude::*; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub struct FpsUnlocker { +pub struct Config { pub fps: Fps, pub power_saving: bool, pub monitor: u64, @@ -12,7 +12,7 @@ pub struct FpsUnlocker { pub priority: u64 } -impl Default for FpsUnlocker { +impl Default for Config { #[inline] fn default() -> Self { Self { @@ -25,7 +25,7 @@ impl Default for FpsUnlocker { } } -impl From<&JsonValue> for FpsUnlocker { +impl From<&JsonValue> for Config { fn from(value: &JsonValue) -> Self { let default = Self::default(); diff --git a/src/genshin/config/schema/game/fps_unlocker/mod.rs b/src/genshin/config/schema/game/fps_unlocker/mod.rs new file mode 100644 index 0000000..203fdba --- /dev/null +++ b/src/genshin/config/schema/game/fps_unlocker/mod.rs @@ -0,0 +1,59 @@ +use std::path::PathBuf; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use crate::genshin::consts::launcher_dir; + +pub mod config; + +pub mod prelude { + pub use super::config::Config as FpsUnlockerConfig; +} + +use prelude::*; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FpsUnlocker { + pub path: PathBuf, + pub enabled: bool, + pub config: FpsUnlockerConfig +} + +impl Default for FpsUnlocker { + fn default() -> Self { + let launcher_dir = launcher_dir().expect("Failed to get launcher dir"); + + Self { + path: launcher_dir.join("fps-unlocker"), + enabled: false, + config: FpsUnlockerConfig::default() + } + } +} + +impl From<&JsonValue> for FpsUnlocker { + 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 + }, + + enabled: match value.get("enabled") { + Some(value) => value.as_bool().unwrap_or(default.enabled), + None => default.enabled + }, + + config: match value.get("config") { + Some(value) => FpsUnlockerConfig::from(value), + None => default.config + } + } + } +} diff --git a/src/genshin/config/schema/game/mod.rs b/src/genshin/config/schema/game/mod.rs index a1a4dee..6373776 100644 --- a/src/genshin/config/schema/game/mod.rs +++ b/src/genshin/config/schema/game/mod.rs @@ -20,6 +20,9 @@ pub mod prelude { pub use super::Wine; pub use super::Dxvk; + #[cfg(feature = "fps-unlocker")] + pub use super::fps_unlocker::prelude::*; + pub use super::paths::Paths; pub use super::enhancements::Enhancements; diff --git a/src/genshin/fps_unlocker/mod.rs b/src/genshin/fps_unlocker/mod.rs index e53356a..ae30d84 100644 --- a/src/genshin/fps_unlocker/mod.rs +++ b/src/genshin/fps_unlocker/mod.rs @@ -4,7 +4,7 @@ use md5::{Md5, Digest}; use anime_game_core::installer::downloader::Downloader; -use super::config::schema::prelude::FpsUnlocker as FpsUnlockerConfig; +use super::config::schema::prelude::FpsUnlockerConfig; pub mod config_schema; diff --git a/src/genshin/game.rs b/src/genshin/game.rs new file mode 100644 index 0000000..95b5afa --- /dev/null +++ b/src/genshin/game.rs @@ -0,0 +1,236 @@ +use std::process::{Command, Stdio}; + +use anime_game_core::prelude::*; +use anime_game_core::genshin::telemetry; +use anime_game_core::genshin::game::Game; + +use crate::config::ConfigExt; +use crate::genshin::config::{Config, Schema}; + +use crate::genshin::consts; + +#[cfg(feature = "fps-unlocker")] +use super::fps_unlocker::FpsUnlocker; + +#[cfg(feature = "discord-rpc")] +use crate::discord_rpc::*; + +fn replace_keywords(command: T, config: &Schema) -> String { + let wine_build = config.game.wine.builds.join(config.game.wine.selected.as_ref().unwrap()); + + command.to_string() + .replace("%build%", &wine_build.to_string_lossy()) + .replace("%prefix%", &config.game.wine.prefix.to_string_lossy()) + .replace("%temp%", &config.launcher.temp.as_ref().unwrap_or(&std::env::temp_dir()).to_string_lossy()) + .replace("%launcher%", &consts::launcher_dir().unwrap().to_string_lossy()) + .replace("%game%", &config.game.path.for_edition(config.launcher.edition).to_string_lossy()) +} + +/// 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()?; + let game_path = config.game.path.for_edition(config.launcher.edition); + + if !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(); + + // Check telemetry servers + + tracing::info!("Checking telemetry"); + + if let Some(server) = telemetry::is_disabled(consts::TELEMETRY_CHECK_TIMEOUT) { + return Err(anyhow::anyhow!("Telemetry server is not disabled: {server}")); + } + + // Prepare fps unlocker + // 1) Download if needed + // 2) Generate config file + // 3) Generate fpsunlocker.bat from launcher.bat + + #[cfg(feature = "fps-unlocker")] + if config.game.enhancements.fps_unlocker.enabled { + tracing::info!("Preparing FPS unlocker"); + + let unlocker = match FpsUnlocker::from_dir(&config.game.enhancements.fps_unlocker.path) { + Ok(Some(unlocker)) => unlocker, + + other => { + // Ok(None) means unknown version, so we should delete it before downloading newer one + // because otherwise downloader will try to continue downloading "partially downloaded" file + if let Ok(None) = other { + std::fs::remove_file(FpsUnlocker::get_binary_in(&config.game.enhancements.fps_unlocker.path))?; + } + + tracing::info!("Unlocker is not downloaded. Downloading"); + + match FpsUnlocker::download(&config.game.enhancements.fps_unlocker.path) { + Ok(unlocker) => unlocker, + Err(err) => return Err(anyhow::anyhow!("Failed to download FPS unlocker: {err}")) + } + } + }; + + // Generate FPS unlocker config file + if let Err(err) = unlocker.update_config(config.game.enhancements.fps_unlocker.config) { + return Err(anyhow::anyhow!("Failed to update FPS unlocker config: {err}")); + } + + let bat_path = game_path.join("fps_unlocker.bat"); + let original_bat_path = game_path.join("launcher.bat"); + + // Generate fpsunlocker.bat from launcher.bat + std::fs::write(bat_path, std::fs::read_to_string(original_bat_path)? + .replace("start GenshinImpact.exe %*", &format!("start GenshinImpact.exe %*\n\nZ:\ncd \"{}\"\nstart unlocker.exe", unlocker.dir().to_string_lossy())) + .replace("start YuanShen.exe %*", &format!("start YuanShen.exe %*\n\nZ:\ncd \"{}\"\nstart unlocker.exe", unlocker.dir().to_string_lossy())))?; + } + + // Generate `config.ini` if environment emulation feature is presented + + #[cfg(feature = "environment-emulation")] { + let game = Game::new(game_path); + + std::fs::write( + game_path.join("config.ini"), + config.launcher.environment.generate_config(game.get_version()?.to_string()) + )?; + } + + // Prepare bash -c '' + + let mut bash_command = String::new(); + let mut windows_command = String::new(); + + if config.game.enhancements.gamemode { + bash_command += "gamemoderun "; + } + + let wine_build = config.game.wine.builds.join(&wine.name); + + let run_command = features.command + .map(|command| replace_keywords(command, &config)) + .unwrap_or(format!("'{}'", wine_build.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 += if config.game.enhancements.fps_unlocker.enabled && cfg!(feature = "fps-unlocker") { + "fps_unlocker.bat " + } else { + "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}"); + } + + // Bundle all windows arguments used to run the game into a single file + if features.compact_launch { + std::fs::write(game_path.join("compact_launch.bat"), format!("start {windows_command}\nexit"))?; + + windows_command = String::from("compact_launch.bat"); + } + + let bash_command = match &config.game.command { + Some(command) => replace_keywords(command, &config).replace("%command%", &bash_command), + None => 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", &config.game.wine.prefix); + + // Add environment flags for selected wine + for (key, value) in features.env.into_iter() { + command.env(key, replace_keywords(value, &config)); + } + + // 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, &config)); + } + } + } + + 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}\""); + + command.current_dir(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/genshin/mod.rs b/src/genshin/mod.rs index a40f4f2..e673290 100644 --- a/src/genshin/mod.rs +++ b/src/genshin/mod.rs @@ -11,3 +11,6 @@ pub mod env_emulation; #[cfg(feature = "fps-unlocker")] pub mod fps_unlocker; + +#[cfg(feature = "game")] +pub mod game; diff --git a/src/genshin/states.rs b/src/genshin/states.rs index 9c57758..753f80e 100644 --- a/src/genshin/states.rs +++ b/src/genshin/states.rs @@ -6,12 +6,10 @@ use wincompatlib::prelude::*; use anime_game_core::prelude::*; use anime_game_core::genshin::prelude::*; -use crate::config::Config as _; +use crate::config::ConfigExt; use crate::genshin::config::Config; -use crate::components::wine::{ - Version as WineVersion, - WincompatlibWine -}; + +use crate::components::wine::WincompatlibWine; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum LauncherState { diff --git a/src/honkai/states.rs b/src/honkai/states.rs index 3ab3137..5685ee3 100644 --- a/src/honkai/states.rs +++ b/src/honkai/states.rs @@ -130,30 +130,28 @@ impl LauncherState { // Check wine existence #[cfg(feature = "components")] { - match config.game.wine.selected { - Some(wine) => { - if let Some(wine) = WineVersion::find_in(&config.components.path, wine)? { - if !config.game.wine.builds.join(&wine.name).exists() { - return Ok(Self::WineNotInstalled); - } - - let wine = wine - .to_wine(&config.components.path, Some(&config.game.wine.builds.join(&wine.name))) - .with_prefix(&config.game.wine.prefix); - - match wine { - WincompatlibWine::Default(wine) => if let Some(prefix) = wine.prefix { - wine_prefix = prefix; - } - - WincompatlibWine::Proton(proton) => if let Some(prefix) = proton.wine().prefix.clone() { - wine_prefix = prefix; - } - } - } + if let Some(wine) = config.get_selected_wine()? { + if !config.game.wine.builds.join(&wine.name).exists() { + return Ok(Self::WineNotInstalled); } - None => return Ok(Self::WineNotInstalled) + let wine = wine + .to_wine(&config.components.path, Some(&config.game.wine.builds.join(&wine.name))) + .with_prefix(&config.game.wine.prefix); + + match wine { + WincompatlibWine::Default(wine) => if let Some(prefix) = wine.prefix { + wine_prefix = prefix; + } + + WincompatlibWine::Proton(proton) => if let Some(prefix) = proton.wine().prefix.clone() { + wine_prefix = prefix; + } + } + } + + else { + return Ok(Self::WineNotInstalled); } }