Added support for FPS unlocker

This commit is contained in:
Observer KRypt0n_ 2022-09-11 23:42:58 +02:00
parent e7c0228fdd
commit a7b1345302
No known key found for this signature in database
GPG key ID: 844DA47BA25FE1E2
13 changed files with 430 additions and 15 deletions

1
Cargo.lock generated
View file

@ -60,6 +60,7 @@ dependencies = [
"gtk4",
"lazy_static",
"libadwaita",
"md5",
"regex",
"serde",
"serde_json",

View file

@ -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"

View file

@ -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;
}
}
}
}

View file

@ -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<Self> {
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
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}
}

View file

@ -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

View file

@ -0,0 +1,64 @@
use serde::Serialize;
use super::FpsUnlockerConfig;
#[derive(Debug, Clone, Serialize)]
#[allow(non_snake_case)]
pub struct ConfigSchema {
pub DllList: Vec<String>,
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<String>
}
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<String> {
serde_json::to_string(self)
}
}

View file

@ -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<T: ToString>(dir: T) -> anyhow::Result<Option<Self>> {
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<T: ToString>(dir: T) -> anyhow::Result<Self> {
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()?
)?)
}
}

View file

@ -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<Terminal> {
/// 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 '<command>'
@ -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(())
}

View file

@ -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};

View file

@ -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((

View file

@ -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)?;