feat: initial wuwa support
This commit is contained in:
parent
dce10eb82f
commit
d2e436a05c
16 changed files with 1199 additions and 5 deletions
|
@ -9,7 +9,7 @@ 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.17.9"
|
tag = "1.18.0"
|
||||||
features = ["all"]
|
features = ["all"]
|
||||||
|
|
||||||
# path = "../anime-game-core" # ! for dev purposes only
|
# path = "../anime-game-core" # ! for dev purposes only
|
||||||
|
@ -34,13 +34,12 @@ genshin = ["anime-game-core/genshin"]
|
||||||
star-rail = ["anime-game-core/star-rail"]
|
star-rail = ["anime-game-core/star-rail"]
|
||||||
honkai = ["anime-game-core/honkai"]
|
honkai = ["anime-game-core/honkai"]
|
||||||
pgr = ["anime-game-core/pgr"]
|
pgr = ["anime-game-core/pgr"]
|
||||||
|
wuwa = ["anime-game-core/wuwa"]
|
||||||
|
|
||||||
star-rail-patch = ["anime-game-core/patch-jadeite"]
|
star-rail-patch = ["anime-game-core/patch-jadeite"]
|
||||||
honkai-patch = [
|
honkai-patch = ["anime-game-core/patch-jadeite"]
|
||||||
"anime-game-core/patch-jadeite",
|
|
||||||
"anime-game-core/patch-mfplat"
|
|
||||||
]
|
|
||||||
pgr-patch = ["anime-game-core/patch-mfc140"]
|
pgr-patch = ["anime-game-core/patch-mfc140"]
|
||||||
|
wuwa-patch = ["anime-game-core/patch-mfc140"]
|
||||||
|
|
||||||
# Common features
|
# Common features
|
||||||
states = []
|
states = []
|
||||||
|
|
|
@ -9,3 +9,6 @@ pub mod honkai;
|
||||||
|
|
||||||
#[cfg(feature = "pgr")]
|
#[cfg(feature = "pgr")]
|
||||||
pub mod pgr;
|
pub mod pgr;
|
||||||
|
|
||||||
|
#[cfg(feature = "wuwa")]
|
||||||
|
pub mod wuwa;
|
||||||
|
|
58
src/games/wuwa/config/mod.rs
Normal file
58
src/games/wuwa/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::wuwa::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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
60
src/games/wuwa/config/schema/components.rs
Normal file
60
src/games/wuwa/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::wuwa::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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
src/games/wuwa/config/schema/game/enhancements.rs
Normal file
36
src/games/wuwa/config/schema/game/enhancements.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
107
src/games/wuwa/config/schema/game/mod.rs
Normal file
107
src/games/wuwa/config/schema/game/mod.rs
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
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::wuwa::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("PGR"),
|
||||||
|
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: 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
73
src/games/wuwa/config/schema/launcher/discord_rpc.rs
Normal file
73
src/games/wuwa/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: 1116308640945688677,
|
||||||
|
enabled: false,
|
||||||
|
|
||||||
|
title: String::from("Commanding the"),
|
||||||
|
subtitle: String::from("Grey Raven unit"),
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
130
src/games/wuwa/config/schema/launcher/mod.rs
Normal file
130
src/games/wuwa/config/schema/launcher/mod.rs
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
|
use enum_ordinalize::Ordinalize;
|
||||||
|
|
||||||
|
#[cfg(feature = "discord-rpc")]
|
||||||
|
pub mod discord_rpc;
|
||||||
|
|
||||||
|
use crate::config::schema_blanks::prelude::*;
|
||||||
|
use crate::wuwa::consts::launcher_dir;
|
||||||
|
|
||||||
|
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 style: LauncherStyle,
|
||||||
|
pub temp: Option<PathBuf>,
|
||||||
|
pub repairer: Repairer,
|
||||||
|
|
||||||
|
#[cfg(feature = "discord-rpc")]
|
||||||
|
pub discord_rpc: DiscordRpc,
|
||||||
|
|
||||||
|
pub behavior: LauncherBehavior
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
|
||||||
|
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
|
||||||
|
},
|
||||||
|
|
||||||
|
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
|
||||||
|
},
|
||||||
|
|
||||||
|
behavior: match value.get("behavior") {
|
||||||
|
Some(value) => serde_json::from_value(value.clone()).unwrap_or(default.behavior),
|
||||||
|
None => default.behavior
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
128
src/games/wuwa/config/schema/mod.rs
Normal file
128
src/games/wuwa/config/schema/mod.rs
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
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;
|
||||||
|
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::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 {
|
||||||
|
UnifiedWine::Default(wine) => wine.prefix,
|
||||||
|
UnifiedWine::Proton(proton) => proton.wine().prefix.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
return prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.game.wine.prefix.clone()
|
||||||
|
}
|
||||||
|
}
|
35
src/games/wuwa/config/schema/patch.rs
Normal file
35
src/games/wuwa/config/schema/patch.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use serde_json::Value as JsonValue;
|
||||||
|
|
||||||
|
use crate::wuwa::consts::launcher_dir;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Patch {
|
||||||
|
pub path: PathBuf
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&JsonValue> for Patch {
|
||||||
|
fn from(value: &JsonValue) -> Self {
|
||||||
|
let default = Self::default();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
path: match value.get("path").and_then(|path| path.as_str()).map(PathBuf::from) {
|
||||||
|
Some(path) => path,
|
||||||
|
None => default.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
src/games/wuwa/consts.rs
Normal file
36
src/games/wuwa/consts.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub const FOLDER_NAME: &str = "wavey-launcher";
|
||||||
|
|
||||||
|
/// Get default launcher dir path
|
||||||
|
///
|
||||||
|
/// If `LAUNCHER_FOLDER` variable is set, then its value will be returned. Otherwise return `$HOME/.local/share/wavey-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/wavey-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/wavey-launcher/config.json`
|
||||||
|
pub fn config_file() -> anyhow::Result<PathBuf> {
|
||||||
|
launcher_dir().map(|dir| dir.join("config.json"))
|
||||||
|
}
|
277
src/games/wuwa/game.rs
Normal file
277
src/games/wuwa/game.rs
Normal file
|
@ -0,0 +1,277 @@
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anime_game_core::wuwa::telemetry;
|
||||||
|
|
||||||
|
use crate::components::wine::Bundle as WineBundle;
|
||||||
|
|
||||||
|
use crate::config::ConfigExt;
|
||||||
|
use crate::wuwa::config::Config;
|
||||||
|
|
||||||
|
use crate::config::schema_blanks::prelude::{
|
||||||
|
WineDrives,
|
||||||
|
AllowedDrives
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::wuwa::consts;
|
||||||
|
|
||||||
|
#[cfg(feature = "discord-rpc")]
|
||||||
|
use crate::discord_rpc::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "sessions")]
|
||||||
|
use crate::{
|
||||||
|
sessions::SessionsExt,
|
||||||
|
wuwa::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()?;
|
||||||
|
|
||||||
|
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 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("pgr") {
|
||||||
|
windows_command += &virtual_desktop;
|
||||||
|
windows_command += " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
windows_command += "PGR.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());
|
||||||
|
|
||||||
|
windows_command = windows_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())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.clone().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("PGR.exe") {
|
||||||
|
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(())
|
||||||
|
}
|
13
src/games/wuwa/mod.rs
Normal file
13
src/games/wuwa/mod.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
pub mod consts;
|
||||||
|
|
||||||
|
#[cfg(feature = "config")]
|
||||||
|
pub mod config;
|
||||||
|
|
||||||
|
#[cfg(feature = "states")]
|
||||||
|
pub mod states;
|
||||||
|
|
||||||
|
#[cfg(feature = "game")]
|
||||||
|
pub mod game;
|
||||||
|
|
||||||
|
#[cfg(feature = "sessions")]
|
||||||
|
pub mod sessions;
|
95
src/games/wuwa/sessions.rs
Normal file
95
src/games/wuwa/sessions.rs
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
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/wavey-launcher/sessions.json`
|
||||||
|
#[inline]
|
||||||
|
pub fn sessions_file() -> anyhow::Result<PathBuf> {
|
||||||
|
launcher_dir().map(|dir| dir.join("sessions.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: update registry entries names
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct SessionData {
|
||||||
|
// [Software\\kurogame\\PGR]
|
||||||
|
pub game_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()
|
||||||
|
};
|
||||||
|
|
||||||
|
for entry in std::fs::read_to_string(prefix.as_ref().join("user.reg"))?.split("\n\n") {
|
||||||
|
if entry.starts_with("[Software\\\\kurogame\\\\PGR]") {
|
||||||
|
new_session.game_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\\\\kurogame\\\\PGR]") {
|
||||||
|
session.game_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()))?)
|
||||||
|
}
|
||||||
|
}
|
141
src/games/wuwa/states.rs
Normal file
141
src/games/wuwa/states.rs
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use wincompatlib::wine::ext::Font;
|
||||||
|
|
||||||
|
use anime_game_core::prelude::*;
|
||||||
|
use anime_game_core::wuwa::prelude::*;
|
||||||
|
|
||||||
|
use crate::config::ConfigExt;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum LauncherState {
|
||||||
|
Launch,
|
||||||
|
|
||||||
|
#[cfg(feature = "components")]
|
||||||
|
WineNotInstalled,
|
||||||
|
|
||||||
|
PrefixNotExists,
|
||||||
|
|
||||||
|
FontsNotInstalled(Vec<Font>),
|
||||||
|
|
||||||
|
TelemetryNotDisabled,
|
||||||
|
|
||||||
|
// Always contains `VersionDiff::Diff`
|
||||||
|
GameUpdateAvailable(VersionDiff),
|
||||||
|
|
||||||
|
/// Always contains `VersionDiff::NotInstalled`
|
||||||
|
GameNotInstalled(VersionDiff)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum StateUpdating {
|
||||||
|
Components,
|
||||||
|
Game
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct LauncherStateParams<F: Fn(StateUpdating)> {
|
||||||
|
pub wine_prefix: PathBuf,
|
||||||
|
pub game_path: PathBuf,
|
||||||
|
pub fast_verify: bool,
|
||||||
|
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 wine components installation status
|
||||||
|
(params.status_updater)(StateUpdating::Components);
|
||||||
|
|
||||||
|
let mut fonts = Vec::new();
|
||||||
|
|
||||||
|
// In future, wincompatlib's Font might contain fonts that won't be actually needed
|
||||||
|
// That's why I listed only needed fonts here
|
||||||
|
const COREFONTS: &[Font] = &[
|
||||||
|
Font::Andale,
|
||||||
|
Font::Arial,
|
||||||
|
Font::Courier,
|
||||||
|
Font::Georgia,
|
||||||
|
Font::Impact,
|
||||||
|
Font::Times,
|
||||||
|
Font::Trebuchet,
|
||||||
|
Font::Verdana,
|
||||||
|
Font::Webdings,
|
||||||
|
|
||||||
|
// Who even needs it?
|
||||||
|
Font::ComicSans
|
||||||
|
];
|
||||||
|
|
||||||
|
for font in COREFONTS.iter().copied() {
|
||||||
|
if !font.is_installed(¶ms.wine_prefix) {
|
||||||
|
fonts.push(font);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fonts.is_empty() {
|
||||||
|
return Ok(Self::FontsNotInstalled(fonts));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check telemetry servers
|
||||||
|
let disabled = telemetry::is_disabled()
|
||||||
|
|
||||||
|
// 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 game installation status
|
||||||
|
(params.status_updater)(StateUpdating::Game);
|
||||||
|
|
||||||
|
let game = Game::new(¶ms.game_path, ())
|
||||||
|
.with_fast_verify(params.fast_verify);
|
||||||
|
|
||||||
|
let diff = game.try_get_diff()?;
|
||||||
|
|
||||||
|
match diff {
|
||||||
|
VersionDiff::Latest(_) => Ok(Self::Launch),
|
||||||
|
VersionDiff::Outdated { .. } => Ok(Self::GameUpdateAvailable(diff)),
|
||||||
|
VersionDiff::NotInstalled { .. } => Ok(Self::GameNotInstalled(diff))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "config")]
|
||||||
|
pub fn get_from_config<T: Fn(StateUpdating)>(status_updater: T) -> anyhow::Result<Self> {
|
||||||
|
tracing::debug!("Trying to get launcher state");
|
||||||
|
|
||||||
|
let config = crate::wuwa::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,
|
||||||
|
fast_verify: config.launcher.repairer.fast,
|
||||||
|
|
||||||
|
status_updater
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,9 @@ pub use games::honkai;
|
||||||
#[cfg(feature = "pgr")]
|
#[cfg(feature = "pgr")]
|
||||||
pub use games::pgr;
|
pub use games::pgr;
|
||||||
|
|
||||||
|
#[cfg(feature = "wuwa")]
|
||||||
|
pub use games::wuwa;
|
||||||
|
|
||||||
#[cfg(feature = "config")]
|
#[cfg(feature = "config")]
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue