From e7d356da5c608ea2a2a7d00e29dd7e31bc7861fb Mon Sep 17 00:00:00 2001 From: Observer KRypt0n_ Date: Fri, 11 Nov 2022 18:29:16 +0200 Subject: [PATCH] Init commit --- .gitignore | 9 + .gitmodules | 3 + Cargo.toml | 26 +++ README.md | 20 +++ anime-game-core | 1 + src/config/game/dxvk.rs | 37 +++++ .../enhancements/fps_unlocker/config/fps.rs | 65 ++++++++ .../enhancements/fps_unlocker/config/mod.rs | 55 ++++++ .../game/enhancements/fps_unlocker/mod.rs | 61 +++++++ src/config/game/enhancements/fsr.rs | 53 ++++++ .../game/enhancements/gamescope/framerate.rs | 26 +++ src/config/game/enhancements/gamescope/mod.rs | 156 ++++++++++++++++++ .../game/enhancements/gamescope/size.rs | 26 +++ .../enhancements/gamescope/window_type.rs | 20 +++ src/config/game/enhancements/hud.rs | 72 ++++++++ src/config/game/enhancements/mod.rs | 61 +++++++ src/config/game/mod.rs | 131 +++++++++++++++ src/config/game/wine/mod.rs | 104 ++++++++++++ src/config/game/wine/virtual_desktop.rs | 60 +++++++ src/config/game/wine/wine_lang.rs | 86 ++++++++++ src/config/game/wine/wine_sync.rs | 64 +++++++ src/config/launcher/mod.rs | 125 ++++++++++++++ src/config/launcher/repairer.rs | 35 ++++ src/config/mod.rs | 150 +++++++++++++++++ src/config/patch.rs | 69 ++++++++ src/config/resolution.rs | 63 +++++++ src/lib.rs | 18 ++ 27 files changed, 1596 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 Cargo.toml create mode 100644 README.md create mode 160000 anime-game-core create mode 100644 src/config/game/dxvk.rs create mode 100644 src/config/game/enhancements/fps_unlocker/config/fps.rs create mode 100644 src/config/game/enhancements/fps_unlocker/config/mod.rs create mode 100644 src/config/game/enhancements/fps_unlocker/mod.rs create mode 100644 src/config/game/enhancements/fsr.rs create mode 100644 src/config/game/enhancements/gamescope/framerate.rs create mode 100644 src/config/game/enhancements/gamescope/mod.rs create mode 100644 src/config/game/enhancements/gamescope/size.rs create mode 100644 src/config/game/enhancements/gamescope/window_type.rs create mode 100644 src/config/game/enhancements/hud.rs create mode 100644 src/config/game/enhancements/mod.rs create mode 100644 src/config/game/mod.rs create mode 100644 src/config/game/wine/mod.rs create mode 100644 src/config/game/wine/virtual_desktop.rs create mode 100644 src/config/game/wine/wine_lang.rs create mode 100644 src/config/game/wine/wine_sync.rs create mode 100644 src/config/launcher/mod.rs create mode 100644 src/config/launcher/repairer.rs create mode 100644 src/config/mod.rs create mode 100644 src/config/patch.rs create mode 100644 src/config/resolution.rs create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ce42ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/target + + +# Added by cargo +# +# already existing elements were commented out + +#/target +/Cargo.lock diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3a3be25 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "anime-game-core"] + path = anime-game-core + url = https://github.com/an-anime-team/anime-game-core diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9277b7f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "anime-launcher-sdk" +version = "0.1.0" +authors = ["Nikita Podvirnyy "] +license = "GPL-3.0" +readme = "README.md" +edition = "2021" + +[dependencies] +anime-game-core = { path = "anime-game-core", features = ["genshin", "all", "static"] } + +anyhow = "1.0" +dirs = "4.0.0" + +serde = { version = "1.0", features = ["derive"], optional = true } +serde_json = { version = "1.0", optional = true } + +[features] +states = [] +config = ["dep:serde", "dep:serde_json"] +components = [] +runner = [] +fps-unlocker = [] + +default = ["all"] +all = ["states", "config", "components", "runner", "fps-unlocker"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..6431d5e --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# Anime Launcher SDK + +## Project goals + +* Unify backends for [gtk](https://github.com/an-anime-team/an-anime-game-launcher-gtk) and [tauri](https://github.com/an-anime-team/an-anime-game-launcher-tauri) launchers so they will have same functionality; +* Remove excess code from gtk launcher and prepare it for relm4 rewrite; +* Prepare codebase for tauri rewrite; + +## Current progress (12.5%) + +| Status | Feature | Description | +| :-: | - | - | +| ❌ | states | Getting current launcher's state (update available, etc.) | +| ✅ | config | Work with config file | +| ❌ | components | Work with components needed to run the game | +| ❌ | | List Wine and DXVK versions | +| ❌ | | Download, delete and select wine | +| ❌ | | Download, delete, select and apply DXVK | +| ❌ | runner | Run the game | +| ❌ | fps-unlocker | Support of FPS unlocker. Manage its config, download, use in game runner | diff --git a/anime-game-core b/anime-game-core new file mode 160000 index 0000000..73d3644 --- /dev/null +++ b/anime-game-core @@ -0,0 +1 @@ +Subproject commit 73d3644761bef06cfc16e4e4bc4f9b9af3c50139 diff --git a/src/config/game/dxvk.rs b/src/config/game/dxvk.rs new file mode 100644 index 0000000..275cc70 --- /dev/null +++ b/src/config/game/dxvk.rs @@ -0,0 +1,37 @@ +use std::path::PathBuf; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use crate::launcher_dir; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Dxvk { + pub builds: PathBuf +} + +impl Default for Dxvk { + fn default() -> Self { + let launcher_dir = launcher_dir().expect("Failed to get launcher dir"); + + Self { + builds: launcher_dir.join("dxvks") + } + } +} + +impl From<&JsonValue> for Dxvk { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + builds: match value.get("builds") { + Some(value) => match value.as_str() { + Some(value) => PathBuf::from(value), + None => default.builds + }, + None => default.builds + } + } + } +} diff --git a/src/config/game/enhancements/fps_unlocker/config/fps.rs b/src/config/game/enhancements/fps_unlocker/config/fps.rs new file mode 100644 index 0000000..b9e605e --- /dev/null +++ b/src/config/game/enhancements/fps_unlocker/config/fps.rs @@ -0,0 +1,65 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Fps { + /// 90 + Ninety, + + /// 120 + HundredTwenty, + + /// 144 + HundredFourtyFour, + + /// 165 + HundredSixtyFive, + + /// 180 + HundredEighty, + + /// 200 + TwoHundred, + + /// 240 + TwoHundredFourty, + + Custom(u64) +} + +impl Fps { + pub fn list() -> Vec { + vec![ + Self::Ninety, + Self::HundredTwenty, + Self::HundredFourtyFour, + Self::HundredSixtyFive, + Self::HundredEighty, + Self::TwoHundred, + Self::TwoHundredFourty + ] + } + + pub fn from_num(fps: u64) -> Self { + match fps { + 90 => Self::Ninety, + 120 => Self::HundredTwenty, + 144 => Self::HundredFourtyFour, + 165 => Self::HundredSixtyFive, + 180 => Self::HundredEighty, + 200 => Self::TwoHundred, + 240 => Self::TwoHundredFourty, + num => Self::Custom(num) + } + } + + pub fn to_num(&self) -> u64 { + match self { + Self::Ninety => 90, + Self::HundredTwenty => 120, + Self::HundredFourtyFour => 144, + Self::HundredSixtyFive => 165, + Self::HundredEighty => 180, + Self::TwoHundred => 200, + Self::TwoHundredFourty => 240, + Self::Custom(num) => *num + } + } +} diff --git a/src/config/game/enhancements/fps_unlocker/config/mod.rs b/src/config/game/enhancements/fps_unlocker/config/mod.rs new file mode 100644 index 0000000..6292740 --- /dev/null +++ b/src/config/game/enhancements/fps_unlocker/config/mod.rs @@ -0,0 +1,55 @@ +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +pub mod fps; + +pub mod prelude { + pub use super::fps::Fps; +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub fps: u64, + pub power_saving: bool, + pub fullscreen: bool, + pub priority: u64 +} + +impl Default for Config { + fn default() -> Self { + Self { + fps: 120, + power_saving: false, + fullscreen: false, + priority: 3 + } + } +} + +impl From<&JsonValue> for Config { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + fps: match value.get("fps") { + Some(value) => value.as_u64().unwrap_or(default.fps), + None => default.fps + }, + + power_saving: match value.get("power_saving") { + Some(value) => value.as_bool().unwrap_or(default.power_saving), + None => default.power_saving + }, + + fullscreen: match value.get("fullscreen") { + Some(value) => value.as_bool().unwrap_or(default.fullscreen), + None => default.fullscreen + }, + + priority: match value.get("priority") { + Some(value) => value.as_u64().unwrap_or(default.priority), + None => default.priority + } + } + } +} diff --git a/src/config/game/enhancements/fps_unlocker/mod.rs b/src/config/game/enhancements/fps_unlocker/mod.rs new file mode 100644 index 0000000..e358589 --- /dev/null +++ b/src/config/game/enhancements/fps_unlocker/mod.rs @@ -0,0 +1,61 @@ +use std::path::PathBuf; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use crate::launcher_dir; + +pub mod config; + +pub mod prelude { + pub use super::config::Config; + + pub use super::config::prelude::*; +} + +use prelude::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FpsUnlocker { + pub path: PathBuf, + pub enabled: bool, + pub config: Config +} + +impl Default for FpsUnlocker { + fn default() -> Self { + let launcher_dir = launcher_dir().expect("Failed to get launcher dir"); + + Self { + path: launcher_dir.join("fps-unlocker"), + enabled: false, + config: Config::default() + } + } +} + +impl From<&JsonValue> for FpsUnlocker { + 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 + }, + + enabled: match value.get("enabled") { + Some(value) => value.as_bool().unwrap_or(default.enabled), + None => default.enabled + }, + + config: match value.get("config") { + Some(value) => Config::from(value), + None => default.config + } + } + } +} diff --git a/src/config/game/enhancements/fsr.rs b/src/config/game/enhancements/fsr.rs new file mode 100644 index 0000000..5b6794f --- /dev/null +++ b/src/config/game/enhancements/fsr.rs @@ -0,0 +1,53 @@ +use std::collections::HashMap; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Fsr { + pub strength: u64, + pub enabled: bool +} + +impl Default for Fsr { + fn default() -> Self { + Self { + strength: 2, + enabled: false + } + } +} + +impl From<&JsonValue> for Fsr { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + strength: match value.get("strength") { + Some(value) => value.as_u64().unwrap_or(default.strength), + None => default.strength + }, + + enabled: match value.get("enabled") { + Some(value) => value.as_bool().unwrap_or(default.enabled), + None => default.enabled + } + } + } +} + +impl Fsr { + /// Get environment variables corresponding to used amd fsr options + pub fn get_env_vars(&self) -> HashMap<&str, String> { + if self.enabled { + HashMap::from([ + ("WINE_FULLSCREEN_FSR", String::from("1")), + ("WINE_FULLSCREEN_FSR_STRENGTH", self.strength.to_string()) + ]) + } + + else { + HashMap::new() + } + } +} diff --git a/src/config/game/enhancements/gamescope/framerate.rs b/src/config/game/enhancements/gamescope/framerate.rs new file mode 100644 index 0000000..35b7061 --- /dev/null +++ b/src/config/game/enhancements/gamescope/framerate.rs @@ -0,0 +1,26 @@ +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] +pub struct Framerate { + pub focused: u64, + pub unfocused: u64 +} + +impl From<&JsonValue> for Framerate { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + focused: match value.get("focused") { + Some(value) => value.as_u64().unwrap_or(default.focused), + None => default.focused + }, + + unfocused: match value.get("unfocused") { + Some(value) => value.as_u64().unwrap_or(default.unfocused), + None => default.unfocused + } + } + } +} diff --git a/src/config/game/enhancements/gamescope/mod.rs b/src/config/game/enhancements/gamescope/mod.rs new file mode 100644 index 0000000..0e5783d --- /dev/null +++ b/src/config/game/enhancements/gamescope/mod.rs @@ -0,0 +1,156 @@ +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +pub mod size; +pub mod framerate; +pub mod window_type; + +pub mod prelude { + pub use super::Gamescope; + pub use super::size::Size; + pub use super::framerate::Framerate; + pub use super::window_type::WindowType; +} + +use prelude::*; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Gamescope { + pub enabled: bool, + pub game: Size, + pub gamescope: Size, + pub framerate: Framerate, + pub integer_scaling: bool, + pub fsr: bool, + pub nis: bool, + pub window_type: WindowType +} + +impl Gamescope { + pub fn get_command(&self) -> Option { + // https://github.com/bottlesdevs/Bottles/blob/b908311348ed1184ead23dd76f9d8af41ff24082/src/backend/wine/winecommand.py#L478 + if self.enabled { + let mut gamescope = String::from("gamescope"); + + // Set window type + match self.window_type { + WindowType::Borderless => gamescope += " -b", + WindowType::Fullscreen => gamescope += " -f" + } + + // Set game width + if self.game.width > 0 { + gamescope += &format!(" -w {}", self.game.width); + } + + // Set game height + if self.game.height > 0 { + gamescope += &format!(" -h {}", self.game.height); + } + + // Set gamescope width + if self.gamescope.width > 0 { + gamescope += &format!(" -W {}", self.gamescope.width); + } + + // Set gamescope height + if self.gamescope.height > 0 { + gamescope += &format!(" -H {}", self.gamescope.height); + } + + // Set focused framerate limit + if self.framerate.focused > 0 { + gamescope += &format!(" -r {}", self.framerate.focused); + } + + // Set unfocused framerate limit + if self.framerate.unfocused > 0 { + gamescope += &format!(" -o {}", self.framerate.unfocused); + } + + // Set integer scaling + if self.integer_scaling { + gamescope += " -n"; + } + + // Set FSR support + if self.fsr { + gamescope += " -U"; + } + + // Set NIS (Nvidia Image Scaling) support + if self.nis { + gamescope += " -Y"; + } + + Some(gamescope) + } + + else { + None + } + } +} + +impl Default for Gamescope { + fn default() -> Self { + Self { + enabled: false, + game: Size::default(), + gamescope: Size::default(), + framerate: Framerate::default(), + integer_scaling: true, + fsr: false, + nis: false, + window_type: WindowType::default() + } + } +} + +impl From<&JsonValue> for Gamescope { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + enabled: match value.get("enabled") { + Some(value) => value.as_bool().unwrap_or(default.enabled), + None => default.enabled + }, + + game: match value.get("game") { + Some(value) => Size::from(value), + None => default.game + }, + + gamescope: match value.get("gamescope") { + Some(value) => Size::from(value), + None => default.gamescope + }, + + framerate: match value.get("framerate") { + Some(value) => Framerate::from(value), + None => default.framerate + }, + + integer_scaling: match value.get("integer_scaling") { + Some(value) => value.as_bool().unwrap_or(default.integer_scaling), + None => default.integer_scaling + }, + + fsr: match value.get("fsr") { + Some(value) => value.as_bool().unwrap_or(default.fsr), + None => default.fsr + }, + + nis: match value.get("nis") { + Some(value) => value.as_bool().unwrap_or(default.nis), + None => default.nis + }, + + window_type: match value.get("window_type") { + Some(value) => WindowType::from(value), + None => default.window_type + } + } + } +} diff --git a/src/config/game/enhancements/gamescope/size.rs b/src/config/game/enhancements/gamescope/size.rs new file mode 100644 index 0000000..fc63de0 --- /dev/null +++ b/src/config/game/enhancements/gamescope/size.rs @@ -0,0 +1,26 @@ +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] +pub struct Size { + pub width: u64, + pub height: u64 +} + +impl From<&JsonValue> for Size { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + width: match value.get("width") { + Some(value) => value.as_u64().unwrap_or(default.width), + None => default.width + }, + + height: match value.get("height") { + Some(value) => value.as_u64().unwrap_or(default.height), + None => default.height + } + } + } +} diff --git a/src/config/game/enhancements/gamescope/window_type.rs b/src/config/game/enhancements/gamescope/window_type.rs new file mode 100644 index 0000000..3889808 --- /dev/null +++ b/src/config/game/enhancements/gamescope/window_type.rs @@ -0,0 +1,20 @@ +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum WindowType { + Borderless, + Fullscreen +} + +impl Default for WindowType { + fn default() -> Self { + Self::Borderless + } +} + +impl From<&JsonValue> for WindowType { + fn from(value: &JsonValue) -> Self { + serde_json::from_value(value.clone()).unwrap_or_default() + } +} diff --git a/src/config/game/enhancements/hud.rs b/src/config/game/enhancements/hud.rs new file mode 100644 index 0000000..20b0d39 --- /dev/null +++ b/src/config/game/enhancements/hud.rs @@ -0,0 +1,72 @@ +use std::collections::HashMap; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use crate::config::Config; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum HUD { + None, + DXVK, + MangoHUD +} + +impl Default for HUD { + fn default() -> Self { + Self::None + } +} + +impl From<&JsonValue> for HUD { + fn from(value: &JsonValue) -> Self { + serde_json::from_value(value.clone()).unwrap_or_default() + } +} + +impl TryFrom for HUD { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(Self::None), + 1 => Ok(Self::DXVK), + 2 => Ok(Self::MangoHUD), + _ => Err(String::from("Failed to convert number to HUD enum")) + } + } +} + +#[allow(clippy::from_over_into)] +impl Into for HUD { + fn into(self) -> u32 { + match self { + Self::None => 0, + Self::DXVK => 1, + Self::MangoHUD => 2 + } + } +} + +impl HUD { + /// Get environment variables corresponding to used wine hud + pub fn get_env_vars(&self, config: &Config) -> HashMap<&str, &str> { + match self { + Self::None => HashMap::new(), + Self::DXVK => HashMap::from([ + ("DXVK_HUD", "fps,frametimes,version,gpuload") + ]), + Self::MangoHUD => { + // Don't show mangohud if gamescope is enabled + // otherwise it'll be doubled + if config.game.enhancements.gamescope.enabled { + HashMap::new() + } else { + HashMap::from([ + ("MANGOHUD", "1") + ]) + } + } + } + } +} diff --git a/src/config/game/enhancements/mod.rs b/src/config/game/enhancements/mod.rs new file mode 100644 index 0000000..1f85c97 --- /dev/null +++ b/src/config/game/enhancements/mod.rs @@ -0,0 +1,61 @@ +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +pub mod fsr; +pub mod hud; +pub mod fps_unlocker; +pub mod gamescope; + +pub mod prelude { + pub use super::gamescope::prelude::*; + pub use super::fps_unlocker::prelude::*; + + pub use super::Enhancements; + pub use super::fsr::Fsr; + pub use super::hud::HUD; + pub use super::fps_unlocker::FpsUnlocker; +} + +use prelude::*; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Enhancements { + pub fsr: Fsr, + pub gamemode: bool, + pub hud: HUD, + pub fps_unlocker: FpsUnlocker, + 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 + }, + + fps_unlocker: match value.get("fps_unlocker") { + Some(value) => FpsUnlocker::from(value), + None => default.fps_unlocker + }, + + gamescope: match value.get("gamescope") { + Some(value) => Gamescope::from(value), + None => default.gamescope + } + } + } +} diff --git a/src/config/game/mod.rs b/src/config/game/mod.rs new file mode 100644 index 0000000..cd2fec6 --- /dev/null +++ b/src/config/game/mod.rs @@ -0,0 +1,131 @@ +use std::collections::HashMap; +use std::path::PathBuf; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use crate::launcher_dir; + +pub mod wine; +pub mod dxvk; +pub mod enhancements; + +pub mod prelude { + pub use super::enhancements::prelude::*; + pub use super::wine::prelude::*; + + pub use super::Game; + pub use super::dxvk::Dxvk; +} + +use prelude::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Game { + pub path: PathBuf, + pub voices: Vec, + pub wine: prelude::Wine, + pub dxvk: prelude::Dxvk, + pub enhancements: prelude::Enhancements, + pub environment: HashMap, + pub command: Option +} + +impl Default for Game { + fn default() -> Self { + let launcher_dir = launcher_dir().expect("Failed to get launcher dir"); + + Self { + path: launcher_dir.join("game/drive_c/Program Files/Genshin Impact"), + voices: vec![ + String::from("en-us") + ], + 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 + }, + + voices: match value.get("voices") { + Some(value) => match value.as_array() { + Some(values) => { + let mut voices = Vec::new(); + + for value in values { + if let Some(voice) = value.as_str() { + voices.push(voice.to_string()); + } + } + + voices + }, + None => default.voices + }, + None => default.voices + }, + + 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 + } + } + } +} diff --git a/src/config/game/wine/mod.rs b/src/config/game/wine/mod.rs new file mode 100644 index 0000000..dbfa10b --- /dev/null +++ b/src/config/game/wine/mod.rs @@ -0,0 +1,104 @@ +use std::path::PathBuf; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use crate::launcher_dir; + +pub mod wine_sync; +pub mod wine_lang; +pub mod virtual_desktop; + +pub mod prelude { + pub use super::Wine; + pub use super::wine_sync::WineSync; + pub use super::wine_lang::WineLang; + pub use super::virtual_desktop::VirtualDesktop; +} + +use prelude::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Wine { + pub prefix: PathBuf, + pub builds: PathBuf, + pub selected: Option, + pub sync: WineSync, + pub language: WineLang, + pub borderless: bool, + pub virtual_desktop: VirtualDesktop +} + +impl Default for Wine { + fn default() -> Self { + let launcher_dir = launcher_dir().expect("Failed to get launcher dir"); + + Self { + prefix: launcher_dir.join("game"), + builds: launcher_dir.join("runners"), + selected: None, + sync: WineSync::default(), + language: WineLang::default(), + borderless: false, + virtual_desktop: VirtualDesktop::default() + } + } +} + +impl From<&JsonValue> for Wine { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + prefix: match value.get("prefix") { + Some(value) => match value.as_str() { + Some(value) => PathBuf::from(value), + None => default.prefix + }, + None => default.prefix + }, + + builds: match value.get("builds") { + Some(value) => match value.as_str() { + Some(value) => PathBuf::from(value), + None => default.builds + }, + None => default.builds + }, + + selected: match value.get("selected") { + Some(value) => { + if value.is_null() { + None + } else { + match value.as_str() { + Some(value) => Some(value.to_string()), + None => default.selected + } + } + }, + None => default.selected + }, + + sync: match value.get("sync") { + Some(value) => WineSync::from(value), + None => default.sync + }, + + language: match value.get("language") { + Some(value) => WineLang::from(value), + None => default.language + }, + + borderless: match value.get("borderless") { + Some(value) => value.as_bool().unwrap_or(default.borderless), + None => default.borderless + }, + + virtual_desktop: match value.get("virtual_desktop") { + Some(value) => VirtualDesktop::from(value), + None => default.virtual_desktop + } + } + } +} diff --git a/src/config/game/wine/virtual_desktop.rs b/src/config/game/wine/virtual_desktop.rs new file mode 100644 index 0000000..fd334f1 --- /dev/null +++ b/src/config/game/wine/virtual_desktop.rs @@ -0,0 +1,60 @@ +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use crate::config::prelude::*; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct VirtualDesktop { + pub enabled: bool, + pub width: u64, + pub height: u64 +} + +impl Default for VirtualDesktop { + fn default() -> Self { + Self { + enabled: false, + width: 1920, + height: 1080 + } + } +} + +impl From<&JsonValue> for VirtualDesktop { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + enabled: match value.get("enabled") { + Some(value) => value.as_bool().unwrap_or(default.enabled), + None => default.enabled + }, + + width: match value.get("width") { + Some(value) => value.as_u64().unwrap_or(default.width), + None => default.width + }, + + height: match value.get("height") { + Some(value) => value.as_u64().unwrap_or(default.height), + None => default.height + } + } + } +} + +impl VirtualDesktop { + pub fn get_resolution(&self) -> Resolution { + Resolution::from_pair(self.width, self.height) + } + + pub fn get_command(&self) -> Option { + if self.enabled { + Some(format!("explorer /desktop=animegame,{}x{}", self.width, self.height)) + } + + else { + None + } + } +} diff --git a/src/config/game/wine/wine_lang.rs b/src/config/game/wine/wine_lang.rs new file mode 100644 index 0000000..09bdd24 --- /dev/null +++ b/src/config/game/wine/wine_lang.rs @@ -0,0 +1,86 @@ +use std::collections::HashMap; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum WineLang { + System, + English, + Russian, + German, + Portuguese, + Polish, + French, + Spanish, + Chinese, + Japanese, + Korean +} + +impl Default for WineLang { + fn default() -> Self { + Self::System + } +} + +impl From<&JsonValue> for WineLang { + fn from(value: &JsonValue) -> Self { + serde_json::from_value(value.clone()).unwrap_or_default() + } +} + +#[allow(clippy::from_over_into)] +impl Into for WineLang { + fn into(self) -> u32 { + for (i, lang) in Self::list().into_iter().enumerate() { + if lang == self { + return i as u32; + } + } + + unreachable!() + } +} + +impl WineLang { + pub fn list() -> Vec { + vec![ + Self::System, + Self::English, + Self::Russian, + Self::German, + Self::Portuguese, + Self::Polish, + Self::French, + Self::Spanish, + Self::Chinese, + Self::Japanese, + Self::Korean + ] + } + + /// Get environment variables corresponding to used wine language + pub fn get_env_vars(&self) -> HashMap<&str, &str> { + HashMap::from([("LANG", match self { + Self::System => return HashMap::new(), + + Self::English => "en_US.UTF8", + Self::Russian => "ru_RU.UTF8", + Self::German => "de_DE.UTF8", + Self::Portuguese => "pt_PT.UTF8", + Self::Polish => "pl_PL.UTF8", + Self::French => "fr_FR.UTF8", + Self::Spanish => "es_ES.UTF8", + Self::Chinese => "zh_CN.UTF8", + Self::Japanese => "ja_JP.UTF8", + Self::Korean => "ko_KR.UTF8" + })]) + } +} + +impl std::fmt::Display for WineLang { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("{:?}", self)) + } +} diff --git a/src/config/game/wine/wine_sync.rs b/src/config/game/wine/wine_sync.rs new file mode 100644 index 0000000..183a834 --- /dev/null +++ b/src/config/game/wine/wine_sync.rs @@ -0,0 +1,64 @@ +use std::collections::HashMap; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum WineSync { + None, + ESync, + FSync, + Futex2 +} + +impl Default for WineSync { + fn default() -> Self { + Self::FSync + } +} + +impl From<&JsonValue> for WineSync { + fn from(value: &JsonValue) -> Self { + serde_json::from_value(value.clone()).unwrap_or_default() + } +} + +impl TryFrom for WineSync { + type Error = String; + + fn try_from(value: u32) -> Result { + match value { + 0 => Ok(Self::None), + 1 => Ok(Self::ESync), + 2 => Ok(Self::FSync), + 3 => Ok(Self::Futex2), + + _ => Err(String::from("Failed to convert number to WineSync enum")) + } + } +} + +#[allow(clippy::from_over_into)] +impl Into for WineSync { + fn into(self) -> u32 { + match self { + Self::None => 0, + Self::ESync => 1, + Self::FSync => 2, + Self::Futex2 => 3 + } + } +} + +impl WineSync { + /// Get environment variables corresponding to used wine sync + pub fn get_env_vars(&self) -> HashMap<&str, &str> { + HashMap::from([(match self { + Self::None => return HashMap::new(), + + Self::ESync => "WINEESYNC", + Self::FSync => "WINEFSYNC", + Self::Futex2 => "WINEFSYNC_FUTEX2" + }, "1")]) + } +} diff --git a/src/config/launcher/mod.rs b/src/config/launcher/mod.rs new file mode 100644 index 0000000..ab0d8df --- /dev/null +++ b/src/config/launcher/mod.rs @@ -0,0 +1,125 @@ +use std::path::PathBuf; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use anime_game_core::genshin::consts::GameEdition as CoreGameEdition; + +use crate::launcher_dir; + +pub mod repairer; + +pub mod prelude { + pub use super::Launcher; + pub use super::repairer::Repairer; +} + +use prelude::*; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum GameEdition { + Global, + China +} + +impl Default for GameEdition { + fn default() -> Self { + let locale = match std::env::var("LC_ALL") { + Ok(locale) => locale, + Err(_) => match std::env::var("LC_MESSAGES") { + Ok(locale) => locale, + Err(_) => match std::env::var("LANG") { + Ok(locale) => locale, + Err(_) => return Self::Global + } + } + }; + + if locale.len() > 4 && &locale[..5].to_lowercase() == "zh_cn" { + Self::China + } else { + Self::Global + } + } +} + +impl From for CoreGameEdition { + fn from(edition: GameEdition) -> Self { + match edition { + GameEdition::Global => CoreGameEdition::Global, + GameEdition::China => CoreGameEdition::China + } + } +} + +impl From for GameEdition { + fn from(edition: CoreGameEdition) -> Self { + match edition { + CoreGameEdition::Global => Self::Global, + CoreGameEdition::China => Self::China + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Launcher { + pub language: String, + pub temp: Option, + pub speed_limit: u64, + pub repairer: Repairer, + pub edition: GameEdition +} + +impl Default for Launcher { + fn default() -> Self { + Self { + language: String::from("en-us"), + temp: launcher_dir(), + speed_limit: 0, + repairer: Repairer::default(), + edition: GameEdition::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 + }, + + 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 + }, + + speed_limit: match value.get("speed_limit") { + Some(value) => value.as_u64().unwrap_or(default.speed_limit), + None => default.speed_limit + }, + + repairer: match value.get("repairer") { + Some(value) => Repairer::from(value), + None => default.repairer + }, + + edition: match value.get("edition") { + Some(value) => serde_json::from_value(value.clone()).unwrap_or(default.edition), + None => default.edition + } + } + } +} diff --git a/src/config/launcher/repairer.rs b/src/config/launcher/repairer.rs new file mode 100644 index 0000000..1c6d2fb --- /dev/null +++ b/src/config/launcher/repairer.rs @@ -0,0 +1,35 @@ +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Repairer { + pub threads: u64, + pub fast: bool +} + +impl Default for Repairer { + fn default() -> Self { + Self { + threads: 4, + fast: false + } + } +} + +impl From<&JsonValue> for Repairer { + fn from(value: &JsonValue) -> Self { + let default = Self::default(); + + Self { + threads: match value.get("threads") { + Some(value) => value.as_u64().unwrap_or(default.threads), + None => default.threads + }, + + fast: match value.get("fast") { + Some(value) => value.as_bool().unwrap_or(default.fast), + None => default.fast + } + } + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..2fc2ad8 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,150 @@ +use std::fs::File; +use std::io::Read; +use std::path::Path; +use std::io::Write; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use crate::config_file; + +pub mod launcher; +pub mod game; +pub mod patch; +pub mod resolution; + +pub mod prelude { + pub use super::launcher::prelude::*; + pub use super::game::prelude::*; + + pub use super::patch::Patch; + pub use super::resolution::Resolution; +} + +use prelude::*; + +static mut CONFIG: Option = None; + +/// Get config data +/// +/// This method will load config from file once and store it into the memory. +/// If you know that the config file was updated - you should run `get_raw` method +/// that always loads config directly from the file. This will also update in-memory config +pub fn get() -> anyhow::Result { + unsafe { + match &CONFIG { + Some(config) => Ok(config.clone()), + None => get_raw() + } + } +} + +/// Get config data +/// +/// This method will always load data directly from the file and update in-memory config +pub fn get_raw() -> anyhow::Result { + match config_file() { + Some(path) => { + // Try to read config if the file exists + if Path::new(&path).exists() { + let mut file = File::open(path)?; + let mut json = String::new(); + + file.read_to_string(&mut json)?; + + match serde_json::from_str(&json) { + Ok(config) => { + let config = Config::from(&config); + + unsafe { + CONFIG = Some(config.clone()); + } + + Ok(config) + }, + Err(err) => Err(anyhow::anyhow!("Failed to decode data from json format: {}", err.to_string())) + } + } + + // Otherwise create default config file + else { + update_raw(Config::default())?; + + Ok(Config::default()) + } + }, + None => Err(anyhow::anyhow!("Failed to get config file path")) + } +} + +/// Update in-memory config data +/// +/// Use `update_raw` if you want to update config file itself +pub fn update(config: Config) { + unsafe { + CONFIG = Some(config); + } +} + +/// Update config file +/// +/// This method will also update in-memory config data +pub fn update_raw(config: Config) -> anyhow::Result<()> { + update(config.clone()); + + match config_file() { + Some(path) => { + let mut file = File::create(&path)?; + + match serde_json::to_string_pretty(&config) { + Ok(json) => { + file.write_all(json.as_bytes())?; + + Ok(()) + }, + Err(err) => Err(anyhow::anyhow!("Failed to encode data into json format: {}", err.to_string())) + } + }, + None => Err(anyhow::anyhow!("Failed to get config file path")) + } +} + +/// Update config file from the in-memory saved config +pub fn flush() -> anyhow::Result<()> { + unsafe { + match &CONFIG { + Some(config) => update_raw(config.clone()), + None => Err(anyhow::anyhow!("Config wasn't loaded into the memory")) + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Config { + pub launcher: Launcher, + pub game: Game, + pub patch: Patch +} + +impl From<&JsonValue> for Config { + 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 + }, + + patch: match value.get("patch") { + Some(value) => Patch::from(value), + None => default.patch + } + } + } +} diff --git a/src/config/patch.rs b/src/config/patch.rs new file mode 100644 index 0000000..2e950a5 --- /dev/null +++ b/src/config/patch.rs @@ -0,0 +1,69 @@ +use std::path::{Path, PathBuf}; + +use serde::{Serialize, Deserialize}; +use serde_json::Value as JsonValue; + +use crate::launcher_dir; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Patch { + pub path: PathBuf, + pub servers: Vec, + pub root: bool +} + +impl Default for Patch { + fn default() -> Self { + let launcher_dir = launcher_dir().expect("Failed to get launcher dir"); + + Self { + path: launcher_dir.join("patch"), + servers: vec![ + "https://notabug.org/Krock/dawn".to_string(), + "https://codespace.gay/Maroxy/dawnin".to_string() + ], + + // Disable root requirement for patching if we're running launcher in flatpak + root: !Path::new("/.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 + }, + + root: match value.get("root") { + Some(value) => value.as_bool().unwrap_or(default.root), + None => default.root + } + } + } +} diff --git a/src/config/resolution.rs b/src/config/resolution.rs new file mode 100644 index 0000000..8185f35 --- /dev/null +++ b/src/config/resolution.rs @@ -0,0 +1,63 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Resolution { + // qHD; 960x540 + MiniHD, + + // 1280x720 + HD, + + // 1920x1080 + FullHD, + + // 2560x1440 + QuadHD, + + // 3840x2160 + UltraHD, + + Custom(u64, u64) +} + +impl Resolution { + pub fn list() -> Vec { + vec![ + Self::MiniHD, + Self::HD, + Self::FullHD, + Self::QuadHD, + Self::UltraHD + ] + } + + pub fn from_pair(width: u64, height: u64) -> Self { + for res in Self::list() { + let pair = res.get_pair(); + + if pair.0 == width && pair.1 == height { + return res; + } + } + + Self::Custom(width, height) + } + + pub fn get_pair(&self) -> (u64, u64) { + match self { + Self::MiniHD => (960, 540), + Self::HD => (1280, 720), + Self::FullHD => (1920, 1080), + Self::QuadHD => (2560, 1440), + Self::UltraHD => (3840, 2160), + + Self::Custom(w, h) => (*w, *h) + } + } +} + +impl std::fmt::Display for Resolution { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let (w, h) = self.get_pair(); + + f.write_str(&format!("{w}x{h}")) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..253e6f0 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,18 @@ +use std::path::PathBuf; + +#[cfg(feature = "config")] +pub mod config; + +/// Get default launcher dir path +/// +/// `$HOME/.local/share/anime-game-launcher` +pub fn launcher_dir() -> Option { + dirs::data_dir().map(|dir| dir.join("anime-game-launcher")) +} + +/// Get default config file path +/// +/// `$HOME/.local/share/anime-game-launcher/config.json` +pub fn config_file() -> Option { + launcher_dir().map(|dir| dir.join("config.json")) +}