feat(zzz): initial implementation
This commit is contained in:
parent
eebd048fb0
commit
d347bbe4d9
17 changed files with 1334 additions and 3 deletions
|
@ -9,7 +9,7 @@ edition = "2021"
|
|||
|
||||
[dependencies.anime-game-core]
|
||||
git = "https://github.com/an-anime-team/anime-game-core"
|
||||
tag = "1.20.2"
|
||||
tag = "1.21.0"
|
||||
features = ["all"]
|
||||
|
||||
# path = "../anime-game-core" # ! for dev purposes only
|
||||
|
@ -21,17 +21,18 @@ tracing = "0.1"
|
|||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
|
||||
cached = { version = "0.51", features = ["proc_macro"] }
|
||||
cached = { version = "0.52", features = ["proc_macro"] }
|
||||
enum-ordinalize = { version = "4.3", optional = true }
|
||||
|
||||
wincompatlib = { version = "0.7.4", features = ["all"], optional = true }
|
||||
lazy_static = { version = "1.4.0", optional = true }
|
||||
lazy_static = { version = "1.5.0", optional = true }
|
||||
md-5 = { version = "0.10", features = ["asm"], optional = true }
|
||||
discord-rich-presence = { version = "0.2.4", optional = true }
|
||||
|
||||
[features]
|
||||
genshin = ["anime-game-core/genshin"]
|
||||
star-rail = ["anime-game-core/star-rail"]
|
||||
zzz = ["anime-game-core/zzz"]
|
||||
honkai = ["anime-game-core/honkai"]
|
||||
pgr = ["anime-game-core/pgr"]
|
||||
wuwa = ["anime-game-core/wuwa"]
|
||||
|
|
|
@ -4,6 +4,9 @@ pub mod genshin;
|
|||
#[cfg(feature = "star-rail")]
|
||||
pub mod star_rail;
|
||||
|
||||
#[cfg(feature = "zzz")]
|
||||
pub mod zzz;
|
||||
|
||||
#[cfg(feature = "honkai")]
|
||||
pub mod honkai;
|
||||
|
||||
|
|
58
src/games/zzz/config/mod.rs
Normal file
58
src/games/zzz/config/mod.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
pub mod schema;
|
||||
|
||||
pub use schema::Schema;
|
||||
|
||||
use crate::config::ConfigExt;
|
||||
use crate::zzz::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.as_ref() {
|
||||
Some(config) => Ok(config.clone()),
|
||||
None => Self::get_raw()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn update(schema: Self::Schema) {
|
||||
unsafe {
|
||||
CONFIG = Some(schema);
|
||||
}
|
||||
}
|
||||
}
|
60
src/games/zzz/config/schema/components.rs
Normal file
60
src/games/zzz/config/schema/components.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use crate::zzz::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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
37
src/games/zzz/config/schema/game/enhancements.rs
Normal file
37
src/games/zzz/config/schema/game/enhancements.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
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: value.get("fsr")
|
||||
.map(Fsr::from)
|
||||
.unwrap_or(default.fsr),
|
||||
|
||||
gamemode: value.get("gamemode")
|
||||
.and_then(JsonValue::as_bool)
|
||||
.unwrap_or(default.gamemode),
|
||||
|
||||
hud: value.get("hud")
|
||||
.map(HUD::from)
|
||||
.unwrap_or(default.hud),
|
||||
|
||||
gamescope: value.get("gamescope")
|
||||
.map(Gamescope::from)
|
||||
.unwrap_or(default.gamescope)
|
||||
}
|
||||
}
|
||||
}
|
104
src/games/zzz/config/schema/game/mod.rs
Normal file
104
src/games/zzz/config/schema/game/mod.rs
Normal file
|
@ -0,0 +1,104 @@
|
|||
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::zzz::consts::launcher_dir;
|
||||
|
||||
crate::config_impl_wine_schema!(launcher_dir);
|
||||
crate::config_impl_dxvk_schema!(launcher_dir);
|
||||
|
||||
pub mod paths;
|
||||
pub mod enhancements;
|
||||
|
||||
pub mod prelude {
|
||||
pub use super::Wine;
|
||||
pub use super::Dxvk;
|
||||
|
||||
pub use super::paths::Paths;
|
||||
pub use super::enhancements::Enhancements;
|
||||
}
|
||||
|
||||
use prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Game {
|
||||
pub path: Paths,
|
||||
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 {
|
||||
Self {
|
||||
path: Paths::default(),
|
||||
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: value.get("path")
|
||||
.map(Paths::from)
|
||||
.unwrap_or(default.path),
|
||||
|
||||
wine: value.get("wine")
|
||||
.map(Wine::from)
|
||||
.unwrap_or(default.wine),
|
||||
|
||||
dxvk: value.get("dxvk")
|
||||
.map(Dxvk::from)
|
||||
.unwrap_or(default.dxvk),
|
||||
|
||||
enhancements: value.get("enhancements")
|
||||
.map(Enhancements::from)
|
||||
.unwrap_or(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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
src/games/zzz/config/schema/game/paths.rs
Normal file
54
src/games/zzz/config/schema/game/paths.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use anime_game_core::zzz::consts::GameEdition;
|
||||
|
||||
use crate::zzz::consts::launcher_dir;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Paths {
|
||||
pub global: PathBuf,
|
||||
pub china: PathBuf
|
||||
}
|
||||
|
||||
impl Paths {
|
||||
#[inline]
|
||||
/// Get game path for given edition
|
||||
pub fn for_edition(&self, edition: impl Into<GameEdition>) -> &Path {
|
||||
match edition.into() {
|
||||
GameEdition::Global => self.global.as_path(),
|
||||
GameEdition::China => self.china.as_path()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Paths {
|
||||
fn default() -> Self {
|
||||
let launcher_dir = launcher_dir().expect("Failed to get launcher dir");
|
||||
|
||||
Self {
|
||||
global: launcher_dir.join(concat!("Zen", "less Z", "one Zero")),
|
||||
china: launcher_dir.join(concat!("Zen", "less Z", "one Zero"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&JsonValue> for Paths {
|
||||
fn from(value: &JsonValue) -> Self {
|
||||
let default = Self::default();
|
||||
|
||||
Self {
|
||||
global: value.get("global")
|
||||
.and_then(JsonValue::as_str)
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or(default.global),
|
||||
|
||||
china: value.get("china")
|
||||
.and_then(JsonValue::as_str)
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or(default.china),
|
||||
}
|
||||
}
|
||||
}
|
73
src/games/zzz/config/schema/launcher/discord_rpc.rs
Normal file
73
src/games/zzz/config/schema/launcher/discord_rpc.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DiscordRpc {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
app_id: 1258318006392590336,
|
||||
enabled: false,
|
||||
|
||||
title: String::from("Exploring"),
|
||||
subtitle: String::from("New Eridu"),
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
154
src/games/zzz/config/schema/launcher/mod.rs
Normal file
154
src/games/zzz/config/schema/launcher/mod.rs
Normal file
|
@ -0,0 +1,154 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use enum_ordinalize::Ordinalize;
|
||||
|
||||
use anime_game_core::zzz::consts::GameEdition;
|
||||
|
||||
use crate::config::schema_blanks::prelude::*;
|
||||
use crate::zzz::consts::launcher_dir;
|
||||
|
||||
#[cfg(feature = "environment-emulation")]
|
||||
use crate::zzz::env_emulation::Environment;
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
pub mod discord_rpc;
|
||||
|
||||
pub mod prelude {
|
||||
pub use super::{
|
||||
Launcher,
|
||||
LauncherStyle,
|
||||
LauncherBehavior
|
||||
};
|
||||
|
||||
#[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, Copy, PartialEq, Eq, Ordinalize, Serialize, Deserialize)]
|
||||
pub enum LauncherBehavior {
|
||||
Nothing,
|
||||
Hide,
|
||||
Close
|
||||
}
|
||||
|
||||
impl Default for LauncherBehavior {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self::Hide
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Launcher {
|
||||
pub language: String,
|
||||
pub edition: GameEdition,
|
||||
pub style: LauncherStyle,
|
||||
pub temp: Option<PathBuf>,
|
||||
pub repairer: Repairer,
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
pub discord_rpc: DiscordRpc,
|
||||
|
||||
#[cfg(feature = "environment-emulation")]
|
||||
pub environment: Environment,
|
||||
|
||||
pub behavior: LauncherBehavior
|
||||
}
|
||||
|
||||
impl Default for Launcher {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
language: String::from("en-us"),
|
||||
edition: GameEdition::from_system_lang(),
|
||||
style: LauncherStyle::default(),
|
||||
temp: launcher_dir().ok(),
|
||||
repairer: Repairer::default(),
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
discord_rpc: DiscordRpc::default(),
|
||||
|
||||
#[cfg(feature = "environment-emulation")]
|
||||
environment: Environment::default(),
|
||||
|
||||
behavior: LauncherBehavior::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
|
||||
},
|
||||
|
||||
edition: match value.get("edition") {
|
||||
Some(value) => serde_json::from_value(value.clone()).unwrap_or(default.edition),
|
||||
None => default.edition
|
||||
},
|
||||
|
||||
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
|
||||
},
|
||||
|
||||
#[cfg(feature = "environment-emulation")]
|
||||
environment: match value.get("environment") {
|
||||
Some(value) => serde_json::from_value(value.clone()).unwrap_or(default.environment),
|
||||
None => default.environment
|
||||
},
|
||||
|
||||
behavior: match value.get("behavior") {
|
||||
Some(value) => serde_json::from_value(value.clone()).unwrap_or(default.behavior),
|
||||
None => default.behavior
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
120
src/games/zzz/config/schema/mod.rs
Normal file
120
src/games/zzz/config/schema/mod.rs
Normal file
|
@ -0,0 +1,120 @@
|
|||
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::{
|
||||
UnifiedWine,
|
||||
Version as WineVersion
|
||||
},
|
||||
dxvk::Version as DxvkVersion
|
||||
};
|
||||
|
||||
pub mod launcher;
|
||||
pub mod game;
|
||||
|
||||
#[cfg(feature = "components")]
|
||||
pub mod components;
|
||||
|
||||
pub mod prelude {
|
||||
pub use super::launcher::prelude::*;
|
||||
pub use super::game::prelude::*;
|
||||
|
||||
pub use super::game::*;
|
||||
|
||||
#[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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
UnifiedWine::Default(wine) => wine.prefix,
|
||||
UnifiedWine::Proton(proton) => proton.wine().prefix.clone()
|
||||
};
|
||||
|
||||
return prefix;
|
||||
}
|
||||
|
||||
self.game.wine.prefix.clone()
|
||||
}
|
||||
}
|
36
src/games/zzz/consts.rs
Normal file
36
src/games/zzz/consts.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
pub const FOLDER_NAME: &str = "sleepy-launcher";
|
||||
|
||||
/// Get default launcher dir path
|
||||
///
|
||||
/// If `LAUNCHER_FOLDER` variable is set, then its value will be returned. Otherwise return `$HOME/.local/share/sleepy-launcher`
|
||||
pub fn launcher_dir() -> anyhow::Result<PathBuf> {
|
||||
if let Ok(folder) = std::env::var("LAUNCHER_FOLDER") {
|
||||
return Ok(folder.into());
|
||||
}
|
||||
|
||||
Ok(std::env::var("XDG_DATA_HOME")
|
||||
.or_else(|_| std::env::var("HOME").map(|home| home + "/.local/share"))
|
||||
.map(|home| PathBuf::from(home).join(FOLDER_NAME))?)
|
||||
}
|
||||
|
||||
/// Get launcher's cache dir path
|
||||
///
|
||||
/// If `CACHE_FOLDER` variable is set, then its value will be returned. Otherwise return `$HOME/.cache/sleepy-launcher`
|
||||
pub fn cache_dir() -> anyhow::Result<PathBuf> {
|
||||
if let Ok(folder) = std::env::var("CACHE_FOLDER") {
|
||||
return Ok(folder.into());
|
||||
}
|
||||
|
||||
Ok(std::env::var("XDG_CACHE_HOME")
|
||||
.or_else(|_| std::env::var("HOME").map(|home| home + "/.cache"))
|
||||
.map(|home| PathBuf::from(home).join(FOLDER_NAME))?)
|
||||
}
|
||||
|
||||
/// Get config file path
|
||||
///
|
||||
/// Default is `$HOME/.local/share/sleepy-launcher/config.json`
|
||||
pub fn config_file() -> anyhow::Result<PathBuf> {
|
||||
launcher_dir().map(|dir| dir.join("config.json"))
|
||||
}
|
77
src/games/zzz/env_emulation.rs
Normal file
77
src/games/zzz/env_emulation.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use enum_ordinalize::Ordinalize;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Ordinalize)]
|
||||
pub enum Environment {
|
||||
/// `config.ini` format:
|
||||
///
|
||||
/// ```ini
|
||||
/// [General]
|
||||
/// channel=1
|
||||
/// cps=mihoyo
|
||||
/// game_version=[game version]
|
||||
/// sub_channel=0
|
||||
/// ```
|
||||
PC,
|
||||
|
||||
/// `config.ini` format:
|
||||
///
|
||||
/// ```ini
|
||||
/// [General]
|
||||
/// channel=1
|
||||
/// cps=pcseaepic
|
||||
/// game_version=[game version]
|
||||
/// # plugin_sdk_version=2.14.2 (??? not used now)
|
||||
/// sub_channel=3
|
||||
/// ```
|
||||
Epic,
|
||||
|
||||
/// `config.ini` format:
|
||||
///
|
||||
/// ```ini
|
||||
/// [General]
|
||||
/// channel=1
|
||||
/// cps=pcgoogle
|
||||
/// game_version=[game version]
|
||||
/// sub_channel=6
|
||||
/// ```
|
||||
Android
|
||||
}
|
||||
|
||||
impl Default for Environment {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self::PC
|
||||
}
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
/// Generate `config.ini`'s content
|
||||
pub fn generate_config(&self, game_version: impl AsRef<str>) -> String {
|
||||
match self {
|
||||
Self::PC => [
|
||||
"[General]",
|
||||
"channel=1",
|
||||
"cps=mihoyo",
|
||||
&format!("game_version={}", game_version.as_ref()),
|
||||
"sub_channel=0"
|
||||
].join("\n"),
|
||||
|
||||
Self::Epic => [
|
||||
"[General]",
|
||||
"channel=1",
|
||||
"cps=pcseaepic",
|
||||
&format!("game_version={}", game_version.as_ref()),
|
||||
"sub_channel=3"
|
||||
].join("\n"),
|
||||
|
||||
Self::Android => [
|
||||
"[General]",
|
||||
"channel=1",
|
||||
"cps=pcgoogle",
|
||||
&format!("game_version={}", game_version.as_ref()),
|
||||
"sub_channel=6"
|
||||
].join("\n")
|
||||
}
|
||||
}
|
||||
}
|
287
src/games/zzz/game.rs
Normal file
287
src/games/zzz/game.rs
Normal file
|
@ -0,0 +1,287 @@
|
|||
use std::process::{Command, Stdio};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anime_game_core::prelude::*;
|
||||
use anime_game_core::zzz::telemetry;
|
||||
use anime_game_core::zzz::game::Game;
|
||||
|
||||
use crate::components::wine::Bundle as WineBundle;
|
||||
|
||||
use crate::config::ConfigExt;
|
||||
use crate::zzz::config::Config;
|
||||
|
||||
use crate::config::schema_blanks::prelude::{
|
||||
WineDrives,
|
||||
AllowedDrives
|
||||
};
|
||||
|
||||
use crate::zzz::consts;
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
use crate::discord_rpc::*;
|
||||
|
||||
#[cfg(feature = "sessions")]
|
||||
use crate::{
|
||||
sessions::SessionsExt,
|
||||
zzz::sessions::Sessions
|
||||
};
|
||||
|
||||
#[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()?;
|
||||
|
||||
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();
|
||||
|
||||
let mut folders = Folders {
|
||||
wine: config.game.wine.builds.join(&wine.name),
|
||||
prefix: config.game.wine.prefix.clone(),
|
||||
game: config.game.path.for_edition(config.launcher.edition).to_path_buf(),
|
||||
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(config.launcher.edition) {
|
||||
return Err(anyhow::anyhow!("Telemetry server is not disabled: {server}"));
|
||||
}
|
||||
|
||||
// Generate `config.ini` if environment emulation feature is presented
|
||||
|
||||
#[cfg(feature = "environment-emulation")] {
|
||||
let game = Game::new(game_path, config.launcher.edition);
|
||||
|
||||
std::fs::write(
|
||||
game_path.join("config.ini"),
|
||||
config.launcher.environment.generate_config(game.get_version()?.to_string())
|
||||
)?;
|
||||
}
|
||||
|
||||
// Prepare wine prefix drives
|
||||
|
||||
config.game.wine.drives.map_folders(&folders.game, &folders.prefix)?;
|
||||
|
||||
// Workaround for sandboxing feature
|
||||
if config.sandbox.enabled {
|
||||
WineDrives::map_folder(&folders.prefix, AllowedDrives::C, "../drive_c")?;
|
||||
WineDrives::map_folder(&folders.prefix, AllowedDrives::Z, "/")?;
|
||||
}
|
||||
|
||||
// Prepare bash -c '<command>'
|
||||
// %command% = %bash_command% %windows_command% %launch_args%
|
||||
|
||||
let mut bash_command = String::new();
|
||||
let mut windows_command = String::new();
|
||||
let mut launch_args = 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 += "ZenlessZoneZero.exe ";
|
||||
|
||||
if config.game.wine.borderless {
|
||||
launch_args += "-screen-fullscreen 0 -popupwindow ";
|
||||
}
|
||||
|
||||
// https://notabug.org/Krock/dawn/src/master/TWEAKS.md
|
||||
if config.game.enhancements.fsr.enabled {
|
||||
launch_args += "-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(folders.game.join("compact_launch.bat"), format!("start {windows_command} {launch_args}\nexit"))?;
|
||||
|
||||
windows_command = String::from("compact_launch.bat");
|
||||
launch_args = String::new();
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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} {launch_args}"))
|
||||
.replace("%bash_command%", &bash_command)
|
||||
.replace("%windows_command%", &windows_command)
|
||||
.replace("%launch_args%", &launch_args),
|
||||
|
||||
// Combine bash and windows parts of the command
|
||||
None => format!("{bash_command} {windows_command} {launch_args}")
|
||||
};
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut wine_folder = folders.wine.clone();
|
||||
|
||||
if features.bundle == Some(WineBundle::Proton) {
|
||||
wine_folder.push("files");
|
||||
}
|
||||
|
||||
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.sync.get_env_vars());
|
||||
command.envs(config.game.wine.language.get_env_vars());
|
||||
command.envs(config.game.wine.shared_libraries.get_env_vars(wine_folder));
|
||||
|
||||
command.envs(&config.game.environment);
|
||||
|
||||
#[cfg(feature = "sessions")]
|
||||
if let Some(current) = Sessions::get_current()? {
|
||||
Sessions::apply(current, config.get_wine_prefix_path())?;
|
||||
}
|
||||
|
||||
// Start Discord RPC just before the game
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
let rpc = if config.launcher.discord_rpc.enabled {
|
||||
Some(DiscordRpc::new(config.launcher.discord_rpc.clone().into()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
if let Some(rpc) = &rpc {
|
||||
rpc.update(RpcUpdates::Connect)?;
|
||||
}
|
||||
|
||||
// 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.for_edition(config.launcher.edition))
|
||||
.spawn()?.wait_with_output()?;
|
||||
|
||||
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("ZenlessZoneZero") {
|
||||
break;
|
||||
}
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
if let Some(rpc) = &rpc {
|
||||
rpc.update(RpcUpdates::Update)?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
if let Some(rpc) = &rpc {
|
||||
rpc.update(RpcUpdates::Disconnect)?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "sessions")]
|
||||
if let Some(current) = Sessions::get_current()? {
|
||||
Sessions::update(current, config.get_wine_prefix_path())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
16
src/games/zzz/mod.rs
Normal file
16
src/games/zzz/mod.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
pub mod consts;
|
||||
|
||||
#[cfg(feature = "config")]
|
||||
pub mod config;
|
||||
|
||||
#[cfg(feature = "states")]
|
||||
pub mod states;
|
||||
|
||||
#[cfg(feature = "environment-emulation")]
|
||||
pub mod env_emulation;
|
||||
|
||||
#[cfg(feature = "game")]
|
||||
pub mod game;
|
||||
|
||||
#[cfg(feature = "sessions")]
|
||||
pub mod sessions;
|
105
src/games/zzz/sessions.rs
Normal file
105
src/games/zzz/sessions.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use crate::sessions::{
|
||||
SessionsExt,
|
||||
Sessions as SessionsDescriptor
|
||||
};
|
||||
|
||||
use super::consts::launcher_dir;
|
||||
|
||||
/// Get default sessions file path
|
||||
///
|
||||
/// `$HOME/.local/share/anime-game-launcher/sessions.json`
|
||||
#[inline]
|
||||
pub fn sessions_file() -> anyhow::Result<PathBuf> {
|
||||
launcher_dir().map(|dir| dir.join("sessions.json"))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SessionData {
|
||||
// [Software\\miHoYo\\Zenless Zone Zero]
|
||||
pub game_reg: String,
|
||||
|
||||
// [Software\\miHoYoSDK]
|
||||
pub sdk_reg: String
|
||||
}
|
||||
|
||||
pub struct Sessions;
|
||||
|
||||
impl SessionsExt for Sessions {
|
||||
type SessionData = SessionData;
|
||||
|
||||
fn get_sessions() -> anyhow::Result<SessionsDescriptor<Self::SessionData>> {
|
||||
let path = sessions_file()?;
|
||||
|
||||
if !path.exists() {
|
||||
tracing::warn!("Session file doesn't exist. Returning default value");
|
||||
|
||||
return Ok(SessionsDescriptor::default());
|
||||
}
|
||||
|
||||
Ok(serde_json::from_slice(&std::fs::read(path)?)?)
|
||||
}
|
||||
|
||||
fn set_sessions(sessions: SessionsDescriptor<Self::SessionData>) -> anyhow::Result<()> {
|
||||
Ok(std::fs::write(sessions_file()?, serde_json::to_string_pretty(&sessions)?)?)
|
||||
}
|
||||
|
||||
fn update(name: String, prefix: impl AsRef<Path>) -> anyhow::Result<()> {
|
||||
let mut sessions = Self::get_sessions()?;
|
||||
|
||||
tracing::info!("Updating session '{name}' from prefix: {:?}", prefix.as_ref());
|
||||
|
||||
let mut new_session = Self::SessionData {
|
||||
game_reg: String::new(),
|
||||
sdk_reg: String::new()
|
||||
};
|
||||
|
||||
for entry in std::fs::read_to_string(prefix.as_ref().join("user.reg"))?.split("\n\n") {
|
||||
if entry.starts_with("[Software\\\\miHoYo\\\\Zenless Zone Zero]") {
|
||||
new_session.game_reg = entry.to_owned();
|
||||
}
|
||||
|
||||
else if entry.starts_with("[Software\\\\miHoYoSDK]") {
|
||||
new_session.sdk_reg = entry.to_owned();
|
||||
}
|
||||
}
|
||||
|
||||
sessions.sessions.insert(name, new_session);
|
||||
|
||||
Self::set_sessions(sessions)
|
||||
}
|
||||
|
||||
fn apply(name: String, prefix: impl AsRef<Path>) -> anyhow::Result<()> {
|
||||
let sessions = Self::get_sessions()?;
|
||||
|
||||
let Some(session) = sessions.sessions.get(&name) else {
|
||||
anyhow::bail!("Session with given name doesn't exist");
|
||||
};
|
||||
|
||||
tracing::info!("Applying session '{name}' to prefix: {:?}", prefix.as_ref());
|
||||
|
||||
let entries: String = std::fs::read_to_string(prefix.as_ref().join("user.reg"))?
|
||||
.split("\n\n")
|
||||
.map(|entry| {
|
||||
let new_entry = if entry.starts_with("[Software\\\\miHoYo\\\\Zenless Zone Zero]") {
|
||||
session.game_reg.clone()
|
||||
}
|
||||
|
||||
else if entry.starts_with("[Software\\\\miHoYoSDK]") {
|
||||
session.sdk_reg.clone()
|
||||
}
|
||||
|
||||
else {
|
||||
entry.to_owned()
|
||||
};
|
||||
|
||||
new_entry + "\n\n"
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(std::fs::write(prefix.as_ref().join("user.reg"), format!("{}\n", entries.trim_end()))?)
|
||||
}
|
||||
}
|
143
src/games/zzz/states.rs
Normal file
143
src/games/zzz/states.rs
Normal file
|
@ -0,0 +1,143 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anime_game_core::prelude::*;
|
||||
use anime_game_core::zzz::prelude::*;
|
||||
|
||||
use crate::config::ConfigExt;
|
||||
use crate::zzz::config::Config;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LauncherState {
|
||||
Launch,
|
||||
|
||||
/// Always contains `VersionDiff::Predownload`
|
||||
PredownloadAvailable {
|
||||
game: VersionDiff
|
||||
},
|
||||
|
||||
FolderMigrationRequired {
|
||||
from: PathBuf,
|
||||
to: PathBuf,
|
||||
cleanup_folder: Option<PathBuf>
|
||||
},
|
||||
|
||||
TelemetryNotDisabled,
|
||||
|
||||
#[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)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StateUpdating {
|
||||
Game
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LauncherStateParams<F: Fn(StateUpdating)> {
|
||||
pub game_path: PathBuf,
|
||||
pub game_edition: GameEdition,
|
||||
pub wine_prefix: 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(¶ms.game_path, params.game_edition);
|
||||
|
||||
let diff = game.try_get_diff()?;
|
||||
|
||||
match diff {
|
||||
VersionDiff::Latest { .. } | VersionDiff::Predownload { .. } => {
|
||||
// Check telemetry servers
|
||||
let disabled = telemetry::is_disabled(params.game_edition)
|
||||
|
||||
// Return true if there's no domain name resolved, or false otherwise
|
||||
.map(|result| result.is_none())
|
||||
|
||||
// And return true if there's an error happened during domain name resolving
|
||||
// FIXME: might not be a good idea? Idk
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::warn!("Failed to check telemetry servers: {err}. Assuming they're disabled");
|
||||
|
||||
true
|
||||
});
|
||||
|
||||
if !disabled {
|
||||
return Ok(Self::TelemetryNotDisabled);
|
||||
}
|
||||
|
||||
// Check if update predownload available
|
||||
if let VersionDiff::Predownload { .. } = diff {
|
||||
Ok(Self::PredownloadAvailable {
|
||||
game: 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 {
|
||||
game_path: config.game.path.for_edition(config.launcher.edition).to_path_buf(),
|
||||
game_edition: config.launcher.edition,
|
||||
wine_prefix: config.get_wine_prefix_path(),
|
||||
|
||||
status_updater
|
||||
})
|
||||
}
|
||||
}
|
|
@ -11,6 +11,9 @@ pub use games::genshin;
|
|||
#[cfg(feature = "star-rail")]
|
||||
pub use games::star_rail;
|
||||
|
||||
#[cfg(feature = "zzz")]
|
||||
pub use games::zzz;
|
||||
|
||||
#[cfg(feature = "honkai")]
|
||||
pub use games::honkai;
|
||||
|
||||
|
|
Loading…
Reference in a new issue