From a7b134530276add2eb94df8cf52196fea772b9ed Mon Sep 17 00:00:00 2001 From: Observer KRypt0n_ Date: Sun, 11 Sep 2022 23:42:58 +0200 Subject: [PATCH] Added support for FPS unlocker --- Cargo.lock | 1 + Cargo.toml | 1 + assets/ui/preferences/enhancements.blp | 22 ++++++ .../enhancements/fps_unlocker/config/fps.rs | 67 +++++++++++++++++++ .../enhancements/fps_unlocker/config/mod.rs | 41 ++++++++++++ .../game/enhancements/fps_unlocker/mod.rs | 56 ++++++++++++++++ src/lib/config/game/enhancements/mod.rs | 11 ++- src/lib/fps_unlocker/config_schema.rs | 64 ++++++++++++++++++ src/lib/fps_unlocker/mod.rs | 67 +++++++++++++++++++ src/lib/game.rs | 47 ++++++++++--- src/lib/mod.rs | 1 + src/ui/main.rs | 2 +- src/ui/preferences/enhancements.rs | 65 +++++++++++++++++- 13 files changed, 430 insertions(+), 15 deletions(-) create mode 100644 src/lib/config/game/enhancements/fps_unlocker/config/fps.rs create mode 100644 src/lib/config/game/enhancements/fps_unlocker/config/mod.rs create mode 100644 src/lib/config/game/enhancements/fps_unlocker/mod.rs create mode 100644 src/lib/fps_unlocker/config_schema.rs create mode 100644 src/lib/fps_unlocker/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 11b4bc9..4879a7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,7 @@ dependencies = [ "gtk4", "lazy_static", "libadwaita", + "md5", "regex", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 2144304..a563b03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,4 @@ wait_not_await = "0.2.1" regex = "1.6.0" lazy_static = "1.4.0" anyhow = "1.0" +md5 = "0.7" diff --git a/assets/ui/preferences/enhancements.blp b/assets/ui/preferences/enhancements.blp index 4fd33fa..2a87ca7 100644 --- a/assets/ui/preferences/enhancements.blp +++ b/assets/ui/preferences/enhancements.blp @@ -99,4 +99,26 @@ Adw.PreferencesPage page { } } } + + Adw.PreferencesGroup { + title: "FPS Unlocker"; + + Adw.ComboRow fps_unlocker_combo { + title: "Enabled"; + subtitle: "Remove frames rendering limitation modifying the game's memory. Can be detected by the anti-cheat"; + + Gtk.Switch fps_unlocker_switcher { + valign: center; + } + } + + Adw.ActionRow { + title: "Power saving"; + subtitle: "Automatically sets the fps limit to 10 and low process priority upon losing focus to the game (e.g. tabbing out of the game)"; + + Gtk.Switch fps_unlocker_power_saving_switcher { + valign: center; + } + } + } } diff --git a/src/lib/config/game/enhancements/fps_unlocker/config/fps.rs b/src/lib/config/game/enhancements/fps_unlocker/config/fps.rs new file mode 100644 index 0000000..7ce5d3e --- /dev/null +++ b/src/lib/config/game/enhancements/fps_unlocker/config/fps.rs @@ -0,0 +1,67 @@ +use gtk4 as gtk; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Fps { + /// 90 + Ninety, + + /// 100 + Hundred, + + /// 120 + HundredTwenty, + + /// 144 + HundredFourtyFour, + + /// 200 + TwoHundred, + + Custom(u64) +} + +impl Fps { + pub fn list() -> Vec { + vec![ + Self::Ninety, + Self::Hundred, + Self::HundredTwenty, + Self::HundredFourtyFour, + Self::TwoHundred + ] + } + + pub fn get_model() -> gtk::StringList { + let model = gtk::StringList::new(&[]); + + model.append("Custom"); + + for res in Self::list() { + model.append(&res.to_num().to_string()); + } + + model + } + + pub fn from_num(fps: u64) -> Self { + match fps { + 90 => Self::Ninety, + 100 => Self::Hundred, + 120 => Self::HundredTwenty, + 144 => Self::HundredFourtyFour, + 200 => Self::TwoHundred, + num => Self::Custom(num) + } + } + + pub fn to_num(&self) -> u64 { + match self { + Fps::Ninety => 90, + Fps::Hundred => 100, + Fps::HundredTwenty => 120, + Fps::HundredFourtyFour => 144, + Fps::TwoHundred => 200, + Fps::Custom(num) => *num + } + } +} diff --git a/src/lib/config/game/enhancements/fps_unlocker/config/mod.rs b/src/lib/config/game/enhancements/fps_unlocker/config/mod.rs new file mode 100644 index 0000000..908986f --- /dev/null +++ b/src/lib/config/game/enhancements/fps_unlocker/config/mod.rs @@ -0,0 +1,41 @@ +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +pub mod fps; + +pub mod prelude { + pub use super::fps::Fps; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub fps: u64, + pub power_saving: bool +} + +impl Default for Config { + fn default() -> Self { + Self { + fps: 120, + power_saving: false + } + } +} + +impl From<&JsonValue> for Config { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + fps: match value.get("fps") { + Some(value) => value.as_u64().unwrap_or(default.fps), + None => default.fps + }, + + power_saving: match value.get("power_saving") { + Some(value) => value.as_bool().unwrap_or(default.power_saving), + None => default.power_saving + } + } + } +} diff --git a/src/lib/config/game/enhancements/fps_unlocker/mod.rs b/src/lib/config/game/enhancements/fps_unlocker/mod.rs new file mode 100644 index 0000000..1a9e1ca --- /dev/null +++ b/src/lib/config/game/enhancements/fps_unlocker/mod.rs @@ -0,0 +1,56 @@ +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use crate::lib::consts::launcher_dir; + +pub mod config; + +pub mod prelude { + pub use super::config::Config; + + pub use super::config::prelude::*; +} + +use prelude::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FpsUnlocker { + pub path: String, + pub enabled: bool, + pub config: Config +} + +impl Default for FpsUnlocker { + fn default() -> Self { + let launcher_dir = launcher_dir().expect("Failed to get launcher dir"); + + Self { + path: format!("{launcher_dir}/fps-unlocker"), + enabled: false, + config: Config::default() + } + } +} + +impl From<&JsonValue> for FpsUnlocker { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + path: match value.get("path") { + Some(value) => value.as_str().unwrap_or(&default.path).to_string(), + 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) => Config::from(value), + None => default.config + } + } + } +} diff --git a/src/lib/config/game/enhancements/mod.rs b/src/lib/config/game/enhancements/mod.rs index b7f1f25..1f85c97 100644 --- a/src/lib/config/game/enhancements/mod.rs +++ b/src/lib/config/game/enhancements/mod.rs @@ -3,23 +3,27 @@ use serde_json::Value as JsonValue; pub mod fsr; pub mod hud; +pub mod fps_unlocker; pub mod gamescope; pub mod prelude { pub use super::gamescope::prelude::*; + pub use super::fps_unlocker::prelude::*; pub use super::Enhancements; pub use super::fsr::Fsr; pub use super::hud::HUD; + pub use super::fps_unlocker::FpsUnlocker; } use prelude::*; -#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Enhancements { pub fsr: Fsr, pub gamemode: bool, pub hud: HUD, + pub fps_unlocker: FpsUnlocker, pub gamescope: Gamescope } @@ -43,6 +47,11 @@ impl From<&JsonValue> for Enhancements { None => default.hud }, + fps_unlocker: match value.get("fps_unlocker") { + Some(value) => FpsUnlocker::from(value), + None => default.fps_unlocker + }, + gamescope: match value.get("gamescope") { Some(value) => Gamescope::from(value), None => default.gamescope diff --git a/src/lib/fps_unlocker/config_schema.rs b/src/lib/fps_unlocker/config_schema.rs new file mode 100644 index 0000000..69401b2 --- /dev/null +++ b/src/lib/fps_unlocker/config_schema.rs @@ -0,0 +1,64 @@ +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, + + ..Self::default() + } + } + + pub fn json(&self) -> serde_json::Result { + serde_json::to_string(self) + } +} diff --git a/src/lib/fps_unlocker/mod.rs b/src/lib/fps_unlocker/mod.rs new file mode 100644 index 0000000..25a135e --- /dev/null +++ b/src/lib/fps_unlocker/mod.rs @@ -0,0 +1,67 @@ +use anime_game_core::installer::downloader::Downloader; + +use crate::lib::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: String +} + +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 hash = format!("{:x}", md5::compute(std::fs::read(format!("{}/unlocker.exe", dir.to_string()))?)); + + if hash == LATEST_INFO.0 { + Ok(Some(Self { dir: dir.to_string() })) + } else { + Ok(None) + } + } + + /// Download FPS unlocker to specified directory + pub fn download(dir: T) -> anyhow::Result { + let mut downloader = Downloader::new(LATEST_INFO.1)?; + + match downloader.download_to(format!("{}/unlocker.exe", dir.to_string()), |_, _| {}) { + Ok(_) => Ok(Self { + dir: dir.to_string() + }), + Err(err) => { + let err: std::io::Error = err.into(); + + Err(err.into()) + } + } + } + + pub fn get_binary(&self) -> String { + format!("{}/unlocker.exe", self.dir) + } + + pub fn dir(&self) -> &str { + self.dir.as_str() + } + + /// 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( + format!("{}/fps_config.json", self.dir), + config.json()? + )?) + } +} diff --git a/src/lib/game.rs b/src/lib/game.rs index 6c521f6..bfc977a 100644 --- a/src/lib/game.rs +++ b/src/lib/game.rs @@ -1,4 +1,3 @@ -use std::io::{Error, ErrorKind}; use std::path::Path; use std::process::Command; @@ -6,6 +5,7 @@ use anime_game_core::genshin::telemetry; use super::consts; use super::config; +use super::fps_unlocker::FpsUnlocker; /*#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Terminal { @@ -64,22 +64,51 @@ pub fn try_get_terminal() -> Option { /// Try to run the game /// /// If `debug = true`, then the game will be run in the new terminal window -pub fn run(debug: bool) -> std::io::Result<()> { +pub fn run() -> anyhow::Result<()> { let config = config::get()?; if !Path::new(&config.game.path).exists() { - return Err(Error::new(ErrorKind::Other, "Game is not installed")); + return Err(anyhow::anyhow!("Game is not installed")); } let wine_executable = match config.try_get_wine_executable() { Some(path) => path, - None => return Err(Error::new(ErrorKind::Other, "Couldn't find wine executable")) + 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(Error::new(ErrorKind::Other, format!("Telemetry server is not disabled: {server}"))); + 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 + + if config.game.enhancements.fps_unlocker.enabled { + let unlocker = match FpsUnlocker::from_dir(&config.game.enhancements.fps_unlocker.path) { + Ok(Some(unlocker)) => unlocker, + Ok(None) => match FpsUnlocker::download(&config.game.enhancements.fps_unlocker.path) { + Ok(unlocker) => unlocker, + Err(err) => return Err(anyhow::anyhow!("Failed to download FPS unlocker: {err}")) + }, + Err(err) => return Err(anyhow::anyhow!("Failed to load 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 = format!("{}/fpsunlocker.bat", config.game.path); + let original_bat_path = format!("{}/launcher.bat", config.game.path); + + // 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())) + .replace("start YuanShen.exe %*", &format!("start YuanShen.exe %*\n\nZ:\ncd \"{}\"\nstart unlocker.exe", unlocker.dir())))?; } // Prepare bash -c '' @@ -96,11 +125,7 @@ pub fn run(debug: bool) -> std::io::Result<()> { bash_chain += &format!("{virtual_desktop} "); } - if debug { - todo!(); - } else { - bash_chain += "launcher.bat "; - } + bash_chain += if config.game.enhancements.fps_unlocker.enabled { "fpsunlocker.bat " } else { "launcher.bat " }; if config.game.wine.borderless { bash_chain += "-screen-fullscreen 0 -popupwindow "; @@ -150,6 +175,6 @@ pub fn run(debug: bool) -> std::io::Result<()> { println!("Running command: bash -c \"{}\"", bash_chain); command.current_dir(config.game.path).spawn()?; - + Ok(()) } diff --git a/src/lib/mod.rs b/src/lib/mod.rs index 3965581..93e0269 100644 --- a/src/lib/mod.rs +++ b/src/lib/mod.rs @@ -6,6 +6,7 @@ pub mod wine; pub mod wine_prefix; pub mod launcher; pub mod prettify_bytes; +pub mod fps_unlocker; use std::process::{Command, Stdio}; diff --git a/src/ui/main.rs b/src/ui/main.rs index e0c93f8..7e9f174 100644 --- a/src/ui/main.rs +++ b/src/ui/main.rs @@ -318,7 +318,7 @@ impl App { std::thread::spawn(move || { // Display toast message if the game is failed to run - if let Err(err) = game::run(false) { + if let Err(err) = game::run() { this.widgets.window.show(); this.update(Actions::Toast(Rc::new(( diff --git a/src/ui/preferences/enhancements.rs b/src/ui/preferences/enhancements.rs index 2ca1bde..dd731cb 100644 --- a/src/ui/preferences/enhancements.rs +++ b/src/ui/preferences/enhancements.rs @@ -38,7 +38,11 @@ pub struct AppWidgets { pub gamescope_settings: gtk::Button, pub gamescope_switcher: gtk::Switch, - pub gamescope_app: GamescopeApp + pub gamescope_app: GamescopeApp, + + pub fps_unlocker_combo: adw::ComboRow, + pub fps_unlocker_switcher: gtk::Switch, + pub fps_unlocker_power_saving_switcher: gtk::Switch } impl AppWidgets { @@ -65,7 +69,11 @@ impl AppWidgets { gamescope_settings: get_object(&builder, "gamescope_settings")?, gamescope_switcher: get_object(&builder, "gamescope_switcher")?, - gamescope_app: GamescopeApp::new(window)? + gamescope_app: GamescopeApp::new(window)?, + + fps_unlocker_combo: get_object(&builder, "fps_unlocker_combo")?, + fps_unlocker_switcher: get_object(&builder, "fps_unlocker_switcher")?, + fps_unlocker_power_saving_switcher: get_object(&builder, "fps_unlocker_power_saving_switcher")? }; // Set availale wine languages @@ -74,6 +82,9 @@ impl AppWidgets { // Set availale virtual desktop resolutions result.virtual_desktop_row.set_model(Some(&Resolution::get_model())); + // Set availale fps unlocker limits + result.fps_unlocker_combo.set_model(Some(&Fps::get_model())); + // Disable gamemode row if it's not available if !lib::is_available("gamemoderun") { result.gamemode_row.set_sensitive(false); @@ -227,6 +238,35 @@ impl App { } }); + // FPS unlocker swithing + self.widgets.fps_unlocker_switcher.connect_state_notify(move |switch| { + if let Ok(mut config) = config::get() { + config.game.enhancements.fps_unlocker.enabled = switch.state(); + + config::update(config); + } + }); + + // FPS unlocker -> fps limit combo + self.widgets.fps_unlocker_combo.connect_selected_notify(move |row| { + if let Ok(mut config) = config::get() { + if row.selected() > 0 { + config.game.enhancements.fps_unlocker.config.fps = Fps::list()[row.selected() as usize - 1].to_num(); + + config::update(config); + } + } + }); + + // FPS unlocker -> power saving swithing + self.widgets.fps_unlocker_power_saving_switcher.connect_state_notify(move |switch| { + if let Ok(mut config) = config::get() { + config.game.enhancements.fps_unlocker.config.power_saving = switch.state(); + + config::update(config); + } + }); + self } @@ -288,6 +328,27 @@ impl App { // Switch gamescope option self.widgets.gamescope_switcher.set_state(config.game.enhancements.gamescope.enabled); + // Switch FPS unlocker + self.widgets.fps_unlocker_switcher.set_state(config.game.enhancements.fps_unlocker.enabled); + + // Select FPS limit + let fps = Fps::from_num(config.game.enhancements.fps_unlocker.config.fps); + + if let Fps::Custom(_) = fps { + self.widgets.fps_unlocker_combo.set_selected(0); + } + + else { + for (i, value) in Fps::list().into_iter().enumerate() { + if value == fps { + self.widgets.fps_unlocker_combo.set_selected(i as u32 + 1); + } + } + } + + // Switch FPS unlocker -> power saving + self.widgets.fps_unlocker_power_saving_switcher.set_state(config.game.enhancements.fps_unlocker.config.power_saving); + // Prepare gamescope settings app self.widgets.gamescope_app.prepare(status_page)?;