feat(star-rail): added initial HSR support
This commit is contained in:
parent
c72815e906
commit
ed246f079b
14 changed files with 1046 additions and 3 deletions
|
@ -7,11 +7,11 @@ readme = "README.md"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies.anime-game-core]
|
||||
git = "https://github.com/an-anime-team/anime-game-core"
|
||||
tag = "1.7.4"
|
||||
# git = "https://github.com/an-anime-team/anime-game-core"
|
||||
# tag = "1.7.4"
|
||||
features = ["all"]
|
||||
|
||||
# path = "../anime-game-core" # ! for dev purposes only
|
||||
path = "../anime-game-core" # ! for dev purposes only
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0", features = ["backtrace"] }
|
||||
|
@ -31,6 +31,7 @@ discord-rich-presence = { version = "0.2.3", optional = true }
|
|||
[features]
|
||||
genshin = ["anime-game-core/genshin"]
|
||||
honkai = ["anime-game-core/honkai"]
|
||||
star-rail = ["anime-game-core/star-rail"]
|
||||
|
||||
# Common features
|
||||
states = []
|
||||
|
|
|
@ -9,6 +9,9 @@ pub mod genshin;
|
|||
#[cfg(feature = "honkai")]
|
||||
pub mod honkai;
|
||||
|
||||
#[cfg(feature = "star-rail")]
|
||||
pub mod star_rail;
|
||||
|
||||
#[cfg(feature = "config")]
|
||||
pub mod config;
|
||||
|
||||
|
|
58
src/star_rail/config/mod.rs
Normal file
58
src/star_rail/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::star_rail::consts::config_file;
|
||||
|
||||
static mut CONFIG: Option<schema::Schema> = None;
|
||||
|
||||
pub struct Config;
|
||||
|
||||
impl ConfigExt for Config {
|
||||
type Schema = schema::Schema;
|
||||
|
||||
#[inline]
|
||||
fn config_file() -> PathBuf {
|
||||
config_file().expect("Failed to resolve config file path")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn default_schema() -> Self::Schema {
|
||||
Self::Schema::default()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn serialize_schema(schema: Self::Schema) -> anyhow::Result<String> {
|
||||
Ok(serde_json::to_string_pretty(&schema)?)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn deserialize_schema<T: AsRef<str>>(schema: T) -> anyhow::Result<Self::Schema> {
|
||||
Ok(Self::Schema::from(&serde_json::from_str(schema.as_ref())?))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn clone_schema(schema: &Self::Schema) -> Self::Schema {
|
||||
schema.clone()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get() -> anyhow::Result<Self::Schema> {
|
||||
unsafe {
|
||||
match &CONFIG {
|
||||
Some(config) => Ok(config.clone()),
|
||||
None => Self::get_raw()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn update(schema: Self::Schema) {
|
||||
unsafe {
|
||||
CONFIG = Some(schema);
|
||||
}
|
||||
}
|
||||
}
|
60
src/star_rail/config/schema/components.rs
Normal file
60
src/star_rail/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::star_rail::consts::launcher_dir;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Components {
|
||||
pub path: PathBuf,
|
||||
pub servers: Vec<String>
|
||||
}
|
||||
|
||||
impl Default for Components {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
let launcher_dir = launcher_dir().expect("Failed to get launcher dir");
|
||||
|
||||
Self {
|
||||
path: launcher_dir.join("components"),
|
||||
servers: vec![
|
||||
String::from("https://github.com/an-anime-team/components")
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&JsonValue> for Components {
|
||||
fn from(value: &JsonValue) -> Self {
|
||||
let default = Self::default();
|
||||
|
||||
Self {
|
||||
path: match value.get("path") {
|
||||
Some(value) => match value.as_str() {
|
||||
Some(value) => PathBuf::from(value),
|
||||
None => default.path
|
||||
},
|
||||
None => default.path
|
||||
},
|
||||
|
||||
servers: match value.get("servers") {
|
||||
Some(value) => match value.as_array() {
|
||||
Some(values) => {
|
||||
let mut servers = Vec::new();
|
||||
|
||||
for value in values {
|
||||
if let Some(server) = value.as_str() {
|
||||
servers.push(server.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
servers
|
||||
},
|
||||
None => default.servers
|
||||
},
|
||||
None => default.servers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
40
src/star_rail/config/schema/game/enhancements.rs
Normal file
40
src/star_rail/config/schema/game/enhancements.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use crate::config::schema_blanks::prelude::*;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Enhancements {
|
||||
pub fsr: Fsr,
|
||||
pub gamemode: bool,
|
||||
pub hud: HUD,
|
||||
pub gamescope: Gamescope
|
||||
}
|
||||
|
||||
impl From<&JsonValue> for Enhancements {
|
||||
fn from(value: &JsonValue) -> Self {
|
||||
let default = Self::default();
|
||||
|
||||
Self {
|
||||
fsr: match value.get("fsr") {
|
||||
Some(value) => Fsr::from(value),
|
||||
None => default.fsr
|
||||
},
|
||||
|
||||
gamemode: match value.get("gamemode") {
|
||||
Some(value) => value.as_bool().unwrap_or(default.gamemode),
|
||||
None => default.gamemode
|
||||
},
|
||||
|
||||
hud: match value.get("hud") {
|
||||
Some(value) => HUD::from(value),
|
||||
None => default.hud
|
||||
},
|
||||
|
||||
gamescope: match value.get("gamescope") {
|
||||
Some(value) => Gamescope::from(value),
|
||||
None => default.gamescope
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
111
src/star_rail/config/schema/game/mod.rs
Normal file
111
src/star_rail/config/schema/game/mod.rs
Normal file
|
@ -0,0 +1,111 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use crate::config::schema_blanks::prelude::*;
|
||||
use crate::star_rail::consts::launcher_dir;
|
||||
|
||||
crate::config_impl_wine_schema!(launcher_dir);
|
||||
crate::config_impl_dxvk_schema!(launcher_dir);
|
||||
|
||||
pub mod enhancements;
|
||||
|
||||
pub mod prelude {
|
||||
pub use super::Wine;
|
||||
pub use super::Dxvk;
|
||||
|
||||
pub use super::enhancements::Enhancements;
|
||||
}
|
||||
|
||||
use prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Game {
|
||||
pub path: PathBuf,
|
||||
pub wine: Wine,
|
||||
pub dxvk: Dxvk,
|
||||
pub enhancements: Enhancements,
|
||||
pub environment: HashMap<String, String>,
|
||||
pub command: Option<String>
|
||||
}
|
||||
|
||||
impl Default for Game {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
let launcher_dir = launcher_dir().expect("Failed to get launcher dir");
|
||||
|
||||
Self {
|
||||
path: launcher_dir.join(concat!("Hon", "kai Sta", "r Rail")),
|
||||
wine: Wine::default(),
|
||||
dxvk: Dxvk::default(),
|
||||
enhancements: Enhancements::default(),
|
||||
environment: HashMap::new(),
|
||||
command: None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&JsonValue> for Game {
|
||||
fn from(value: &JsonValue) -> Self {
|
||||
let default = Self::default();
|
||||
|
||||
Self {
|
||||
path: match value.get("path") {
|
||||
Some(value) => match value.as_str() {
|
||||
Some(value) => PathBuf::from(value),
|
||||
None => default.path
|
||||
},
|
||||
None => default.path
|
||||
},
|
||||
|
||||
wine: match value.get("wine") {
|
||||
Some(value) => Wine::from(value),
|
||||
None => default.wine
|
||||
},
|
||||
|
||||
dxvk: match value.get("dxvk") {
|
||||
Some(value) => Dxvk::from(value),
|
||||
None => default.dxvk
|
||||
},
|
||||
|
||||
enhancements: match value.get("enhancements") {
|
||||
Some(value) => Enhancements::from(value),
|
||||
None => default.enhancements
|
||||
},
|
||||
|
||||
environment: match value.get("environment") {
|
||||
Some(value) => match value.as_object() {
|
||||
Some(values) => {
|
||||
let mut vars = HashMap::new();
|
||||
|
||||
for (name, value) in values {
|
||||
if let Some(value) = value.as_str() {
|
||||
vars.insert(name.clone(), value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
vars
|
||||
},
|
||||
None => default.environment
|
||||
},
|
||||
None => default.environment
|
||||
},
|
||||
|
||||
command: match value.get("command") {
|
||||
Some(value) => {
|
||||
if value.is_null() {
|
||||
None
|
||||
} else {
|
||||
match value.as_str() {
|
||||
Some(value) => Some(value.to_string()),
|
||||
None => default.command
|
||||
}
|
||||
}
|
||||
},
|
||||
None => default.command
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
75
src/star_rail/config/schema/launcher/discord_rpc.rs
Normal file
75
src/star_rail/config/schema/launcher/discord_rpc.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use crate::discord_rpc::DiscordRpcParams;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct DiscordRpc {
|
||||
pub app_id: u64,
|
||||
pub enabled: bool,
|
||||
pub title: String,
|
||||
pub subtitle: String,
|
||||
pub icon: String
|
||||
}
|
||||
|
||||
impl From<DiscordRpc> for DiscordRpcParams {
|
||||
#[inline]
|
||||
fn from(config: DiscordRpc) -> Self {
|
||||
Self {
|
||||
app_id: config.app_id,
|
||||
enabled: config.enabled,
|
||||
title: config.title,
|
||||
subtitle: config.subtitle,
|
||||
icon: config.icon
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add honkers-specific discord rpc
|
||||
|
||||
impl Default for DiscordRpc {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
app_id: 901534333360304168,
|
||||
enabled: false,
|
||||
|
||||
title: String::from("Researching the world"),
|
||||
subtitle: String::from("of Teyvat"),
|
||||
icon: String::from("launcher")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&JsonValue> for DiscordRpc {
|
||||
fn from(value: &JsonValue) -> Self {
|
||||
let default = Self::default();
|
||||
|
||||
Self {
|
||||
app_id: match value.get("app_id") {
|
||||
Some(value) => value.as_u64().unwrap_or(default.app_id),
|
||||
None => default.app_id
|
||||
},
|
||||
|
||||
enabled: match value.get("enabled") {
|
||||
Some(value) => value.as_bool().unwrap_or(default.enabled),
|
||||
None => default.enabled
|
||||
},
|
||||
|
||||
title: match value.get("title") {
|
||||
Some(value) => value.as_str().unwrap_or(&default.title).to_string(),
|
||||
None => default.title
|
||||
},
|
||||
|
||||
subtitle: match value.get("subtitle") {
|
||||
Some(value) => value.as_str().unwrap_or(&default.subtitle).to_string(),
|
||||
None => default.subtitle
|
||||
},
|
||||
|
||||
icon: match value.get("icon") {
|
||||
Some(value) => value.as_str().unwrap_or(&default.icon).to_string(),
|
||||
None => default.icon
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
101
src/star_rail/config/schema/launcher/mod.rs
Normal file
101
src/star_rail/config/schema/launcher/mod.rs
Normal file
|
@ -0,0 +1,101 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use enum_ordinalize::Ordinalize;
|
||||
|
||||
use crate::config::schema_blanks::prelude::*;
|
||||
use crate::star_rail::consts::launcher_dir;
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
pub mod discord_rpc;
|
||||
|
||||
pub mod prelude {
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
pub use super::discord_rpc::DiscordRpc;
|
||||
}
|
||||
|
||||
use prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ordinalize, Serialize, Deserialize)]
|
||||
pub enum LauncherStyle {
|
||||
Modern,
|
||||
Classic
|
||||
}
|
||||
|
||||
impl Default for LauncherStyle {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self::Modern
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Launcher {
|
||||
pub language: String,
|
||||
pub style: LauncherStyle,
|
||||
pub temp: Option<PathBuf>,
|
||||
pub repairer: Repairer,
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
pub discord_rpc: DiscordRpc
|
||||
}
|
||||
|
||||
impl Default for Launcher {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
language: String::from("en-us"),
|
||||
style: LauncherStyle::default(),
|
||||
temp: launcher_dir().ok(),
|
||||
repairer: Repairer::default(),
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
discord_rpc: DiscordRpc::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&JsonValue> for Launcher {
|
||||
fn from(value: &JsonValue) -> Self {
|
||||
let default = Self::default();
|
||||
|
||||
Self {
|
||||
language: match value.get("language") {
|
||||
Some(value) => value.as_str().unwrap_or(&default.language).to_string(),
|
||||
None => default.language
|
||||
},
|
||||
|
||||
style: match value.get("style") {
|
||||
Some(value) => serde_json::from_value(value.to_owned()).unwrap_or_default(),
|
||||
None => default.style
|
||||
},
|
||||
|
||||
temp: match value.get("temp") {
|
||||
Some(value) => {
|
||||
if value.is_null() {
|
||||
None
|
||||
} else {
|
||||
match value.as_str() {
|
||||
Some(value) => Some(PathBuf::from(value)),
|
||||
None => default.temp
|
||||
}
|
||||
}
|
||||
},
|
||||
None => default.temp
|
||||
},
|
||||
|
||||
repairer: match value.get("repairer") {
|
||||
Some(value) => Repairer::from(value),
|
||||
None => default.repairer
|
||||
},
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
discord_rpc: match value.get("discord_rpc") {
|
||||
Some(value) => DiscordRpc::from(value),
|
||||
None => default.discord_rpc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
132
src/star_rail/config/schema/mod.rs
Normal file
132
src/star_rail/config/schema/mod.rs
Normal file
|
@ -0,0 +1,132 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use wincompatlib::prelude::*;
|
||||
|
||||
#[cfg(feature = "sandbox")]
|
||||
use crate::config::schema_blanks::sandbox::Sandbox;
|
||||
|
||||
#[cfg(feature = "components")]
|
||||
use crate::components::wine::{
|
||||
WincompatlibWine,
|
||||
Version as WineVersion
|
||||
};
|
||||
|
||||
#[cfg(feature = "components")]
|
||||
use crate::components::dxvk::Version as DxvkVersion;
|
||||
|
||||
pub mod launcher;
|
||||
pub mod game;
|
||||
pub mod patch;
|
||||
|
||||
#[cfg(feature = "components")]
|
||||
pub mod components;
|
||||
|
||||
pub mod prelude {
|
||||
pub use super::launcher::prelude::*;
|
||||
pub use super::game::prelude::*;
|
||||
|
||||
pub use super::launcher::*;
|
||||
pub use super::game::*;
|
||||
pub use super::patch::*;
|
||||
|
||||
#[cfg(feature = "components")]
|
||||
pub use super::components::*;
|
||||
}
|
||||
|
||||
use prelude::*;
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Schema {
|
||||
pub launcher: Launcher,
|
||||
pub game: Game,
|
||||
|
||||
#[cfg(feature = "sandbox")]
|
||||
pub sandbox: Sandbox,
|
||||
|
||||
#[cfg(feature = "components")]
|
||||
pub components: Components,
|
||||
|
||||
pub patch: Patch
|
||||
}
|
||||
|
||||
impl From<&JsonValue> for Schema {
|
||||
fn from(value: &JsonValue) -> Self {
|
||||
let default = Self::default();
|
||||
|
||||
Self {
|
||||
launcher: match value.get("launcher") {
|
||||
Some(value) => Launcher::from(value),
|
||||
None => default.launcher
|
||||
},
|
||||
|
||||
game: match value.get("game") {
|
||||
Some(value) => Game::from(value),
|
||||
None => default.game
|
||||
},
|
||||
|
||||
#[cfg(feature = "sandbox")]
|
||||
sandbox: match value.get("sandbox") {
|
||||
Some(value) => Sandbox::from(value),
|
||||
None => default.sandbox
|
||||
},
|
||||
|
||||
#[cfg(feature = "components")]
|
||||
components: match value.get("components") {
|
||||
Some(value) => Components::from(value),
|
||||
None => default.components
|
||||
},
|
||||
|
||||
patch: match value.get("patch") {
|
||||
Some(value) => Patch::from(value),
|
||||
None => default.patch
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Schema {
|
||||
#[cfg(feature = "components")]
|
||||
/// Get selected wine version
|
||||
pub fn get_selected_wine(&self) -> anyhow::Result<Option<WineVersion>> {
|
||||
match &self.game.wine.selected {
|
||||
Some(selected) => WineVersion::find_in(&self.components.path, selected),
|
||||
None => Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "components")]
|
||||
/// Get selected dxvk version
|
||||
pub fn get_selected_dxvk(&self) -> anyhow::Result<Option<DxvkVersion>> {
|
||||
match wincompatlib::dxvk::Dxvk::get_version(&self.game.wine.prefix)? {
|
||||
Some(version) => DxvkVersion::find_in(&self.components.path, version),
|
||||
None => Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "components")]
|
||||
/// Resolve real wine prefix path using wincompatlib
|
||||
///
|
||||
/// - For general wine build returns `game.wine.prefix`
|
||||
/// - For proton-like builds return `game.wine.prefix`/`pfx`
|
||||
pub fn get_wine_prefix_path(&self) -> PathBuf {
|
||||
if let Ok(Some(wine)) = self.get_selected_wine() {
|
||||
let wine = wine
|
||||
.to_wine(&self.components.path, Some(&self.game.wine.builds.join(&wine.name)))
|
||||
.with_prefix(&self.game.wine.prefix);
|
||||
|
||||
let prefix = match wine {
|
||||
WincompatlibWine::Default(wine) => wine.prefix,
|
||||
WincompatlibWine::Proton(proton) => proton.wine().prefix.clone()
|
||||
};
|
||||
|
||||
if let Some(prefix) = prefix {
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
|
||||
self.game.wine.prefix.clone()
|
||||
}
|
||||
}
|
78
src/star_rail/config/schema/patch.rs
Normal file
78
src/star_rail/config/schema/patch.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
use crate::star_rail::consts::launcher_dir;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Patch {
|
||||
pub path: PathBuf,
|
||||
pub servers: Vec<String>,
|
||||
pub apply_xlua: bool,
|
||||
pub root: bool
|
||||
}
|
||||
|
||||
impl Default for Patch {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
let launcher_dir = launcher_dir().expect("Failed to get launcher dir");
|
||||
|
||||
Self {
|
||||
path: launcher_dir.join("patch"),
|
||||
|
||||
servers: vec![
|
||||
String::from("https://notabug.org/Krock/dawn")
|
||||
],
|
||||
|
||||
apply_xlua: true,
|
||||
|
||||
// Disable root requirement for patching if we're running launcher in flatpak
|
||||
root: !PathBuf::from("/.flatpak-info").exists()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&JsonValue> for Patch {
|
||||
fn from(value: &JsonValue) -> Self {
|
||||
let default = Self::default();
|
||||
|
||||
Self {
|
||||
path: match value.get("path") {
|
||||
Some(value) => match value.as_str() {
|
||||
Some(value) => PathBuf::from(value),
|
||||
None => default.path
|
||||
},
|
||||
None => default.path
|
||||
},
|
||||
|
||||
servers: match value.get("servers") {
|
||||
Some(value) => match value.as_array() {
|
||||
Some(values) => {
|
||||
let mut servers = Vec::new();
|
||||
|
||||
for value in values {
|
||||
if let Some(server) = value.as_str() {
|
||||
servers.push(server.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
servers
|
||||
},
|
||||
None => default.servers
|
||||
},
|
||||
None => default.servers
|
||||
},
|
||||
|
||||
apply_xlua: match value.get("apply_xlua") {
|
||||
Some(value) => value.as_bool().unwrap_or(default.apply_xlua),
|
||||
None => default.apply_xlua
|
||||
},
|
||||
|
||||
root: match value.get("root") {
|
||||
Some(value) => value.as_bool().unwrap_or(default.root),
|
||||
None => default.root
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
src/star_rail/consts.rs
Normal file
19
src/star_rail/consts.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
/// Get default launcher dir path
|
||||
///
|
||||
/// `$HOME/.local/share/honkers-railway-launcher`
|
||||
#[inline]
|
||||
pub fn launcher_dir() -> anyhow::Result<PathBuf> {
|
||||
Ok(std::env::var("XDG_DATA_HOME")
|
||||
.or_else(|_| std::env::var("HOME").map(|home| home + "/.local/share"))
|
||||
.map(|home| PathBuf::from(home).join("honkers-railway-launcher"))?)
|
||||
}
|
||||
|
||||
/// Get default config file path
|
||||
///
|
||||
/// `$HOME/.local/share/honkers-railway-launcher/config.json`
|
||||
#[inline]
|
||||
pub fn config_file() -> anyhow::Result<PathBuf> {
|
||||
launcher_dir().map(|dir| dir.join("config.json"))
|
||||
}
|
221
src/star_rail/game.rs
Normal file
221
src/star_rail/game.rs
Normal file
|
@ -0,0 +1,221 @@
|
|||
use std::process::{Command, Stdio};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anime_game_core::star_rail::telemetry;
|
||||
|
||||
use crate::config::ConfigExt;
|
||||
use crate::star_rail::config::Config;
|
||||
|
||||
use crate::star_rail::consts;
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
use crate::discord_rpc::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Folders {
|
||||
pub wine: PathBuf,
|
||||
pub prefix: PathBuf,
|
||||
pub game: PathBuf,
|
||||
pub temp: PathBuf
|
||||
}
|
||||
|
||||
fn replace_keywords(command: impl ToString, folders: &Folders) -> String {
|
||||
command.to_string()
|
||||
.replace("%build%", folders.wine.to_str().unwrap())
|
||||
.replace("%prefix%", folders.prefix.to_str().unwrap())
|
||||
.replace("%temp%", folders.game.to_str().unwrap())
|
||||
.replace("%launcher%", &consts::launcher_dir().unwrap().to_string_lossy())
|
||||
.replace("%game%", folders.temp.to_str().unwrap())
|
||||
}
|
||||
|
||||
/// Try to run the game
|
||||
///
|
||||
/// This function will freeze thread it was called from while the game is running
|
||||
#[tracing::instrument(level = "info", ret)]
|
||||
pub fn run() -> anyhow::Result<()> {
|
||||
tracing::info!("Preparing to run the game");
|
||||
|
||||
let config = Config::get()?;
|
||||
|
||||
if !config.game.path.exists() {
|
||||
return Err(anyhow::anyhow!("Game is not installed"));
|
||||
}
|
||||
|
||||
let Some(wine) = config.get_selected_wine()? else {
|
||||
anyhow::bail!("Couldn't find wine executable");
|
||||
};
|
||||
|
||||
let features = wine.features(&config.components.path)?.unwrap_or_default();
|
||||
|
||||
let mut folders = Folders {
|
||||
wine: config.game.wine.builds.join(&wine.name),
|
||||
prefix: config.game.wine.prefix.clone(),
|
||||
game: config.game.path.clone(),
|
||||
temp: config.launcher.temp.clone().unwrap_or(std::env::temp_dir())
|
||||
};
|
||||
|
||||
// Check telemetry servers
|
||||
|
||||
tracing::info!("Checking telemetry");
|
||||
|
||||
if let Ok(Some(server)) = telemetry::is_disabled() {
|
||||
return Err(anyhow::anyhow!("Telemetry server is not disabled: {server}"));
|
||||
}
|
||||
|
||||
// Prepare bash -c '<command>'
|
||||
|
||||
let mut bash_command = String::new();
|
||||
let mut windows_command = String::new();
|
||||
|
||||
if config.game.enhancements.gamemode {
|
||||
bash_command += "gamemoderun ";
|
||||
}
|
||||
|
||||
let run_command = features.command
|
||||
.map(|command| replace_keywords(command, &folders))
|
||||
.unwrap_or(format!("'{}'", folders.wine.join(wine.files.wine64.unwrap_or(wine.files.wine)).to_string_lossy()));
|
||||
|
||||
bash_command += &run_command;
|
||||
bash_command += " ";
|
||||
|
||||
if let Some(virtual_desktop) = config.game.wine.virtual_desktop.get_command("an_anime_game") {
|
||||
windows_command += &virtual_desktop;
|
||||
windows_command += " ";
|
||||
}
|
||||
|
||||
windows_command += "launcher.bat ";
|
||||
|
||||
if config.game.wine.borderless {
|
||||
windows_command += "-screen-fullscreen 0 -popupwindow ";
|
||||
}
|
||||
|
||||
// https://notabug.org/Krock/dawn/src/master/TWEAKS.md
|
||||
if config.game.enhancements.fsr.enabled {
|
||||
windows_command += "-window-mode exclusive ";
|
||||
}
|
||||
|
||||
// gamescope <params> -- <command to run>
|
||||
if let Some(gamescope) = config.game.enhancements.gamescope.get_command() {
|
||||
bash_command = format!("{gamescope} -- {bash_command}");
|
||||
}
|
||||
|
||||
// bwrap <params> -- <command to run>
|
||||
#[cfg(feature = "sandbox")]
|
||||
if config.sandbox.enabled {
|
||||
let bwrap = config.sandbox.get_command(
|
||||
folders.wine.to_str().unwrap(),
|
||||
folders.prefix.to_str().unwrap(),
|
||||
folders.game.to_str().unwrap()
|
||||
);
|
||||
|
||||
let sandboxed_folders = Folders {
|
||||
wine: PathBuf::from("/tmp/sandbox/wine"),
|
||||
prefix: PathBuf::from("/tmp/sandbox/prefix"),
|
||||
game: PathBuf::from("/tmp/sandbox/game"),
|
||||
temp: PathBuf::from("/tmp")
|
||||
};
|
||||
|
||||
bash_command = bash_command
|
||||
.replace(folders.wine.to_str().unwrap(), sandboxed_folders.wine.to_str().unwrap())
|
||||
.replace(folders.prefix.to_str().unwrap(), sandboxed_folders.prefix.to_str().unwrap())
|
||||
.replace(folders.game.to_str().unwrap(), sandboxed_folders.game.to_str().unwrap())
|
||||
.replace(folders.temp.to_str().unwrap(), sandboxed_folders.temp.to_str().unwrap());
|
||||
|
||||
bash_command = format!("{bwrap} --chdir /tmp/sandbox/game -- {bash_command}");
|
||||
folders = sandboxed_folders;
|
||||
}
|
||||
|
||||
// Bundle all windows arguments used to run the game into a single file
|
||||
if features.compact_launch {
|
||||
std::fs::write(folders.game.join("compact_launch.bat"), format!("start {windows_command}\nexit"))?;
|
||||
|
||||
windows_command = String::from("compact_launch.bat");
|
||||
}
|
||||
|
||||
// Finalize launching command
|
||||
bash_command = match &config.game.command {
|
||||
// Use user-given launch command
|
||||
Some(command) => replace_keywords(command, &folders)
|
||||
.replace("%command%", &format!("{bash_command} {windows_command}"))
|
||||
.replace("%bash_command%", &bash_command)
|
||||
.replace("%windows_command%", &windows_command),
|
||||
|
||||
// Combine bash and windows parts of the command
|
||||
None => format!("{bash_command} {windows_command}")
|
||||
};
|
||||
|
||||
let mut command = Command::new("bash");
|
||||
|
||||
command.arg("-c");
|
||||
command.arg(&bash_command);
|
||||
|
||||
// Setup environment
|
||||
|
||||
command.env("WINEARCH", "win64");
|
||||
command.env("WINEPREFIX", &folders.prefix);
|
||||
|
||||
// Add environment flags for selected wine
|
||||
for (key, value) in features.env.into_iter() {
|
||||
command.env(key, replace_keywords(value, &folders));
|
||||
}
|
||||
|
||||
// Add environment flags for selected dxvk
|
||||
if let Ok(Some(dxvk )) = config.get_selected_dxvk() {
|
||||
if let Ok(Some(features)) = dxvk.features(&config.components.path) {
|
||||
for (key, value) in features.env.iter() {
|
||||
command.env(key, replace_keywords(value, &folders));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
command.envs(config.game.wine.sync.get_env_vars());
|
||||
command.envs(config.game.enhancements.hud.get_env_vars(config.game.enhancements.gamescope.enabled));
|
||||
command.envs(config.game.enhancements.fsr.get_env_vars());
|
||||
command.envs(config.game.wine.language.get_env_vars());
|
||||
|
||||
command.envs(config.game.environment);
|
||||
|
||||
// Run command
|
||||
|
||||
let variables = command
|
||||
.get_envs()
|
||||
.map(|(key, value)| format!("{}=\"{}\"", key.to_string_lossy(), value.unwrap_or_default().to_string_lossy()))
|
||||
.fold(String::new(), |acc, env| acc + " " + &env);
|
||||
|
||||
tracing::info!("Running the game with command: {variables} bash -c \"{bash_command}\"");
|
||||
|
||||
// We use real current dir here because sandboxed one
|
||||
// obviously doesn't exist
|
||||
command.current_dir(&config.game.path)
|
||||
.spawn()?.wait_with_output()?;
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
let rpc = if config.launcher.discord_rpc.enabled {
|
||||
Some(DiscordRpc::new(config.launcher.discord_rpc.into()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
if let Some(rpc) = &rpc {
|
||||
rpc.update(RpcUpdates::Connect)?;
|
||||
}
|
||||
|
||||
loop {
|
||||
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||
|
||||
let output = Command::new("ps").arg("-A").stdout(Stdio::piped()).output()?;
|
||||
let output = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
if !output.contains("GenshinImpact.e") && !output.contains("unlocker.exe") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
if let Some(rpc) = &rpc {
|
||||
rpc.update(RpcUpdates::Disconnect)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
10
src/star_rail/mod.rs
Normal file
10
src/star_rail/mod.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
pub mod consts;
|
||||
|
||||
#[cfg(feature = "config")]
|
||||
pub mod config;
|
||||
|
||||
#[cfg(feature = "states")]
|
||||
pub mod states;
|
||||
|
||||
#[cfg(feature = "game")]
|
||||
pub mod game;
|
134
src/star_rail/states.rs
Normal file
134
src/star_rail/states.rs
Normal file
|
@ -0,0 +1,134 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use anime_game_core::prelude::*;
|
||||
use anime_game_core::star_rail::prelude::*;
|
||||
|
||||
use crate::config::ConfigExt;
|
||||
use crate::star_rail::config::Config;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum LauncherState {
|
||||
Launch,
|
||||
|
||||
/// Always contains `VersionDiff::Predownload`
|
||||
PredownloadAvailable(VersionDiff),
|
||||
|
||||
MainPatchAvailable(MainPatch),
|
||||
|
||||
#[cfg(feature = "components")]
|
||||
WineNotInstalled,
|
||||
|
||||
PrefixNotExists,
|
||||
|
||||
// Always contains `VersionDiff::Diff`
|
||||
GameUpdateAvailable(VersionDiff),
|
||||
|
||||
/// Always contains `VersionDiff::Outdated`
|
||||
GameOutdated(VersionDiff),
|
||||
|
||||
/// Always contains `VersionDiff::NotInstalled`
|
||||
GameNotInstalled(VersionDiff)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum StateUpdating {
|
||||
Game,
|
||||
Patch
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LauncherStateParams<F: Fn(StateUpdating)> {
|
||||
pub wine_prefix: PathBuf,
|
||||
pub game_path: PathBuf,
|
||||
|
||||
pub patch_servers: Vec<String>,
|
||||
pub patch_folder: PathBuf,
|
||||
|
||||
pub status_updater: F
|
||||
}
|
||||
|
||||
impl LauncherState {
|
||||
pub fn get<F: Fn(StateUpdating)>(params: LauncherStateParams<F>) -> anyhow::Result<Self> {
|
||||
tracing::debug!("Trying to get launcher state");
|
||||
|
||||
// Check prefix existence
|
||||
if !params.wine_prefix.join("drive_c").exists() {
|
||||
return Ok(Self::PrefixNotExists);
|
||||
}
|
||||
|
||||
// Check game installation status
|
||||
(params.status_updater)(StateUpdating::Game);
|
||||
|
||||
let game = Game::new(¶ms.game_path);
|
||||
let diff = game.try_get_diff()?;
|
||||
|
||||
match diff {
|
||||
VersionDiff::Latest(_) | VersionDiff::Predownload { .. } => {
|
||||
// Check game patch status
|
||||
(params.status_updater)(StateUpdating::Patch);
|
||||
|
||||
let patch = Patch::new(¶ms.patch_folder);
|
||||
|
||||
// Sync local patch folder with remote if needed
|
||||
// TODO: maybe I shouldn't do it here?
|
||||
if patch.is_sync(¶ms.patch_servers)?.is_none() {
|
||||
for server in ¶ms.patch_servers {
|
||||
if patch.sync(server).is_ok() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check the main patch
|
||||
let main_patch = patch.main_patch()?;
|
||||
|
||||
if !main_patch.is_applied(¶ms.game_path)? {
|
||||
return Ok(Self::MainPatchAvailable(main_patch));
|
||||
}
|
||||
|
||||
// Check if update predownload available
|
||||
if let VersionDiff::Predownload { .. } = diff {
|
||||
Ok(Self::PredownloadAvailable(diff))
|
||||
}
|
||||
|
||||
// Otherwise we can launch the game
|
||||
else {
|
||||
Ok(Self::Launch)
|
||||
}
|
||||
}
|
||||
|
||||
VersionDiff::Diff { .. } => Ok(Self::GameUpdateAvailable(diff)),
|
||||
VersionDiff::Outdated { .. } => Ok(Self::GameOutdated(diff)),
|
||||
VersionDiff::NotInstalled { .. } => Ok(Self::GameNotInstalled(diff))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "config")]
|
||||
#[tracing::instrument(level = "debug", skip(status_updater), ret)]
|
||||
pub fn get_from_config<T: Fn(StateUpdating)>(status_updater: T) -> anyhow::Result<Self> {
|
||||
tracing::debug!("Trying to get launcher state");
|
||||
|
||||
let config = Config::get()?;
|
||||
|
||||
match &config.game.wine.selected {
|
||||
#[cfg(feature = "components")]
|
||||
Some(selected) if !config.game.wine.builds.join(selected).exists() => return Ok(Self::WineNotInstalled),
|
||||
|
||||
None => return Ok(Self::WineNotInstalled),
|
||||
|
||||
_ => ()
|
||||
}
|
||||
|
||||
Self::get(LauncherStateParams {
|
||||
wine_prefix: config.get_wine_prefix_path(),
|
||||
game_path: config.game.path,
|
||||
|
||||
patch_servers: config.patch.servers,
|
||||
patch_folder: config.patch.path,
|
||||
|
||||
status_updater
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue