Added work with components and states system
This commit is contained in:
parent
e7d356da5c
commit
19bf0fe01c
17 changed files with 466 additions and 24 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -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
|
||||
|
|
|
@ -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 = []
|
||||
|
||||
|
|
|
@ -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
1
components
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit f7219b5b1b50d8b09ab14142c5972656bef42900
|
104
src/components/dxvk.rs
Normal file
104
src/components/dxvk.rs
Normal 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
2
src/components/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod wine;
|
||||
pub mod dxvk;
|
99
src/components/wine.rs
Normal file
99
src/components/wine.rs
Normal 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)
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
22
src/consts.rs
Normal 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"))
|
||||
}
|
33
src/lib.rs
33
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<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
141
src/states/mod.rs
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue