Added work with components and states system

This commit is contained in:
Observer KRypt0n_ 2022-11-11 23:31:36 +02:00
parent e7d356da5c
commit 19bf0fe01c
No known key found for this signature in database
GPG key ID: 844DA47BA25FE1E2
17 changed files with 466 additions and 24 deletions

3
.gitmodules vendored
View file

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

View file

@ -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 = []

View file

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

1
components Submodule

@ -0,0 +1 @@
Subproject commit f7219b5b1b50d8b09ab14142c5972656bef42900

104
src/components/dxvk.rs Normal file
View file

@ -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<Group> = vec![
Group {
name: String::from("Vanilla"),
versions: serde_json::from_str::<Vec<Version>>(include_str!("../../components/dxvk/vanilla.json")).unwrap().into_iter().take(12).collect()
},
Group {
name: String::from("Async"),
versions: serde_json::from_str::<Vec<Version>>(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<Version>
}
#[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<T: Into<PathBuf>>(&self, folder: T) -> bool {
folder.into().join(&self.name).exists()
}
pub fn apply<T: Into<PathBuf>>(&self, dxvks_folder: T, prefix_path: T) -> anyhow::Result<Output> {
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<Group> {
GROUPS.clone()
}
/// List downloaded DXVK versions in some specific folder
pub fn get_downloaded<T: Into<PathBuf>>(folder: T) -> std::io::Result<Vec<Version>> {
let mut downloaded = Vec::new();
let list = get_groups()
.into_iter()
.flat_map(|group| group.versions)
.collect::<Vec<Version>>();
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)
}

2
src/components/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod wine;
pub mod dxvk;

99
src/components/wine.rs Normal file
View file

@ -0,0 +1,99 @@
use std::path::PathBuf;
use serde::{Serialize, Deserialize};
use wincompatlib::prelude::*;
lazy_static::lazy_static! {
static ref GROUPS: Vec<Group> = vec![
Group {
name: String::from("Wine-GE-Proton"),
versions: serde_json::from_str::<Vec<Version>>(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::<Vec<Version>>(include_str!("../../components/wine/ge-proton.json")).unwrap().into_iter().take(12).collect()
},
Group {
name: String::from("Soda"),
versions: serde_json::from_str::<Vec<Version>>(include_str!("../../components/wine/soda.json")).unwrap().into_iter().take(12).collect()
},
Group {
name: String::from("Lutris"),
versions: serde_json::from_str::<Vec<Version>>(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<Version>
}
#[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<T: Into<PathBuf>>(&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<Group> {
GROUPS.clone()
}
pub fn get_downloaded<T: Into<PathBuf>>(folder: T) -> std::io::Result<Vec<Version>> {
let mut downloaded = Vec::new();
let list = get_groups()
.into_iter()
.flat_map(|group| group.versions)
.collect::<Vec<Version>>();
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)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<WineVersion> {
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<PathBuf> {
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<Option<DxvkVersion>> {
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
})
}
}

View file

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

22
src/consts.rs Normal file
View file

@ -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<Duration> = 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<Duration> = Some(Duration::from_secs(5));
/// Get default launcher dir path
///
/// `$HOME/.local/share/anime-game-launcher`
pub fn launcher_dir() -> Option<PathBuf> {
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<PathBuf> {
launcher_dir().map(|dir| dir.join("config.json"))
}

View file

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

141
src/states/mod.rs Normal file
View file

@ -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<VersionDiff>
},
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<T: Fn(StateUpdating)>(status: T) -> anyhow::Result<Self> {
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)
})
}
}