use std::{
collections::HashMap,
ffi::OsString,
fmt::{Display, Formatter, Result},
path::Path,
};
use inner_future::process::{Child, Command};
use super::{
auth::structs::AuthMethod,
version::structs::{Argument, VersionInfo},
};
use crate::{
java::JavaRuntime,
prelude::*,
utils::{get_full_path, CLASSPATH_SEPARATOR, NATIVE_ARCH_LAZY, TARGET_OS},
version::structs::{Allowed, VersionMeta},
};
pub const LOG4J_PATCH: &[u8] = include_bytes!("../assets/log4j-patch-agent-1.0.jar");
#[derive(Debug, Clone)]
pub struct ClientConfig {
pub auth: AuthMethod,
pub version_info: VersionInfo,
pub version_type: String,
pub custom_java_args: Vec<String>,
pub custom_args: Vec<String>,
pub java_runtime: JavaRuntime,
pub max_mem: u32,
pub recheck: bool,
}
pub struct Client {
pub cmd: Command,
pub game_dir: String,
pub java_path: String,
pub args: Vec<String>,
pub process: Option<Child>,
}
fn get_game_directory(cfg: &ClientConfig) -> String {
let version_base = std::path::Path::new(&cfg.version_info.version_base);
let version_dir = version_base.join(&cfg.version_info.version);
let version_dir = get_full_path(version_dir);
let game_dir = version_base.parent().unwrap();
let game_dir = get_full_path(game_dir);
if let Some(_meta) = &cfg.version_info.meta {
if let Some(scl) = &cfg.version_info.scl_launch_config {
if scl.game_independent {
version_dir
} else {
game_dir
}
} else {
game_dir
}
} else {
game_dir
}
}
async fn parse_inheritsed_meta(cfg: &ClientConfig) -> VersionMeta {
let meta = cfg.version_info.meta.as_ref().unwrap();
let inherits_from = if !meta.inherits_from.is_empty() {
meta.inherits_from.as_str()
} else if !meta.client_version.is_empty() && cfg.version_info.version != meta.client_version {
meta.client_version.as_str()
} else {
""
};
if inherits_from.is_empty() {
meta.to_owned()
} else {
let meta = meta.to_owned();
let mut base_info = VersionInfo {
version: inherits_from.to_owned(),
version_base: cfg.version_info.version_base.to_owned(),
..Default::default()
};
if base_info.load().await.is_ok() {
if let Some(base) = &mut base_info.meta {
let mut base = base.to_owned();
base += meta;
base
} else {
meta.to_owned()
}
} else {
meta
}
}
}
impl Client {
pub async fn new(mut cfg: ClientConfig) -> DynResult<Self> {
if cfg.version_info.meta.is_none() {
anyhow::bail!("version_info is empty");
}
let mut args = Vec::<String>::with_capacity(64);
let meta = parse_inheritsed_meta(&cfg).await;
cfg.version_info.meta = Some(meta.to_owned());
let java_runtime = if let Some(scl_config) = &cfg.version_info.scl_launch_config {
if scl_config.java_path.is_empty() {
cfg.java_runtime.to_owned()
} else {
JavaRuntime::from_java_path(OsString::from(&scl_config.java_path)).await?
}
} else {
cfg.java_runtime.to_owned()
};
let mut variables: HashMap<&'static str, String> = HashMap::with_capacity(19);
variables.insert("${library_directory}", {
let lib_path = std::path::Path::new(&cfg.version_info.version_base);
let lib_path = lib_path
.parent()
.ok_or_else(|| anyhow::anyhow!("There's no parent from the library path"))?
.join("libraries");
let lib_path = get_full_path(lib_path);
lib_path.replace(|a| a == '/' || a == '\\', "/")
});
variables.insert("${classpath}", {
let lib_base_path = variables
.get("${library_directory}")
.unwrap()
.replace('/', std::path::MAIN_SEPARATOR_STR);
let mut lib_args: HashMap<String, String> =
HashMap::with_capacity(meta.libraries.len());
for lib in &meta.libraries {
let class_name = lib.name.as_str()
[0..lib.name.rfind(':').expect("Can't parse class name")]
.to_string();
if !lib.rules.is_allowed() {
continue;
}
let lib_path = {
let lib: Vec<&str> = lib.name.splitn(3, ':').collect();
let (package, name, version) = (lib[0], lib[1], lib[2]);
let package_path: Vec<&str> = package.split('.').collect();
format!(
"{}{sep}{}{sep}{}{sep}{}{sep}{}-{}.jar",
lib_base_path,
package_path.join(std::path::MAIN_SEPARATOR_STR),
name,
version,
name,
version,
sep = std::path::MAIN_SEPARATOR,
)
};
let mut lib_path = if let Some(ds) = &lib.downloads {
if let Some(d) = &ds.artifact {
format!(
"{}{sep}{}",
lib_base_path,
d.path
.replace(|a| a == '/' || a == '\\', std::path::MAIN_SEPARATOR_STR),
sep = std::path::MAIN_SEPARATOR
)
} else {
lib_path
}
} else {
lib_path
};
if let Some(n) = &lib.natives {
if false {
if let Some(native_key) = n.get(TARGET_OS) {
let native_key =
native_key.replace("${arch}", NATIVE_ARCH_LAZY.as_ref());
let classifier = lib
.downloads
.as_ref()
.ok_or_else(|| {
anyhow::anyhow!("No downloads struct for {}", &native_key)
})?
.classifiers
.as_ref()
.ok_or_else(|| {
anyhow::anyhow!("No classifiers struct for {}", &native_key)
})?
.get(&native_key)
.ok_or_else(|| {
anyhow::anyhow!("No classifier struct for {}", &native_key)
})?;
lib_path += CLASSPATH_SEPARATOR;
lib_path += &lib_base_path;
lib_path.push(std::path::MAIN_SEPARATOR);
lib_path += &classifier
.path
.replace(|a| a == '/' || a == '\\', std::path::MAIN_SEPARATOR_STR);
}
}
}
lib_args.insert(class_name, lib_path);
}
let lib_args: Vec<_> =
if meta.main_class == "cpw.mods.bootstraplauncher.BootstrapLauncher" {
lib_args.into_iter().map(|x| x.1).collect()
} else {
lib_args
.into_iter()
.map(|x| x.1)
.chain(meta.main_jars.iter().map(|a| {
a.replace(|a| a == '/' || a == '\\', std::path::MAIN_SEPARATOR_STR)
}))
.collect()
};
#[cfg(target_os = "windows")]
{
lib_args.join(";")
}
#[cfg(target_os = "linux")]
{
lib_args.join(":")
}
#[cfg(target_os = "macos")]
{
lib_args.join(":")
}
});
variables.insert("${max_memory}", format!("-Xmx{}m", cfg.max_mem));
variables.insert(
"${auth_player_name}",
match &cfg.auth {
AuthMethod::Offline { player_name, .. } => player_name.to_owned(),
AuthMethod::Mojang { player_name, .. } => player_name.to_owned(),
AuthMethod::Microsoft { player_name, .. } => player_name.to_owned(),
AuthMethod::AuthlibInjector { player_name, .. } => player_name.to_owned(),
},
);
#[cfg(target_os = "windows")]
let sub_native_dir = match java_runtime.arch() {
crate::utils::Arch::X86 => "natives-windows-x86",
crate::utils::Arch::X64 => "natives-windows",
crate::utils::Arch::ARM64 => "natives-windows-arm64",
};
#[cfg(target_os = "linux")]
let sub_native_dir = "natives-linux";
#[cfg(target_os = "macos")]
let sub_native_dir = match java_runtime.arch() {
crate::utils::Arch::X86 | crate::utils::Arch::X64 => "natives-macos",
crate::utils::Arch::ARM64 => "natives-macos-arm64",
};
variables.insert(
"${natives_directory}",
get_full_path(Path::new(&format!(
"{}{sep}{ver}{sep}natives/{}",
cfg.version_info.version_base,
sub_native_dir,
ver = cfg.version_info.version,
sep = std::path::MAIN_SEPARATOR,
))),
);
variables.insert("${version_name}", cfg.version_info.version.to_owned());
variables.insert("${classpath_separator}", CLASSPATH_SEPARATOR.to_owned());
variables.insert("${game_directory}", get_game_directory(&cfg));
variables.insert("${assets_root}", {
let assets_path = std::path::Path::new(&cfg.version_info.version_base);
let assets_path = assets_path.parent().unwrap().join("assets");
if cfg
.version_info
.meta
.as_ref()
.map(|x| {
x.asset_index
.as_ref()
.map(|x| &x.id == "pre-1.6")
.unwrap_or_default()
})
.unwrap_or_default()
{
let game_dir = get_game_directory(&cfg);
let resources_path = Path::new(&game_dir).join("resources");
let assets_path = assets_path.join("virtual").join("pre-1.6");
tracing::trace!(
"正在映射 {} 至 {}",
assets_path.to_string_lossy(),
resources_path.to_string_lossy()
);
let _ = fs_extra::dir::copy(
&assets_path,
&resources_path,
&fs_extra::dir::CopyOptions::new()
.skip_exist(true)
.content_only(true),
);
get_full_path(assets_path)
} else {
get_full_path(assets_path)
}
});
variables.insert(
"${game_assets}",
variables.get("${assets_root}").unwrap().to_owned(),
);
variables.insert(
"${assets_index_name}",
if let Some(asset_index) = &meta.asset_index {
asset_index.id.to_owned()
} else {
String::new()
},
);
variables.insert("${auth_session}", "token:0".into());
variables.insert("${clientid}", "00000000402b5328".into());
variables.insert(
"${auth_access_token}",
match &cfg.auth {
AuthMethod::Offline { uuid, .. } => uuid.repeat(2), AuthMethod::Mojang { access_token, .. } => access_token.to_owned_string(),
AuthMethod::Microsoft { access_token, .. } => access_token.to_owned_string(),
AuthMethod::AuthlibInjector { access_token, .. } => access_token.to_owned_string(),
},
);
variables.insert(
"${auth_uuid}",
match &cfg.auth {
AuthMethod::Offline { uuid, .. } => uuid.to_owned(),
AuthMethod::Mojang { uuid, .. } => uuid.to_owned(),
AuthMethod::Microsoft { uuid, .. } => uuid.to_owned(),
AuthMethod::AuthlibInjector { uuid, .. } => uuid.to_owned(),
},
);
variables.insert(
"${user_type}",
match &cfg.auth {
AuthMethod::Offline { .. } => "Legacy".into(),
AuthMethod::Mojang { .. } | AuthMethod::AuthlibInjector { .. } => "Mojang".into(),
AuthMethod::Microsoft { uuid: _, .. } => "msa".into(),
},
);
variables.insert("${version_type}", cfg.version_type.to_owned());
variables.insert("${user_properties}", "{}".into());
variables.insert("${launcher_name}", "SharpCraftLauncher".into());
variables.insert("${launcher_version}", "221".into());
fn replace_each(variables: &HashMap<&'static str, String>, arg: String) -> String {
let mut arg = arg;
for (k, v) in variables {
if arg.contains(*k) {
arg = arg.replace(*k, v);
}
}
arg
}
args.push("-Dlog4j2.formatMsgNoLookups=true".into());
for arg in &cfg.custom_java_args {
args.push(arg.to_owned());
}
if let Some(scl_config) = &cfg.version_info.scl_launch_config {
if !scl_config.jvm_args.trim().is_empty() {
if let Ok(jvm_args) = shell_words::split(&scl_config.jvm_args) {
for arg in jvm_args {
args.push(arg);
}
} else {
args.push(scl_config.jvm_args.to_owned());
}
}
}
if let AuthMethod::AuthlibInjector {
api_location,
server_meta,
..
} = &cfg.auth
{
let authlib_injector_path = get_full_path(format!(
"{}{sep}..{sep}authlib-injector.jar",
cfg.version_info.version_base,
sep = std::path::MAIN_SEPARATOR
));
args.push(format!("-javaagent:{authlib_injector_path}={api_location}"));
args.push(format!(
"-Dauthlibinjector.yggdrasil.prefetched={server_meta}"
));
}
if let Some(max_mem) = variables.get("${max_memory}") {
args.push(max_mem.to_owned());
}
if let Some(arguments) = &meta.arguments {
for arg in &arguments.jvm {
match arg {
Argument::Common(arg) => args.push(replace_each(&variables, arg.to_owned())),
Argument::Specify(arg) => {
if arg.rules.is_allowed() {
for value in arg.value.iter() {
args.push(value.to_owned())
}
}
}
}
}
} else {
args.push(format!(
"-Djava.library.path={}",
variables.get("${natives_directory}").unwrap()
));
args.push(format!(
"-Dminecraft.launcher.brand={}",
variables.get("${launcher_name}").unwrap()
));
args.push(format!(
"-Dminecraft.launcher.version={}",
variables.get("${launcher_version}").unwrap()
));
args.push("-cp".into());
args.push(variables.get("${classpath}").unwrap().to_owned());
}
args.push(meta.main_class.to_owned());
fn dedup_argument(args: &mut Vec<String>, arg: &String) -> bool {
let exist_arg = args.iter().enumerate().find(|x| x.1 == arg).map(|x| x.0);
if let Some(exist_arg) = exist_arg {
args.remove(exist_arg);
if arg.starts_with('-') {
args.remove(exist_arg);
}
true
} else {
false
}
}
let splited = meta.minecraft_arguments.trim().split(' ');
let mut skip_next_dedup = false;
for arg in splited {
if !arg.is_empty() {
let arg = replace_each(&variables, arg.to_owned());
if skip_next_dedup {
skip_next_dedup = false
} else {
skip_next_dedup = dedup_argument(&mut args, &arg);
}
args.push(arg);
}
}
if let Some(arguments) = &meta.arguments {
let mut skip_next_dedup = false;
for arg in &arguments.game {
match arg {
Argument::Common(arg) => {
let arg = replace_each(&variables, arg.to_owned());
if skip_next_dedup {
skip_next_dedup = false
} else {
skip_next_dedup = dedup_argument(&mut args, &arg);
}
args.push(arg);
}
Argument::Specify(_) => {
}
}
}
}
for arg in &cfg.custom_args {
args.push(arg.to_owned());
}
if let Some(scl_config) = &cfg.version_info.scl_launch_config {
if !scl_config.game_args.trim().is_empty() {
if let Ok(game_args) = shell_words::split(&scl_config.game_args) {
for arg in game_args {
args.push(arg);
}
} else {
args.push(scl_config.game_args.to_owned());
}
}
}
let wrapper_path = cfg
.version_info
.scl_launch_config
.as_ref()
.map(|x| x.wrapper_path.to_owned())
.unwrap_or_default();
let mut cmd = if wrapper_path.is_empty() {
Command::new(java_runtime.path())
} else {
let mut cmd = Command::new(&wrapper_path);
let wrapper_args = cfg
.version_info
.scl_launch_config
.as_ref()
.map(|x| x.wrapper_args.to_owned())
.unwrap_or_default();
if !wrapper_args.is_empty() {
cmd.arg(wrapper_args);
}
cmd.arg(java_runtime.path());
cmd
};
cmd.args(&args);
cmd.current_dir(get_game_directory(&cfg));
#[cfg(target_os = "windows")]
{
cmd.env("APPDATA", get_game_directory(&cfg));
}
cmd.env("FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS", "true");
args.insert(0, java_runtime.path().to_owned());
Ok(Self {
cmd,
game_dir: get_game_directory(&cfg),
java_path: java_runtime.path().to_owned(),
args,
process: None,
})
}
pub fn stdin(mut self, cfg: impl Into<std::process::Stdio>) -> Self {
self.set_stdin(cfg);
self
}
pub fn stdout(mut self, cfg: impl Into<std::process::Stdio>) -> Self {
self.set_stdout(cfg);
self
}
pub fn stderr(mut self, cfg: impl Into<std::process::Stdio>) -> Self {
self.set_stderr(cfg);
self
}
pub fn set_stdin(&mut self, cfg: impl Into<std::process::Stdio>) {
self.cmd.stdin(cfg);
}
pub fn set_stdout(&mut self, cfg: impl Into<std::process::Stdio>) {
self.cmd.stdout(cfg);
}
pub fn set_stderr(&mut self, cfg: impl Into<std::process::Stdio>) {
self.cmd.stderr(cfg);
}
pub fn get_args(&self) -> &[String] {
&self.args
}
pub fn take_args(self) -> Vec<String> {
self.args
}
pub fn take_cmd(self) -> Command {
self.cmd
}
pub async fn launch(&mut self) -> DynResult<u32> {
#[cfg(windows)]
{
use inner_future::process::windows::*;
self.cmd.creation_flags(0x08000000);
}
let c = match self.cmd.spawn() {
Ok(c) => c,
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
anyhow::bail!("启动游戏时发生错误:找不到 Java 执行文件,请确认你的 Java 文件是否存在 {:?}", e)
} else {
anyhow::bail!("启动游戏时发生错误 {:?}", e)
}
}
};
let pid = c.id();
self.process = Some(c);
Ok(pid)
}
pub fn stop(&mut self) -> DynResult {
if let Some(mut p) = self.process.take() {
p.kill()?;
}
Ok(())
}
}
impl Display for Client {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let running = if self.process.is_some() {
"running"
} else {
"idle"
};
write!(f, "[MCClient {} args={:?}]", running, self.args)
}
}