feat(star-rail): added initial HSR support

This commit is contained in:
Observer KRypt0n_ 2023-04-22 23:31:00 +02:00
parent c72815e906
commit ed246f079b
No known key found for this signature in database
GPG key ID: 844DA47BA25FE1E2
14 changed files with 1046 additions and 3 deletions

View file

@ -7,11 +7,11 @@ readme = "README.md"
edition = "2021" edition = "2021"
[dependencies.anime-game-core] [dependencies.anime-game-core]
git = "https://github.com/an-anime-team/anime-game-core" # git = "https://github.com/an-anime-team/anime-game-core"
tag = "1.7.4" # tag = "1.7.4"
features = ["all"] features = ["all"]
# path = "../anime-game-core" # ! for dev purposes only path = "../anime-game-core" # ! for dev purposes only
[dependencies] [dependencies]
anyhow = { version = "1.0", features = ["backtrace"] } anyhow = { version = "1.0", features = ["backtrace"] }
@ -31,6 +31,7 @@ discord-rich-presence = { version = "0.2.3", optional = true }
[features] [features]
genshin = ["anime-game-core/genshin"] genshin = ["anime-game-core/genshin"]
honkai = ["anime-game-core/honkai"] honkai = ["anime-game-core/honkai"]
star-rail = ["anime-game-core/star-rail"]
# Common features # Common features
states = [] states = []

View file

@ -9,6 +9,9 @@ pub mod genshin;
#[cfg(feature = "honkai")] #[cfg(feature = "honkai")]
pub mod honkai; pub mod honkai;
#[cfg(feature = "star-rail")]
pub mod star_rail;
#[cfg(feature = "config")] #[cfg(feature = "config")]
pub mod config; pub mod config;

View file

@ -0,0 +1,58 @@
use std::path::PathBuf;
pub mod schema;
pub use schema::Schema;
use crate::config::ConfigExt;
use crate::star_rail::consts::config_file;
static mut CONFIG: Option<schema::Schema> = None;
pub struct Config;
impl ConfigExt for Config {
type Schema = schema::Schema;
#[inline]
fn config_file() -> PathBuf {
config_file().expect("Failed to resolve config file path")
}
#[inline]
fn default_schema() -> Self::Schema {
Self::Schema::default()
}
#[inline]
fn serialize_schema(schema: Self::Schema) -> anyhow::Result<String> {
Ok(serde_json::to_string_pretty(&schema)?)
}
#[inline]
fn deserialize_schema<T: AsRef<str>>(schema: T) -> anyhow::Result<Self::Schema> {
Ok(Self::Schema::from(&serde_json::from_str(schema.as_ref())?))
}
#[inline]
fn clone_schema(schema: &Self::Schema) -> Self::Schema {
schema.clone()
}
#[inline]
fn get() -> anyhow::Result<Self::Schema> {
unsafe {
match &CONFIG {
Some(config) => Ok(config.clone()),
None => Self::get_raw()
}
}
}
#[inline]
fn update(schema: Self::Schema) {
unsafe {
CONFIG = Some(schema);
}
}
}

View file

@ -0,0 +1,60 @@
use std::path::PathBuf;
use serde::{Serialize, Deserialize};
use serde_json::Value as JsonValue;
use crate::star_rail::consts::launcher_dir;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Components {
pub path: PathBuf,
pub servers: Vec<String>
}
impl Default for Components {
#[inline]
fn default() -> Self {
let launcher_dir = launcher_dir().expect("Failed to get launcher dir");
Self {
path: launcher_dir.join("components"),
servers: vec![
String::from("https://github.com/an-anime-team/components")
]
}
}
}
impl From<&JsonValue> for Components {
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
},
servers: match value.get("servers") {
Some(value) => match value.as_array() {
Some(values) => {
let mut servers = Vec::new();
for value in values {
if let Some(server) = value.as_str() {
servers.push(server.to_string());
}
}
servers
},
None => default.servers
},
None => default.servers
}
}
}
}

View file

@ -0,0 +1,40 @@
use serde::{Serialize, Deserialize};
use serde_json::Value as JsonValue;
use crate::config::schema_blanks::prelude::*;
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Enhancements {
pub fsr: Fsr,
pub gamemode: bool,
pub hud: HUD,
pub gamescope: Gamescope
}
impl From<&JsonValue> for Enhancements {
fn from(value: &JsonValue) -> Self {
let default = Self::default();
Self {
fsr: match value.get("fsr") {
Some(value) => Fsr::from(value),
None => default.fsr
},
gamemode: match value.get("gamemode") {
Some(value) => value.as_bool().unwrap_or(default.gamemode),
None => default.gamemode
},
hud: match value.get("hud") {
Some(value) => HUD::from(value),
None => default.hud
},
gamescope: match value.get("gamescope") {
Some(value) => Gamescope::from(value),
None => default.gamescope
}
}
}
}

View file

@ -0,0 +1,111 @@
use std::collections::HashMap;
use std::path::PathBuf;
use serde::{Serialize, Deserialize};
use serde_json::Value as JsonValue;
use crate::config::schema_blanks::prelude::*;
use crate::star_rail::consts::launcher_dir;
crate::config_impl_wine_schema!(launcher_dir);
crate::config_impl_dxvk_schema!(launcher_dir);
pub mod enhancements;
pub mod prelude {
pub use super::Wine;
pub use super::Dxvk;
pub use super::enhancements::Enhancements;
}
use prelude::*;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Game {
pub path: PathBuf,
pub wine: Wine,
pub dxvk: Dxvk,
pub enhancements: Enhancements,
pub environment: HashMap<String, String>,
pub command: Option<String>
}
impl Default for Game {
#[inline]
fn default() -> Self {
let launcher_dir = launcher_dir().expect("Failed to get launcher dir");
Self {
path: launcher_dir.join(concat!("Hon", "kai Sta", "r Rail")),
wine: Wine::default(),
dxvk: Dxvk::default(),
enhancements: Enhancements::default(),
environment: HashMap::new(),
command: None
}
}
}
impl From<&JsonValue> for Game {
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
},
wine: match value.get("wine") {
Some(value) => Wine::from(value),
None => default.wine
},
dxvk: match value.get("dxvk") {
Some(value) => Dxvk::from(value),
None => default.dxvk
},
enhancements: match value.get("enhancements") {
Some(value) => Enhancements::from(value),
None => default.enhancements
},
environment: match value.get("environment") {
Some(value) => match value.as_object() {
Some(values) => {
let mut vars = HashMap::new();
for (name, value) in values {
if let Some(value) = value.as_str() {
vars.insert(name.clone(), value.to_string());
}
}
vars
},
None => default.environment
},
None => default.environment
},
command: match value.get("command") {
Some(value) => {
if value.is_null() {
None
} else {
match value.as_str() {
Some(value) => Some(value.to_string()),
None => default.command
}
}
},
None => default.command
}
}
}
}

View file

@ -0,0 +1,75 @@
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use crate::discord_rpc::DiscordRpcParams;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiscordRpc {
pub app_id: u64,
pub enabled: bool,
pub title: String,
pub subtitle: String,
pub icon: String
}
impl From<DiscordRpc> for DiscordRpcParams {
#[inline]
fn from(config: DiscordRpc) -> Self {
Self {
app_id: config.app_id,
enabled: config.enabled,
title: config.title,
subtitle: config.subtitle,
icon: config.icon
}
}
}
// TODO: add honkers-specific discord rpc
impl Default for DiscordRpc {
#[inline]
fn default() -> Self {
Self {
app_id: 901534333360304168,
enabled: false,
title: String::from("Researching the world"),
subtitle: String::from("of Teyvat"),
icon: String::from("launcher")
}
}
}
impl From<&JsonValue> for DiscordRpc {
fn from(value: &JsonValue) -> Self {
let default = Self::default();
Self {
app_id: match value.get("app_id") {
Some(value) => value.as_u64().unwrap_or(default.app_id),
None => default.app_id
},
enabled: match value.get("enabled") {
Some(value) => value.as_bool().unwrap_or(default.enabled),
None => default.enabled
},
title: match value.get("title") {
Some(value) => value.as_str().unwrap_or(&default.title).to_string(),
None => default.title
},
subtitle: match value.get("subtitle") {
Some(value) => value.as_str().unwrap_or(&default.subtitle).to_string(),
None => default.subtitle
},
icon: match value.get("icon") {
Some(value) => value.as_str().unwrap_or(&default.icon).to_string(),
None => default.icon
}
}
}
}

View file

@ -0,0 +1,101 @@
use std::path::PathBuf;
use serde::{Serialize, Deserialize};
use serde_json::Value as JsonValue;
use enum_ordinalize::Ordinalize;
use crate::config::schema_blanks::prelude::*;
use crate::star_rail::consts::launcher_dir;
#[cfg(feature = "discord-rpc")]
pub mod discord_rpc;
pub mod prelude {
#[cfg(feature = "discord-rpc")]
pub use super::discord_rpc::DiscordRpc;
}
use prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ordinalize, Serialize, Deserialize)]
pub enum LauncherStyle {
Modern,
Classic
}
impl Default for LauncherStyle {
#[inline]
fn default() -> Self {
Self::Modern
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Launcher {
pub language: String,
pub style: LauncherStyle,
pub temp: Option<PathBuf>,
pub repairer: Repairer,
#[cfg(feature = "discord-rpc")]
pub discord_rpc: DiscordRpc
}
impl Default for Launcher {
#[inline]
fn default() -> Self {
Self {
language: String::from("en-us"),
style: LauncherStyle::default(),
temp: launcher_dir().ok(),
repairer: Repairer::default(),
#[cfg(feature = "discord-rpc")]
discord_rpc: DiscordRpc::default()
}
}
}
impl From<&JsonValue> for Launcher {
fn from(value: &JsonValue) -> Self {
let default = Self::default();
Self {
language: match value.get("language") {
Some(value) => value.as_str().unwrap_or(&default.language).to_string(),
None => default.language
},
style: match value.get("style") {
Some(value) => serde_json::from_value(value.to_owned()).unwrap_or_default(),
None => default.style
},
temp: match value.get("temp") {
Some(value) => {
if value.is_null() {
None
} else {
match value.as_str() {
Some(value) => Some(PathBuf::from(value)),
None => default.temp
}
}
},
None => default.temp
},
repairer: match value.get("repairer") {
Some(value) => Repairer::from(value),
None => default.repairer
},
#[cfg(feature = "discord-rpc")]
discord_rpc: match value.get("discord_rpc") {
Some(value) => DiscordRpc::from(value),
None => default.discord_rpc
}
}
}
}

View file

@ -0,0 +1,132 @@
use std::path::PathBuf;
use serde::{Serialize, Deserialize};
use serde_json::Value as JsonValue;
use wincompatlib::prelude::*;
#[cfg(feature = "sandbox")]
use crate::config::schema_blanks::sandbox::Sandbox;
#[cfg(feature = "components")]
use crate::components::wine::{
WincompatlibWine,
Version as WineVersion
};
#[cfg(feature = "components")]
use crate::components::dxvk::Version as DxvkVersion;
pub mod launcher;
pub mod game;
pub mod patch;
#[cfg(feature = "components")]
pub mod components;
pub mod prelude {
pub use super::launcher::prelude::*;
pub use super::game::prelude::*;
pub use super::launcher::*;
pub use super::game::*;
pub use super::patch::*;
#[cfg(feature = "components")]
pub use super::components::*;
}
use prelude::*;
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Schema {
pub launcher: Launcher,
pub game: Game,
#[cfg(feature = "sandbox")]
pub sandbox: Sandbox,
#[cfg(feature = "components")]
pub components: Components,
pub patch: Patch
}
impl From<&JsonValue> for Schema {
fn from(value: &JsonValue) -> Self {
let default = Self::default();
Self {
launcher: match value.get("launcher") {
Some(value) => Launcher::from(value),
None => default.launcher
},
game: match value.get("game") {
Some(value) => Game::from(value),
None => default.game
},
#[cfg(feature = "sandbox")]
sandbox: match value.get("sandbox") {
Some(value) => Sandbox::from(value),
None => default.sandbox
},
#[cfg(feature = "components")]
components: match value.get("components") {
Some(value) => Components::from(value),
None => default.components
},
patch: match value.get("patch") {
Some(value) => Patch::from(value),
None => default.patch
},
}
}
}
impl Schema {
#[cfg(feature = "components")]
/// Get selected wine version
pub fn get_selected_wine(&self) -> anyhow::Result<Option<WineVersion>> {
match &self.game.wine.selected {
Some(selected) => WineVersion::find_in(&self.components.path, selected),
None => Ok(None)
}
}
#[cfg(feature = "components")]
/// Get selected dxvk version
pub fn get_selected_dxvk(&self) -> anyhow::Result<Option<DxvkVersion>> {
match wincompatlib::dxvk::Dxvk::get_version(&self.game.wine.prefix)? {
Some(version) => DxvkVersion::find_in(&self.components.path, version),
None => Ok(None)
}
}
#[cfg(feature = "components")]
/// Resolve real wine prefix path using wincompatlib
///
/// - For general wine build returns `game.wine.prefix`
/// - For proton-like builds return `game.wine.prefix`/`pfx`
pub fn get_wine_prefix_path(&self) -> PathBuf {
if let Ok(Some(wine)) = self.get_selected_wine() {
let wine = wine
.to_wine(&self.components.path, Some(&self.game.wine.builds.join(&wine.name)))
.with_prefix(&self.game.wine.prefix);
let prefix = match wine {
WincompatlibWine::Default(wine) => wine.prefix,
WincompatlibWine::Proton(proton) => proton.wine().prefix.clone()
};
if let Some(prefix) = prefix {
return prefix;
}
}
self.game.wine.prefix.clone()
}
}

View file

@ -0,0 +1,78 @@
use std::path::PathBuf;
use serde::{Serialize, Deserialize};
use serde_json::Value as JsonValue;
use crate::star_rail::consts::launcher_dir;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Patch {
pub path: PathBuf,
pub servers: Vec<String>,
pub apply_xlua: bool,
pub root: bool
}
impl Default for Patch {
#[inline]
fn default() -> Self {
let launcher_dir = launcher_dir().expect("Failed to get launcher dir");
Self {
path: launcher_dir.join("patch"),
servers: vec![
String::from("https://notabug.org/Krock/dawn")
],
apply_xlua: true,
// Disable root requirement for patching if we're running launcher in flatpak
root: !PathBuf::from("/.flatpak-info").exists()
}
}
}
impl From<&JsonValue> for Patch {
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
},
servers: match value.get("servers") {
Some(value) => match value.as_array() {
Some(values) => {
let mut servers = Vec::new();
for value in values {
if let Some(server) = value.as_str() {
servers.push(server.to_string());
}
}
servers
},
None => default.servers
},
None => default.servers
},
apply_xlua: match value.get("apply_xlua") {
Some(value) => value.as_bool().unwrap_or(default.apply_xlua),
None => default.apply_xlua
},
root: match value.get("root") {
Some(value) => value.as_bool().unwrap_or(default.root),
None => default.root
}
}
}
}

19
src/star_rail/consts.rs Normal file
View file

@ -0,0 +1,19 @@
use std::path::PathBuf;
/// Get default launcher dir path
///
/// `$HOME/.local/share/honkers-railway-launcher`
#[inline]
pub fn launcher_dir() -> anyhow::Result<PathBuf> {
Ok(std::env::var("XDG_DATA_HOME")
.or_else(|_| std::env::var("HOME").map(|home| home + "/.local/share"))
.map(|home| PathBuf::from(home).join("honkers-railway-launcher"))?)
}
/// Get default config file path
///
/// `$HOME/.local/share/honkers-railway-launcher/config.json`
#[inline]
pub fn config_file() -> anyhow::Result<PathBuf> {
launcher_dir().map(|dir| dir.join("config.json"))
}

221
src/star_rail/game.rs Normal file
View file

@ -0,0 +1,221 @@
use std::process::{Command, Stdio};
use std::path::PathBuf;
use anime_game_core::star_rail::telemetry;
use crate::config::ConfigExt;
use crate::star_rail::config::Config;
use crate::star_rail::consts;
#[cfg(feature = "discord-rpc")]
use crate::discord_rpc::*;
#[derive(Debug, Clone)]
struct Folders {
pub wine: PathBuf,
pub prefix: PathBuf,
pub game: PathBuf,
pub temp: PathBuf
}
fn replace_keywords(command: impl ToString, folders: &Folders) -> String {
command.to_string()
.replace("%build%", folders.wine.to_str().unwrap())
.replace("%prefix%", folders.prefix.to_str().unwrap())
.replace("%temp%", folders.game.to_str().unwrap())
.replace("%launcher%", &consts::launcher_dir().unwrap().to_string_lossy())
.replace("%game%", folders.temp.to_str().unwrap())
}
/// 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()?;
if !config.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();
let mut folders = Folders {
wine: config.game.wine.builds.join(&wine.name),
prefix: config.game.wine.prefix.clone(),
game: config.game.path.clone(),
temp: config.launcher.temp.clone().unwrap_or(std::env::temp_dir())
};
// Check telemetry servers
tracing::info!("Checking telemetry");
if let Ok(Some(server)) = telemetry::is_disabled() {
return Err(anyhow::anyhow!("Telemetry server is not disabled: {server}"));
}
// 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 run_command = features.command
.map(|command| replace_keywords(command, &folders))
.unwrap_or(format!("'{}'", folders.wine.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 += "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}");
}
// bwrap <params> -- <command to run>
#[cfg(feature = "sandbox")]
if config.sandbox.enabled {
let bwrap = config.sandbox.get_command(
folders.wine.to_str().unwrap(),
folders.prefix.to_str().unwrap(),
folders.game.to_str().unwrap()
);
let sandboxed_folders = Folders {
wine: PathBuf::from("/tmp/sandbox/wine"),
prefix: PathBuf::from("/tmp/sandbox/prefix"),
game: PathBuf::from("/tmp/sandbox/game"),
temp: PathBuf::from("/tmp")
};
bash_command = bash_command
.replace(folders.wine.to_str().unwrap(), sandboxed_folders.wine.to_str().unwrap())
.replace(folders.prefix.to_str().unwrap(), sandboxed_folders.prefix.to_str().unwrap())
.replace(folders.game.to_str().unwrap(), sandboxed_folders.game.to_str().unwrap())
.replace(folders.temp.to_str().unwrap(), sandboxed_folders.temp.to_str().unwrap());
bash_command = format!("{bwrap} --chdir /tmp/sandbox/game -- {bash_command}");
folders = sandboxed_folders;
}
// Bundle all windows arguments used to run the game into a single file
if features.compact_launch {
std::fs::write(folders.game.join("compact_launch.bat"), format!("start {windows_command}\nexit"))?;
windows_command = String::from("compact_launch.bat");
}
// Finalize launching command
bash_command = match &config.game.command {
// Use user-given launch command
Some(command) => replace_keywords(command, &folders)
.replace("%command%", &format!("{bash_command} {windows_command}"))
.replace("%bash_command%", &bash_command)
.replace("%windows_command%", &windows_command),
// Combine bash and windows parts of the command
None => format!("{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", &folders.prefix);
// Add environment flags for selected wine
for (key, value) in features.env.into_iter() {
command.env(key, replace_keywords(value, &folders));
}
// 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, &folders));
}
}
}
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}\"");
// We use real current dir here because sandboxed one
// obviously doesn't exist
command.current_dir(&config.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(())
}

10
src/star_rail/mod.rs Normal file
View file

@ -0,0 +1,10 @@
pub mod consts;
#[cfg(feature = "config")]
pub mod config;
#[cfg(feature = "states")]
pub mod states;
#[cfg(feature = "game")]
pub mod game;

134
src/star_rail/states.rs Normal file
View file

@ -0,0 +1,134 @@
use std::path::PathBuf;
use serde::{Serialize, Deserialize};
use anime_game_core::prelude::*;
use anime_game_core::star_rail::prelude::*;
use crate::config::ConfigExt;
use crate::star_rail::config::Config;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum LauncherState {
Launch,
/// Always contains `VersionDiff::Predownload`
PredownloadAvailable(VersionDiff),
MainPatchAvailable(MainPatch),
#[cfg(feature = "components")]
WineNotInstalled,
PrefixNotExists,
// Always contains `VersionDiff::Diff`
GameUpdateAvailable(VersionDiff),
/// Always contains `VersionDiff::Outdated`
GameOutdated(VersionDiff),
/// Always contains `VersionDiff::NotInstalled`
GameNotInstalled(VersionDiff)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum StateUpdating {
Game,
Patch
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LauncherStateParams<F: Fn(StateUpdating)> {
pub wine_prefix: PathBuf,
pub game_path: PathBuf,
pub patch_servers: Vec<String>,
pub patch_folder: PathBuf,
pub status_updater: F
}
impl LauncherState {
pub fn get<F: Fn(StateUpdating)>(params: LauncherStateParams<F>) -> anyhow::Result<Self> {
tracing::debug!("Trying to get launcher state");
// Check prefix existence
if !params.wine_prefix.join("drive_c").exists() {
return Ok(Self::PrefixNotExists);
}
// Check game installation status
(params.status_updater)(StateUpdating::Game);
let game = Game::new(&params.game_path);
let diff = game.try_get_diff()?;
match diff {
VersionDiff::Latest(_) | VersionDiff::Predownload { .. } => {
// Check game patch status
(params.status_updater)(StateUpdating::Patch);
let patch = Patch::new(&params.patch_folder);
// Sync local patch folder with remote if needed
// TODO: maybe I shouldn't do it here?
if patch.is_sync(&params.patch_servers)?.is_none() {
for server in &params.patch_servers {
if patch.sync(server).is_ok() {
break;
}
}
}
// Check the main patch
let main_patch = patch.main_patch()?;
if !main_patch.is_applied(&params.game_path)? {
return Ok(Self::MainPatchAvailable(main_patch));
}
// Check if update predownload available
if let VersionDiff::Predownload { .. } = diff {
Ok(Self::PredownloadAvailable(diff))
}
// Otherwise we can launch the game
else {
Ok(Self::Launch)
}
}
VersionDiff::Diff { .. } => Ok(Self::GameUpdateAvailable(diff)),
VersionDiff::Outdated { .. } => Ok(Self::GameOutdated(diff)),
VersionDiff::NotInstalled { .. } => Ok(Self::GameNotInstalled(diff))
}
}
#[cfg(feature = "config")]
#[tracing::instrument(level = "debug", skip(status_updater), ret)]
pub fn get_from_config<T: Fn(StateUpdating)>(status_updater: T) -> anyhow::Result<Self> {
tracing::debug!("Trying to get launcher state");
let config = Config::get()?;
match &config.game.wine.selected {
#[cfg(feature = "components")]
Some(selected) if !config.game.wine.builds.join(selected).exists() => return Ok(Self::WineNotInstalled),
None => return Ok(Self::WineNotInstalled),
_ => ()
}
Self::get(LauncherStateParams {
wine_prefix: config.get_wine_prefix_path(),
game_path: config.game.path,
patch_servers: config.patch.servers,
patch_folder: config.patch.path,
status_updater
})
}
}