feat(genshin): improved compatibility

This commit is contained in:
Observer KRypt0n_ 2023-04-15 12:19:39 +02:00
parent ab8f4f8746
commit 7ed5457174
No known key found for this signature in database
GPG key ID: 844DA47BA25FE1E2
10 changed files with 331 additions and 34 deletions

View file

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

View file

@ -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<schema::Schema> = None;
pub struct Config;
impl ConfigTrait for Config {
impl ConfigExt for Config {
type Schema = schema::Schema;
#[inline]

View file

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

View file

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

View file

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

View file

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

236
src/genshin/game.rs Normal file
View file

@ -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<T: ToString>(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 '<command>'
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 <params> -- <command to run>
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(())
}

View file

@ -11,3 +11,6 @@ pub mod env_emulation;
#[cfg(feature = "fps-unlocker")]
pub mod fps_unlocker;
#[cfg(feature = "game")]
pub mod game;

View file

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

View file

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