diff --git a/.gitmodules b/.gitmodules index 3a3be25..183769b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "anime-game-core"] path = anime-game-core url = https://github.com/an-anime-team/anime-game-core +[submodule "components"] + path = components + url = https://github.com/an-anime-team/components diff --git a/Cargo.toml b/Cargo.toml index 9277b7f..4c343e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,11 +14,13 @@ dirs = "4.0.0" 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 } [features] states = [] config = ["dep:serde", "dep:serde_json"] -components = [] +components = ["dep:wincompatlib", "dep:lazy_static"] runner = [] fps-unlocker = [] diff --git a/README.md b/README.md index 6431d5e..71f1cb1 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,14 @@ * Remove excess code from gtk launcher and prepare it for relm4 rewrite; * Prepare codebase for tauri rewrite; -## Current progress (12.5%) +## Current progress (50%) | Status | Feature | Description | | :-: | - | - | -| ❌ | states | Getting current launcher's state (update available, etc.) | +| ✅ | states | Getting current launcher's state (update available, etc.) | | ✅ | config | Work with config file | -| ❌ | components | Work with components needed to run the game | -| ❌ | | List Wine and DXVK versions | +| ✅ | components | Work with components needed to run the game | +| ✅ | | List Wine and DXVK versions | | ❌ | | Download, delete and select wine | | ❌ | | Download, delete, select and apply DXVK | | ❌ | runner | Run the game | diff --git a/components b/components new file mode 160000 index 0000000..f7219b5 --- /dev/null +++ b/components @@ -0,0 +1 @@ +Subproject commit f7219b5b1b50d8b09ab14142c5972656bef42900 diff --git a/src/components/dxvk.rs b/src/components/dxvk.rs new file mode 100644 index 0000000..394672d --- /dev/null +++ b/src/components/dxvk.rs @@ -0,0 +1,104 @@ +use std::process::Output; +use std::path::PathBuf; + +use serde::{Serialize, Deserialize}; +use wincompatlib::prelude::*; + +use crate::config; + +lazy_static::lazy_static! { + static ref GROUPS: Vec = vec![ + Group { + name: String::from("Vanilla"), + versions: serde_json::from_str::>(include_str!("../../components/dxvk/vanilla.json")).unwrap().into_iter().take(12).collect() + }, + Group { + name: String::from("Async"), + versions: serde_json::from_str::>(include_str!("../../components/dxvk/async.json")).unwrap().into_iter().take(12).collect() + } + ]; +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Group { + pub name: String, + pub versions: Vec +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Version { + pub name: String, + pub version: String, + pub uri: String, + pub recommended: bool +} + +impl Version { + pub fn latest() -> Self { + get_groups()[0].versions[0].clone() + } + + pub fn is_downloaded_in>(&self, folder: T) -> bool { + folder.into().join(&self.name).exists() + } + + 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()?; + + let (wine_path, wineserver_path, wineboot_path) = match config.try_get_selected_wine_info() { + Some(wine) => { + let wine_folder = config.game.wine.builds.join(wine.name); + + let wine_path = wine_folder.join(wine.files.wine64); + let wineserver_path = wine_folder.join(wine.files.wineserver); + let wineboot_path = wine_folder.join(wine.files.wineboot); + + (wine_path, wineserver_path, wineboot_path) + }, + None => (PathBuf::from("wine64"), PathBuf::from("wineserver"), PathBuf::from("wineboot")) + }; + + let result = Dxvk::install( + apply_path, + prefix_path.into(), + wine_path.clone(), + wine_path, + wineboot_path, + wineserver_path + ); + + match result { + Ok(output) => Ok(output), + Err(err) => Err(err.into()) + } + } +} + +pub fn get_groups() -> Vec { + GROUPS.clone() +} + +/// List downloaded DXVK versions in some specific folder +pub fn get_downloaded>(folder: T) -> std::io::Result> { + let mut downloaded = Vec::new(); + + let list = get_groups() + .into_iter() + .flat_map(|group| group.versions) + .collect::>(); + + for entry in folder.into().read_dir()? { + let name = entry?.file_name(); + + for version in &list { + if name == version.name.as_str() { + downloaded.push(version.clone()); + + break; + } + } + } + + Ok(downloaded) +} diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..969952d --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,2 @@ +pub mod wine; +pub mod dxvk; diff --git a/src/components/wine.rs b/src/components/wine.rs new file mode 100644 index 0000000..e71bd7e --- /dev/null +++ b/src/components/wine.rs @@ -0,0 +1,99 @@ +use std::path::PathBuf; + +use serde::{Serialize, Deserialize}; +use wincompatlib::prelude::*; + +lazy_static::lazy_static! { + static ref GROUPS: Vec = vec![ + Group { + name: String::from("Wine-GE-Proton"), + versions: serde_json::from_str::>(include_str!("../../components/wine/wine-ge-proton.json")).unwrap().into_iter().take(12).collect() + }, + Group { + name: String::from("GE-Proton"), + versions: serde_json::from_str::>(include_str!("../../components/wine/ge-proton.json")).unwrap().into_iter().take(12).collect() + }, + Group { + name: String::from("Soda"), + versions: serde_json::from_str::>(include_str!("../../components/wine/soda.json")).unwrap().into_iter().take(12).collect() + }, + Group { + name: String::from("Lutris"), + versions: serde_json::from_str::>(include_str!("../../components/wine/lutris.json")).unwrap().into_iter().take(12).collect() + } + ]; +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Group { + pub name: String, + pub versions: Vec +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Version { + pub name: String, + pub title: String, + pub uri: String, + pub files: Files, + pub recommended: bool +} + +impl Version { + pub fn latest() -> Self { + get_groups()[0].versions[0].clone() + } + + pub fn is_downloaded_in>(&self, folder: T) -> bool { + folder.into().join(&self.name).exists() + } + + pub fn to_wine(&self) -> Wine { + Wine::new( + &self.files.wine64, + None, + Some(WineArch::Win64), + Some(&self.files.wineboot), + Some(&self.files.wineserver), + WineLoader::Current + ) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Files { + pub wine: String, + pub wine64: String, + pub wineserver: String, + pub wineboot: String, + pub winecfg: String +} + +pub fn get_groups() -> Vec { + GROUPS.clone() +} + +pub fn get_downloaded>(folder: T) -> std::io::Result> { + let mut downloaded = Vec::new(); + + let list = get_groups() + .into_iter() + .flat_map(|group| group.versions) + .collect::>(); + + for entry in folder.into().read_dir()? { + let name = entry?.file_name(); + + for version in &list { + if name == version.name.as_str() { + downloaded.push(version.clone()); + + break; + } + } + } + + downloaded.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap()); + + Ok(downloaded) +} diff --git a/src/config/game/dxvk.rs b/src/config/game/dxvk.rs index 275cc70..256d7be 100644 --- a/src/config/game/dxvk.rs +++ b/src/config/game/dxvk.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use serde::{Serialize, Deserialize}; use serde_json::Value as JsonValue; -use crate::launcher_dir; +use crate::consts::launcher_dir; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Dxvk { diff --git a/src/config/game/enhancements/fps_unlocker/mod.rs b/src/config/game/enhancements/fps_unlocker/mod.rs index e358589..dfa43cc 100644 --- a/src/config/game/enhancements/fps_unlocker/mod.rs +++ b/src/config/game/enhancements/fps_unlocker/mod.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use serde::{Serialize, Deserialize}; use serde_json::Value as JsonValue; -use crate::launcher_dir; +use crate::consts::launcher_dir; pub mod config; diff --git a/src/config/game/mod.rs b/src/config/game/mod.rs index cd2fec6..8ae3cfc 100644 --- a/src/config/game/mod.rs +++ b/src/config/game/mod.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use serde::{Serialize, Deserialize}; use serde_json::Value as JsonValue; -use crate::launcher_dir; +use crate::consts::launcher_dir; pub mod wine; pub mod dxvk; diff --git a/src/config/game/wine/mod.rs b/src/config/game/wine/mod.rs index dbfa10b..6b3a36e 100644 --- a/src/config/game/wine/mod.rs +++ b/src/config/game/wine/mod.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use serde::{Serialize, Deserialize}; use serde_json::Value as JsonValue; -use crate::launcher_dir; +use crate::consts::launcher_dir; pub mod wine_sync; pub mod wine_lang; diff --git a/src/config/launcher/mod.rs b/src/config/launcher/mod.rs index ab0d8df..dc71170 100644 --- a/src/config/launcher/mod.rs +++ b/src/config/launcher/mod.rs @@ -5,7 +5,7 @@ use serde_json::Value as JsonValue; use anime_game_core::genshin::consts::GameEdition as CoreGameEdition; -use crate::launcher_dir; +use crate::consts::launcher_dir; pub mod repairer; diff --git a/src/config/mod.rs b/src/config/mod.rs index 2fc2ad8..5390c48 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,12 +1,12 @@ use std::fs::File; use std::io::Read; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::io::Write; use serde::{Serialize, Deserialize}; use serde_json::Value as JsonValue; -use crate::config_file; +use crate::consts::config_file; pub mod launcher; pub mod game; @@ -148,3 +148,60 @@ impl From<&JsonValue> for Config { } } } + +#[cfg(feature = "components")] +use crate::components::wine::{self, Version as WineVersion}; + +#[cfg(feature = "components")] +use crate::components::dxvk::{self, Version as DxvkVersion}; + +#[cfg(feature = "components")] +impl Config { + pub fn try_get_selected_wine_info(&self) -> Option { + match &self.game.wine.selected { + Some(selected) => { + wine::get_groups() + .iter() + .flat_map(|group| group.versions.clone()) + .find(|version| version.name.eq(selected)) + }, + None => None + } + } + + /// Try to get a path to the wine64 executable based on `game.wine.builds` and `game.wine.selected` + /// + /// Returns `Some("wine64")` if: + /// 1) `game.wine.selected = None` + /// 2) wine64 installed and available in system + pub fn try_get_wine_executable(&self) -> Option { + match self.try_get_selected_wine_info() { + Some(selected) => Some(self.game.wine.builds.join(selected.name).join(selected.files.wine64)), + None => { + if crate::is_available("wine64") { + Some(PathBuf::from("wine64")) + } else { + None + } + } + } + } + + /// Try to get DXVK version applied to wine prefix + /// + /// Returns: + /// 1) `Ok(Some(..))` if version was found + /// 2) `Ok(None)` if version wasn't found, so too old or dxvk is not applied + /// 3) `Err(..)` if failed to get applied dxvk version, likely because wrong prefix path specified + pub fn try_get_selected_dxvk_info(&self) -> std::io::Result> { + Ok(match wincompatlib::dxvk::Dxvk::get_version(&self.game.wine.prefix)? { + Some(version) => { + dxvk::get_groups() + .iter() + .flat_map(|group| group.versions.clone()) + .find(move |dxvk| dxvk.version == version) + }, + None => None + }) + } +} diff --git a/src/config/patch.rs b/src/config/patch.rs index 2e950a5..ec63ba1 100644 --- a/src/config/patch.rs +++ b/src/config/patch.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use serde::{Serialize, Deserialize}; use serde_json::Value as JsonValue; -use crate::launcher_dir; +use crate::consts::launcher_dir; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Patch { diff --git a/src/consts.rs b/src/consts.rs new file mode 100644 index 0000000..08d416e --- /dev/null +++ b/src/consts.rs @@ -0,0 +1,22 @@ +use std::time::Duration; +use std::path::PathBuf; + +/// Timeout used by `anime_game_core::telemetry::is_disabled` to check acessibility of telemetry servers +pub const TELEMETRY_CHECK_TIMEOUT: Option = Some(Duration::from_secs(3)); + +/// Timeout used by `anime_game_core::linux_patch::Patch::try_fetch` to fetch patch info +pub const PATCH_FETCHING_TIMEOUT: Option = Some(Duration::from_secs(5)); + +/// Get default launcher dir path +/// +/// `$HOME/.local/share/anime-game-launcher` +pub fn launcher_dir() -> Option { + dirs::data_dir().map(|dir| dir.join("anime-game-launcher")) +} + +/// Get default config file path +/// +/// `$HOME/.local/share/anime-game-launcher/config.json` +pub fn config_file() -> Option { + launcher_dir().map(|dir| dir.join("config.json")) +} diff --git a/src/lib.rs b/src/lib.rs index 253e6f0..138bd18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,29 @@ -use std::path::PathBuf; +use std::process::{Command, Stdio}; + +pub mod consts; #[cfg(feature = "config")] pub mod config; -/// Get default launcher dir path -/// -/// `$HOME/.local/share/anime-game-launcher` -pub fn launcher_dir() -> Option { - dirs::data_dir().map(|dir| dir.join("anime-game-launcher")) -} +#[cfg(feature = "states")] +pub mod states; -/// Get default config file path +#[cfg(feature = "components")] +pub mod components; + +/// Check if specified binary is available /// -/// `$HOME/.local/share/anime-game-launcher/config.json` -pub fn config_file() -> Option { - launcher_dir().map(|dir| dir.join("config.json")) +/// ``` +/// assert!(anime_launcher_sdk::is_available("bash")); +/// ``` +#[allow(unused_must_use)] +pub fn is_available(binary: &str) -> bool { + match Command::new(binary).stdout(Stdio::null()).stderr(Stdio::null()).spawn() { + Ok(mut child) => { + child.kill(); + + true + }, + Err(_) => false + } } diff --git a/src/states/mod.rs b/src/states/mod.rs new file mode 100644 index 0000000..3780c71 --- /dev/null +++ b/src/states/mod.rs @@ -0,0 +1,141 @@ +use anime_game_core::prelude::*; +use anime_game_core::genshin::prelude::*; + +use serde::{Serialize, Deserialize}; + +use crate::consts; +use crate::config; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum LauncherState { + Launch, + + /// Always contains `VersionDiff::Predownload` + PredownloadAvailable { + game: VersionDiff, + voices: Vec + }, + + PatchAvailable(Patch), + + #[cfg(feature = "components")] + WineNotInstalled, + + PrefixNotExists, + + // Always contains `VersionDiff::Diff` + VoiceUpdateAvailable(VersionDiff), + + /// Always contains `VersionDiff::Outdated` + VoiceOutdated(VersionDiff), + + /// Always contains `VersionDiff::NotInstalled` + VoiceNotInstalled(VersionDiff), + + // Always contains `VersionDiff::Diff` + GameUpdateAvailable(VersionDiff), + + /// Always contains `VersionDiff::Outdated` + GameOutdated(VersionDiff), + + /// Always contains `VersionDiff::NotInstalled` + GameNotInstalled(VersionDiff) +} + +impl Default for LauncherState { + fn default() -> Self { + Self::Launch + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum StateUpdating { + Game, + Voice(VoiceLocale), + Patch +} + +impl LauncherState { + pub fn get(status: T) -> anyhow::Result { + let config = config::get()?; + + // Check wine existence + #[cfg(feature = "components")] + { + if config.try_get_wine_executable().is_none() { + return Ok(Self::WineNotInstalled); + } + } + + // Check prefix existence + if !config.game.wine.prefix.join("drive_c").exists() { + return Ok(Self::PrefixNotExists); + } + + // Check game installation status + status(StateUpdating::Game); + + let game = Game::new(&config.game.path); + let diff = game.try_get_diff()?; + + Ok(match diff { + VersionDiff::Latest(_) | VersionDiff::Predownload { .. } => { + let mut predownload_voice = Vec::new(); + + for voice_package in &config.game.voices { + let mut voice_package = VoicePackage::with_locale(match VoiceLocale::from_str(voice_package) { + Some(locale) => locale, + None => return Err(anyhow::anyhow!("Incorrect voice locale \"{}\" specified in the config", voice_package)) + })?; + + status(StateUpdating::Voice(voice_package.locale())); + + // Replace voice package struct with the one constructed in the game's folder + // so it'll properly calculate its difference instead of saying "not installed" + if voice_package.is_installed_in(&config.game.path) { + voice_package = match VoicePackage::new(get_voice_package_path(&config.game.path, voice_package.locale())) { + Some(locale) => locale, + None => return Err(anyhow::anyhow!("Failed to load {} voice package", voice_package.locale().to_name())) + }; + } + + let diff = voice_package.try_get_diff()?; + + match diff { + VersionDiff::Latest(_) => (), + VersionDiff::Predownload { .. } => predownload_voice.push(diff), + + VersionDiff::Diff { .. } => return Ok(Self::VoiceUpdateAvailable(diff)), + VersionDiff::Outdated { .. } => return Ok(Self::VoiceOutdated(diff)), + VersionDiff::NotInstalled { .. } => return Ok(Self::VoiceNotInstalled(diff)) + } + } + + status(StateUpdating::Patch); + + let patch = Patch::try_fetch(config.patch.servers.clone(), consts::PATCH_FETCHING_TIMEOUT)?; + + if patch.is_applied(&config.game.path)? { + if let VersionDiff::Predownload { .. } = diff { + Self::PredownloadAvailable { + game: diff, + voices: predownload_voice + } + } + + else { + Self::Launch + } + } + + else { + Self::PatchAvailable(patch) + } + } + + VersionDiff::Diff { .. } => Self::GameUpdateAvailable(diff), + VersionDiff::Outdated { .. } => Self::GameOutdated(diff), + VersionDiff::NotInstalled { .. } => Self::GameNotInstalled(diff) + }) + } +}