feat(genshin): improved compatibility
This commit is contained in:
parent
ab8f4f8746
commit
7ed5457174
10 changed files with 331 additions and 34 deletions
|
@ -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;
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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();
|
||||
|
59
src/genshin/config/schema/game/fps_unlocker/mod.rs
Normal file
59
src/genshin/config/schema/game/fps_unlocker/mod.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
236
src/genshin/game.rs
Normal 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(())
|
||||
}
|
|
@ -11,3 +11,6 @@ pub mod env_emulation;
|
|||
|
||||
#[cfg(feature = "fps-unlocker")]
|
||||
pub mod fps_unlocker;
|
||||
|
||||
#[cfg(feature = "game")]
|
||||
pub mod game;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -130,9 +130,7 @@ 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 let Some(wine) = config.get_selected_wine()? {
|
||||
if !config.game.wine.builds.join(&wine.name).exists() {
|
||||
return Ok(Self::WineNotInstalled);
|
||||
}
|
||||
|
@ -151,9 +149,9 @@ impl LauncherState {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None => return Ok(Self::WineNotInstalled)
|
||||
else {
|
||||
return Ok(Self::WineNotInstalled);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue