diff --git a/Cargo.toml b/Cargo.toml index 4c343e0..1ae0a8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,13 +16,14 @@ serde = { version = "1.0", features = ["derive"], optional = true } serde_json = { version = "1.0", optional = true } wincompatlib = { version = "0.1.2", features = ["dxvk"], optional = true } lazy_static = { version = "1.4.0", optional = true } +md5 = { version = "0.7.0", optional = true } [features] states = [] config = ["dep:serde", "dep:serde_json"] components = ["dep:wincompatlib", "dep:lazy_static"] -runner = [] -fps-unlocker = [] +game = ["components"] +fps-unlocker = ["dep:md5"] default = ["all"] -all = ["states", "config", "components", "runner", "fps-unlocker"] +all = ["states", "config", "components", "game", "fps-unlocker"] diff --git a/README.md b/README.md index 71f1cb1..69be6a6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ * Remove excess code from gtk launcher and prepare it for relm4 rewrite; * Prepare codebase for tauri rewrite; -## Current progress (50%) +## Current progress (75%) | Status | Feature | Description | | :-: | - | - | @@ -16,5 +16,5 @@ | ✅ | | List Wine and DXVK versions | | ❌ | | Download, delete and select wine | | ❌ | | Download, delete, select and apply DXVK | -| ❌ | runner | Run the game | -| ❌ | fps-unlocker | Support of FPS unlocker. Manage its config, download, use in game runner | +| ✅ | game | Run the game | +| ✅ | fps-unlocker | Support of FPS unlocker. Manage its config, download, use in game runner | diff --git a/src/components/dxvk.rs b/src/components/dxvk.rs index 394672d..f8ffd4e 100644 --- a/src/components/dxvk.rs +++ b/src/components/dxvk.rs @@ -34,14 +34,17 @@ pub struct Version { } impl Version { + /// Get latest recommended dxvk version pub fn latest() -> Self { get_groups()[0].versions[0].clone() } + /// Check is current dxvk downloaded in specified folder pub fn is_downloaded_in>(&self, folder: T) -> bool { folder.into().join(&self.name).exists() } + /// Apply current dxvk to specified wine prefix pub fn apply>(&self, dxvks_folder: T, prefix_path: T) -> anyhow::Result { let apply_path = dxvks_folder.into().join(&self.name).join("setup_dxvk.sh"); let config = config::get()?; @@ -75,11 +78,12 @@ impl Version { } } +/// Get dxvk groups pub fn get_groups() -> Vec { GROUPS.clone() } -/// List downloaded DXVK versions in some specific folder +/// List downloaded dxvk versions in some specific folder pub fn get_downloaded>(folder: T) -> std::io::Result> { let mut downloaded = Vec::new(); diff --git a/src/components/wine.rs b/src/components/wine.rs index e71bd7e..4f5fd8a 100644 --- a/src/components/wine.rs +++ b/src/components/wine.rs @@ -40,14 +40,17 @@ pub struct Version { } impl Version { + /// Get latest recommended wine version pub fn latest() -> Self { get_groups()[0].versions[0].clone() } + /// Check is current wine downloaded in specified folder pub fn is_downloaded_in>(&self, folder: T) -> bool { folder.into().join(&self.name).exists() } + /// Convert current wine struct to one from `wincompatlib` pub fn to_wine(&self) -> Wine { Wine::new( &self.files.wine64, @@ -69,10 +72,12 @@ pub struct Files { pub winecfg: String } +/// Get wine groups pub fn get_groups() -> Vec { GROUPS.clone() } +/// List downloaded wine versions in some specific folder pub fn get_downloaded>(folder: T) -> std::io::Result> { let mut downloaded = Vec::new(); diff --git a/src/fps_unlocker/config_schema.rs b/src/fps_unlocker/config_schema.rs new file mode 100644 index 0000000..c32a5d4 --- /dev/null +++ b/src/fps_unlocker/config_schema.rs @@ -0,0 +1,66 @@ +use serde::Serialize; + +use super::FpsUnlockerConfig; + +#[derive(Debug, Clone, Serialize)] +#[allow(non_snake_case)] +pub struct ConfigSchema { + pub DllList: Vec, + pub Priority: u64, + pub MonitorNum: u64, + pub CustomResY: u64, + pub CustomResX: u64, + pub FPSTarget: u64, + pub UsePowerSave: bool, + pub StartMinimized: bool, + pub IsExclusiveFullscreen: bool, + pub UseCustomRes: bool, + pub Fullscreen: bool, + pub PopupWindow: bool, + pub AutoClose: bool, + pub AutoDisableVSync: bool, + pub AutoStart: bool, + pub GamePath: Option +} + +impl Default for ConfigSchema { + fn default() -> Self { + Self { + DllList: vec![], + Priority: 3, + MonitorNum: 1, + CustomResY: 1080, + CustomResX: 1920, + FPSTarget: 120, + UsePowerSave: false, + IsExclusiveFullscreen: false, + UseCustomRes: false, + Fullscreen: false, + PopupWindow: false, + AutoDisableVSync: true, + GamePath: None, + + // Launcher-specific settings + AutoStart: true, + AutoClose: true, + StartMinimized: true + } + } +} + +impl ConfigSchema { + pub fn from_config(config: FpsUnlockerConfig) -> Self { + Self { + FPSTarget: config.fps, + UsePowerSave: config.power_saving, + Fullscreen: config.fullscreen, + Priority: config.priority, + + ..Self::default() + } + } + + pub fn json(&self) -> serde_json::Result { + serde_json::to_string(self) + } +} diff --git a/src/fps_unlocker/mod.rs b/src/fps_unlocker/mod.rs new file mode 100644 index 0000000..c738ced --- /dev/null +++ b/src/fps_unlocker/mod.rs @@ -0,0 +1,82 @@ +use std::path::PathBuf; + +use anime_game_core::installer::downloader::Downloader; + +use crate::config::game::enhancements::fps_unlocker::config::Config as FpsUnlockerConfig; + +pub mod config_schema; + +const LATEST_INFO: (&str, &str) = ( + "6040a6f0be5dbf4d55d6b129cad47b5b", + "https://github.com/34736384/genshin-fps-unlock/releases/download/v2.0.0/unlockfps_clr.exe" +); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FpsUnlocker { + dir: PathBuf +} + +impl FpsUnlocker { + /// Get FpsUnlocker from its containment directory + /// + /// Returns + /// - `Err(..)` if failed to read `unlocker.exe` file + /// - `Ok(None)` if version is not latest + /// - `Ok(..)` if version is latest + pub fn from_dir>(dir: T) -> anyhow::Result> { + let dir = dir.into(); + + let hash = format!("{:x}", md5::compute(std::fs::read(dir.join("unlocker.exe"))?)); + + Ok(if hash == LATEST_INFO.0 { + Some(Self { dir }) + } else { + None + }) + } + + /// Download FPS unlocker to specified directory + pub fn download>(dir: T) -> anyhow::Result { + let mut downloader = Downloader::new(LATEST_INFO.1)?; + + let dir = dir.into(); + + // Create FPS unlocker folder if needed + if !dir.exists() { + std::fs::create_dir_all(&dir)?; + } + + match downloader.download_to(dir.join("unlocker.exe"), |_, _| {}) { + Ok(_) => Ok(Self { + dir + }), + Err(err) => { + let err: std::io::Error = err.into(); + + Err(err.into()) + } + } + } + + pub fn get_binary(&self) -> PathBuf { + Self::get_binary_in(&self.dir) + } + + pub fn get_binary_in>(dir: T) -> PathBuf { + dir.into().join("unlocker.exe") + } + + pub fn dir(&self) -> &PathBuf { + &self.dir + } + + /// Generate and save FPS unlocker config file to the game's directory + pub fn update_config(&self, config: FpsUnlockerConfig) -> anyhow::Result<()> { + let config = config_schema::ConfigSchema::from_config(config); + + Ok(std::fs::write( + self.dir.join("fps_config.json"), + config.json()? + )?) + } +} diff --git a/src/game/mod.rs b/src/game/mod.rs new file mode 100644 index 0000000..cb2c7a7 --- /dev/null +++ b/src/game/mod.rs @@ -0,0 +1,138 @@ +use std::process::Command; + +use anime_game_core::genshin::telemetry; + +use super::consts; +use super::config; +use super::fps_unlocker::FpsUnlocker; + +/// Try to run the game +/// +/// If `debug = true`, then the game will be run in the new terminal window +pub fn run() -> anyhow::Result<()> { + let config = config::get()?; + + if !config.game.path.exists() { + return Err(anyhow::anyhow!("Game is not installed")); + } + + let wine_executable = match config.try_get_wine_executable() { + Some(path) => path, + None => return Err(anyhow::anyhow!("Couldn't find wine executable")) + }; + + // Check telemetry servers + + 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 { + 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))?; + } + + 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.clone()) { + return Err(anyhow::anyhow!("Failed to update FPS unlocker config: {err}")); + } + + let bat_path = config.game.path.join("fpsunlocker.bat"); + let original_bat_path = config.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())))?; + } + + // Prepare bash -c '' + + let mut bash_chain = String::new(); + + if config.game.enhancements.gamemode { + bash_chain += "gamemoderun "; + } + + bash_chain += &format!("'{}' ", wine_executable.to_string_lossy()); + + if let Some(virtual_desktop) = config.game.wine.virtual_desktop.get_command() { + bash_chain += &format!("{virtual_desktop} "); + } + + bash_chain += if config.game.enhancements.fps_unlocker.enabled && cfg!(feature = "fps-unlocker") { + "fpsunlocker.bat " + } else { + "launcher.bat " + }; + + if config.game.wine.borderless { + bash_chain += "-screen-fullscreen 0 -popupwindow "; + } + + // https://notabug.org/Krock/dawn/src/master/TWEAKS.md + if config.game.enhancements.fsr.enabled { + bash_chain += "-window-mode exclusive "; + } + + // gamescope -- + if let Some(gamescope) = config.game.enhancements.gamescope.get_command() { + bash_chain = format!("{gamescope} -- {bash_chain}"); + } + + let bash_chain = match &config.game.command { + Some(command) => command.replace("%command%", &bash_chain), + None => bash_chain + }; + + let mut command = Command::new("bash"); + + command.arg("-c"); + command.arg(&bash_chain); + + // Setup environment + + command.env("WINEARCH", "win64"); + command.env("WINEPREFIX", &config.game.wine.prefix); + + // Add DXVK_ASYNC=1 for dxvk-async builds automatically + if let Ok(Some(dxvk)) = &config.try_get_selected_dxvk_info() { + if dxvk.version.contains("async") { + command.env("DXVK_ASYNC", "1"); + } + } + + command.envs(config.game.wine.sync.get_env_vars()); + command.envs(config.game.enhancements.hud.get_env_vars(&config)); + 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 + + println!("Running command: bash -c \"{}\"", bash_chain); + + command.current_dir(config.game.path).spawn()?; + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs index 138bd18..7c6a56f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,12 @@ pub mod states; #[cfg(feature = "components")] pub mod components; +#[cfg(feature = "game")] +pub mod game; + +#[cfg(feature = "fps-unlocker")] +pub mod fps_unlocker; + /// Check if specified binary is available /// /// ```