feat(zzz): initial implementation

This commit is contained in:
Nikita Podvirnyi 2024-07-04 09:37:47 +02:00
parent eebd048fb0
commit d347bbe4d9
No known key found for this signature in database
GPG key ID: 859D416E5142AFF3
17 changed files with 1334 additions and 3 deletions

View file

@ -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.20.2" tag = "1.21.0"
features = ["all"] features = ["all"]
# path = "../anime-game-core" # ! for dev purposes only # path = "../anime-game-core" # ! for dev purposes only
@ -21,17 +21,18 @@ tracing = "0.1"
serde = { version = "1.0", features = ["derive"], optional = true } serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", 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 } enum-ordinalize = { version = "4.3", optional = true }
wincompatlib = { version = "0.7.4", features = ["all"], 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 } md-5 = { version = "0.10", features = ["asm"], optional = true }
discord-rich-presence = { version = "0.2.4", optional = true } discord-rich-presence = { version = "0.2.4", optional = true }
[features] [features]
genshin = ["anime-game-core/genshin"] genshin = ["anime-game-core/genshin"]
star-rail = ["anime-game-core/star-rail"] star-rail = ["anime-game-core/star-rail"]
zzz = ["anime-game-core/zzz"]
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"] wuwa = ["anime-game-core/wuwa"]

View file

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

View 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);
}
}
}

View 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
}
}
}
}

View 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)
}
}
}

View 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
}
}
}
}

View 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),
}
}
}

View 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
}
}
}
}

View 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
}
}
}
}

View 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
View 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"))
}

View 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
View 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
View 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
View 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
View 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(&params.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
})
}
}

View file

@ -11,6 +11,9 @@ pub use games::genshin;
#[cfg(feature = "star-rail")] #[cfg(feature = "star-rail")]
pub use games::star_rail; pub use games::star_rail;
#[cfg(feature = "zzz")]
pub use games::zzz;
#[cfg(feature = "honkai")] #[cfg(feature = "honkai")]
pub use games::honkai; pub use games::honkai;