scl_core/
client.rs

1//! 客户端结构,用于启动游戏
2use std::{
3    collections::HashMap,
4    fmt::{Display, Formatter, Result},
5    path::Path,
6};
7
8use inner_future::process::{Child, Command};
9
10use super::{
11    auth::structs::AuthMethod,
12    version::structs::{Argument, VersionInfo},
13};
14use crate::{
15    prelude::*,
16    utils::{get_full_path, CLASSPATH_SEPARATOR, NATIVE_ARCH_LAZY, TARGET_OS},
17    version::structs::{Allowed, VersionMeta},
18};
19
20/// 用于修复 CVE-2021-44228 远程代码执行漏洞
21///
22/// 似乎只需要加 `-Dlog4j2.formatMsgNoLookups=true` 参数到 `classpath` 之前就可以解决问题了
23///
24/// 一般用不到这个
25pub const LOG4J_PATCH: &[u8] = include_bytes!("../assets/log4j-patch-agent-1.0.jar");
26
27/// 一个客户端配置结构,开发者需要填充内部的一部分数据后传递给 [`Client::new`] 方可正确启动游戏
28#[derive(Debug, Clone)]
29pub struct ClientConfig {
30    /// 使用的玩家账户
31    pub auth: AuthMethod,
32    /// 启动的版本元数据信息
33    pub version_info: VersionInfo,
34    /// 启动的版本类型
35    pub version_type: String,
36    /// 自定义 JVM 参数,这将会附加在 Class Path 之前的位置
37    pub custom_java_args: Vec<String>,
38    /// 自定义游戏参数,这将会附加在参数的最后部分
39    pub custom_args: Vec<String>,
40    /// 需要使用的 Java 运行时文件路径
41    pub java_path: String,
42    /// 最高内存,以 MB 为单位
43    pub max_mem: u32,
44    /// 是否进行预先资源及依赖检查
45    pub recheck: bool,
46}
47
48/// 一个客户端结构,通过 [`ClientConfig`] 提供的信息组合启动参数,运行游戏
49pub struct Client {
50    /// 客户端的实际指令对象
51    pub cmd: Command,
52    /// 当前游戏目录路径
53    pub game_dir: String,
54    /// 当前使用的 Java 运行时路径
55    pub java_path: String,
56    /// 当前启动参数的副本,包含 Java 自身
57    pub args: Vec<String>,
58    /// 正在运行的进程对象
59    pub process: Option<Child>,
60}
61
62fn get_game_directory(cfg: &ClientConfig) -> String {
63    let version_base = std::path::Path::new(&cfg.version_info.version_base);
64    let version_dir = version_base.join(&cfg.version_info.version);
65    let version_dir = get_full_path(version_dir);
66    let game_dir = version_base.parent().unwrap();
67    let game_dir = get_full_path(game_dir);
68    if let Some(_meta) = &cfg.version_info.meta {
69        if let Some(scl) = &cfg.version_info.scl_launch_config {
70            if scl.game_independent {
71                version_dir
72            } else {
73                game_dir
74            }
75        } else {
76            game_dir
77        }
78    } else {
79        game_dir
80    }
81}
82
83async fn parse_inheritsed_meta(cfg: &ClientConfig) -> VersionMeta {
84    let meta = cfg.version_info.meta.as_ref().unwrap();
85    let inherits_from = if !meta.inherits_from.is_empty() {
86        meta.inherits_from.as_str()
87    } else if !meta.client_version.is_empty() && cfg.version_info.version != meta.client_version {
88        meta.client_version.as_str()
89    } else {
90        ""
91    };
92    if inherits_from.is_empty() {
93        meta.to_owned()
94    } else {
95        let meta = meta.to_owned();
96        let mut base_info = VersionInfo {
97            version: inherits_from.to_owned(),
98            version_base: cfg.version_info.version_base.to_owned(),
99            ..Default::default()
100        };
101        if base_info.load().await.is_ok() {
102            if let Some(base) = &mut base_info.meta {
103                let mut base = base.to_owned();
104                base += meta;
105                base
106            } else {
107                meta.to_owned()
108            }
109        } else {
110            meta
111        }
112    }
113}
114
115impl Client {
116    /// 根据传入的启动客户端版本设定创建一个客户端
117    ///
118    /// 这将会检查元数据,并组合出启动参数,之后可以使用 [`Client::launch`] 启动游戏
119    pub async fn new(mut cfg: ClientConfig) -> DynResult<Self> {
120        if cfg.version_info.meta.is_none() {
121            anyhow::bail!("version_info is empty");
122        }
123        // let build_args_timer = std::time::Instant::now();
124        let mut args = Vec::<String>::with_capacity(64);
125
126        // 检查是否继承版本
127        let meta = parse_inheritsed_meta(&cfg).await;
128
129        cfg.version_info.meta = Some(meta.to_owned());
130
131        // 变量集,用来给参数中 ${VAR} 做文本替换
132        let mut variables: HashMap<&'static str, String> = HashMap::with_capacity(19);
133        variables.insert("${library_directory}", {
134            // crate::path::MINECRAFT_LIBRARIES_PATH.to_string()
135            let lib_path = std::path::Path::new(&cfg.version_info.version_base);
136            let lib_path = lib_path
137                .parent()
138                .ok_or_else(|| anyhow::anyhow!("There's no parent from the library path"))?
139                .join("libraries");
140            let lib_path = get_full_path(lib_path);
141            lib_path.replace(|a| a == '/' || a == '\\', "/")
142        });
143        variables.insert("${classpath}", {
144            // 类路径,所有的 jar 库
145            // 先添加 libraries 的库,然后添加自身
146            let lib_base_path = variables
147                .get("${library_directory}")
148                .unwrap()
149                .replace('/', std::path::MAIN_SEPARATOR_STR);
150            // 使用 HashMap 以将模组加载器中的 Jar 进行覆盖
151            let mut lib_args: HashMap<String, String> =
152                HashMap::with_capacity(meta.libraries.len());
153            for lib in &meta.libraries {
154                let class_name = lib.name.as_str()
155                    [0..lib.name.rfind(':').expect("Can't parse class name")]
156                    .to_string();
157                if !lib.rules.is_allowed() {
158                    continue;
159                }
160                let lib_path = {
161                    // 处理 name
162                    let lib: Vec<&str> = lib.name.splitn(3, ':').collect();
163                    let (package, name, version) = (lib[0], lib[1], lib[2]);
164                    let package_path: Vec<&str> = package.split('.').collect();
165                    format!(
166                        "{}{sep}{}{sep}{}{sep}{}{sep}{}-{}.jar",
167                        lib_base_path,
168                        package_path.join(std::path::MAIN_SEPARATOR_STR),
169                        name,
170                        version,
171                        name,
172                        version,
173                        sep = std::path::MAIN_SEPARATOR,
174                    )
175                };
176                let mut lib_path = if let Some(ds) = &lib.downloads {
177                    if let Some(d) = &ds.artifact {
178                        // 使用 artifact.path
179                        format!(
180                            "{}{sep}{}",
181                            lib_base_path,
182                            d.path
183                                .replace(|a| a == '/' || a == '\\', std::path::MAIN_SEPARATOR_STR),
184                            sep = std::path::MAIN_SEPARATOR
185                        )
186                    } else {
187                        lib_path
188                    }
189                } else {
190                    lib_path
191                };
192                if let Some(n) = &lib.natives {
193                    if false {
194                        if let Some(native_key) = n.get(TARGET_OS) {
195                            let native_key =
196                                native_key.replace("${arch}", NATIVE_ARCH_LAZY.as_ref());
197                            let classifier = lib
198                                .downloads
199                                .as_ref()
200                                .ok_or_else(|| {
201                                    anyhow::anyhow!("No downloads struct for {}", &native_key)
202                                })?
203                                .classifiers
204                                .as_ref()
205                                .ok_or_else(|| {
206                                    anyhow::anyhow!("No classifiers struct for {}", &native_key)
207                                })?
208                                .get(&native_key)
209                                .ok_or_else(|| {
210                                    anyhow::anyhow!("No classifier struct for {}", &native_key)
211                                })?;
212                            lib_path += CLASSPATH_SEPARATOR;
213                            lib_path += &lib_base_path;
214                            lib_path.push(std::path::MAIN_SEPARATOR);
215                            lib_path += &classifier
216                                .path
217                                .replace(|a| a == '/' || a == '\\', std::path::MAIN_SEPARATOR_STR);
218                            // TODO: 解压原生库
219                        }
220                    }
221                    // 可能有原生库要添加
222                }
223                lib_args.insert(class_name, lib_path);
224            }
225
226            let lib_args: Vec<_> =
227                if meta.main_class == "cpw.mods.bootstraplauncher.BootstrapLauncher" {
228                    // 新版 Forge 不再需要引入版本文件夹下的 jar 文件了
229                    lib_args.into_iter().map(|x| x.1).collect()
230                } else {
231                    lib_args
232                        .into_iter()
233                        .map(|x| x.1)
234                        .chain(meta.main_jars.iter().map(|a| {
235                            a.replace(|a| a == '/' || a == '\\', std::path::MAIN_SEPARATOR_STR)
236                        }))
237                        .collect()
238                };
239
240            #[cfg(target_os = "windows")]
241            {
242                lib_args.join(";")
243            }
244            #[cfg(target_os = "linux")]
245            {
246                lib_args.join(":")
247            }
248            #[cfg(target_os = "macos")]
249            {
250                lib_args.join(":")
251            }
252        });
253        variables.insert("${max_memory}", format!("-Xmx{}m", cfg.max_mem));
254        variables.insert(
255            "${auth_player_name}",
256            match &cfg.auth {
257                AuthMethod::Offline { player_name, .. } => player_name.to_owned(),
258                AuthMethod::Mojang { player_name, .. } => player_name.to_owned(),
259                AuthMethod::Microsoft { player_name, .. } => player_name.to_owned(),
260                AuthMethod::AuthlibInjector { player_name, .. } => player_name.to_owned(),
261            },
262        );
263        variables.insert(
264            "${natives_directory}",
265            get_full_path(Path::new(&format!(
266                "{}{sep}{ver}{sep}natives",
267                cfg.version_info.version_base,
268                ver = cfg.version_info.version,
269                sep = std::path::MAIN_SEPARATOR,
270            ))),
271        );
272        variables.insert("${version_name}", cfg.version_info.version.to_owned());
273        variables.insert("${classpath_separator}", CLASSPATH_SEPARATOR.to_owned());
274        variables.insert("${game_directory}", get_game_directory(&cfg));
275        variables.insert("${assets_root}", {
276            let assets_path = std::path::Path::new(&cfg.version_info.version_base);
277            let assets_path = assets_path.parent().unwrap().join("assets");
278            if cfg
279                .version_info
280                .meta
281                .as_ref()
282                .map(|x| {
283                    x.asset_index
284                        .as_ref()
285                        .map(|x| &x.id == "pre-1.6")
286                        .unwrap_or_default()
287                })
288                .unwrap_or_default()
289            {
290                // 使用旧版 Minecraft 资源文件结构的版本需要将资源文件复制到 gameDir/resources 文件夹下方可正确读取游戏资源
291                let game_dir = get_game_directory(&cfg);
292                let resources_path = Path::new(&game_dir).join("resources");
293                let assets_path = assets_path.join("virtual").join("pre-1.6");
294                println!(
295                    "正在映射 {} 至 {}",
296                    assets_path.to_string_lossy(),
297                    resources_path.to_string_lossy()
298                );
299                let _ = dbg!(fs_extra::dir::copy(
300                    &assets_path,
301                    &resources_path,
302                    &fs_extra::dir::CopyOptions::new()
303                        .skip_exist(true)
304                        .content_only(true),
305                ));
306                get_full_path(assets_path)
307            } else {
308                get_full_path(assets_path)
309            }
310        });
311        variables.insert(
312            "${game_assets}",
313            variables.get("${assets_root}").unwrap().to_owned(),
314        );
315        variables.insert(
316            "${assets_index_name}",
317            if let Some(asset_index) = &meta.asset_index {
318                asset_index.id.to_owned()
319            } else {
320                String::new()
321            },
322        );
323        variables.insert("${auth_session}", "token:0".into());
324        variables.insert("${clientid}", "00000000402b5328".into());
325        variables.insert(
326            "${auth_access_token}",
327            match &cfg.auth {
328                AuthMethod::Offline { uuid, .. } => uuid.repeat(2), // 防止因为参数去重而被删除
329                AuthMethod::Mojang { access_token, .. } => access_token.to_owned_string(),
330
331                AuthMethod::Microsoft { access_token, .. } => access_token.to_owned_string(),
332                AuthMethod::AuthlibInjector { access_token, .. } => access_token.to_owned_string(),
333            },
334        );
335        variables.insert(
336            "${auth_uuid}",
337            match &cfg.auth {
338                AuthMethod::Offline { uuid, .. } => uuid.to_owned(),
339                AuthMethod::Mojang { uuid, .. } => uuid.to_owned(),
340                AuthMethod::Microsoft { uuid, .. } => uuid.to_owned(),
341                AuthMethod::AuthlibInjector { uuid, .. } => uuid.to_owned(),
342            },
343        );
344        variables.insert(
345            "${user_type}",
346            match &cfg.auth {
347                AuthMethod::Offline { .. } => "Legacy".into(),
348                AuthMethod::Mojang { .. } | AuthMethod::AuthlibInjector { .. } => "Mojang".into(),
349                AuthMethod::Microsoft { uuid: _, .. } => "msa".into(),
350            },
351        );
352        variables.insert("${version_type}", cfg.version_type.to_owned());
353        variables.insert("${user_properties}", "{}".into());
354        variables.insert("${launcher_name}", "SharpCraftLauncher".into());
355        variables.insert("${launcher_version}", "221".into());
356
357        fn replace_each(variables: &HashMap<&'static str, String>, arg: String) -> String {
358            let mut arg = arg;
359            for (k, v) in variables {
360                if arg.contains(*k) {
361                    arg = arg.replace(*k, v);
362                }
363            }
364            arg
365        }
366
367        // --- 组装参数
368        // -- JVM 参数
369        // JVM 参数
370        // Encoding
371
372        // 禁用 JNDI
373        args.push("-Dlog4j2.formatMsgNoLookups=true".into());
374
375        // 用户自定义JVM参数
376        if let Some(scl_config) = &cfg.version_info.scl_launch_config {
377            if !scl_config.jvm_args.trim().is_empty() {
378                if let Ok(jvm_args) = shell_words::split(&scl_config.jvm_args) {
379                    for arg in jvm_args {
380                        args.push(arg);
381                    }
382                } else {
383                    args.push(scl_config.jvm_args.to_owned());
384                }
385            }
386        }
387
388        if let AuthMethod::AuthlibInjector {
389            api_location,
390            server_meta,
391            ..
392        } = &cfg.auth
393        {
394            // 注入 Authlib Injector
395            let authlib_injector_path = get_full_path(format!(
396                "{}{sep}..{sep}authlib-injector.jar",
397                cfg.version_info.version_base,
398                sep = std::path::MAIN_SEPARATOR
399            ));
400            args.push(format!("-javaagent:{authlib_injector_path}={api_location}"));
401            args.push(format!(
402                "-Dauthlibinjector.yggdrasil.prefetched={server_meta}"
403            ));
404        }
405        if let Some(max_mem) = variables.get("${max_memory}") {
406            args.push(max_mem.to_owned());
407        }
408
409        if let Some(arguments) = &meta.arguments {
410            for arg in &arguments.jvm {
411                match arg {
412                    Argument::Common(arg) => args.push(replace_each(&variables, arg.to_owned())),
413                    Argument::Specify(arg) => {
414                        if arg.rules.is_allowed() {
415                            for value in arg.value.iter() {
416                                args.push(value.to_owned())
417                            }
418                        }
419                    }
420                }
421            }
422        } else {
423            // 以前的 MC 元数据不包含 JVM 参数,所以咱还得手动加
424            // Native Library Path
425            args.push(format!(
426                "-Djava.library.path={}",
427                variables.get("${natives_directory}").unwrap()
428            ));
429            // Launcher name & version
430            args.push(format!(
431                "-Dminecraft.launcher.brand={}",
432                variables.get("${launcher_name}").unwrap()
433            ));
434            args.push(format!(
435                "-Dminecraft.launcher.version={}",
436                variables.get("${launcher_version}").unwrap()
437            ));
438            // Class Path
439            args.push("-cp".into());
440            args.push(variables.get("${classpath}").unwrap().to_owned());
441        }
442
443        // 游戏主类
444        args.push(meta.main_class.to_owned());
445
446        fn dedup_argument(args: &mut Vec<String>, arg: &String) -> bool {
447            let exist_arg = args.iter().enumerate().find(|x| x.1 == arg).map(|x| x.0);
448            if let Some(exist_arg) = exist_arg {
449                args.remove(exist_arg);
450                if arg.starts_with('-') {
451                    // 将附带参数一并删除
452                    args.remove(exist_arg);
453                }
454                true
455            } else {
456                false
457            }
458        }
459
460        // 游戏参数 旧版本 使用 minecraftArgument
461        let splited = meta.minecraft_arguments.trim().split(' ');
462        let mut skip_next_dedup = false;
463        for arg in splited {
464            if !arg.is_empty() {
465                let arg = replace_each(&variables, arg.to_owned());
466                if skip_next_dedup {
467                    skip_next_dedup = false
468                } else {
469                    skip_next_dedup = dedup_argument(&mut args, &arg);
470                }
471                args.push(arg);
472            }
473        }
474
475        // 游戏参数
476        if let Some(arguments) = &meta.arguments {
477            let mut skip_next_dedup = false;
478            for arg in &arguments.game {
479                match arg {
480                    Argument::Common(arg) => {
481                        let arg = replace_each(&variables, arg.to_owned());
482                        if skip_next_dedup {
483                            skip_next_dedup = false
484                        } else {
485                            skip_next_dedup = dedup_argument(&mut args, &arg);
486                        }
487                        args.push(arg);
488                    }
489                    Argument::Specify(_) => {
490                        // TODO: 是否为试玩版,自定义窗口大小等自定义参数
491                    }
492                }
493            }
494        }
495
496        // 用户自定义游戏参数
497        if let Some(scl_config) = &cfg.version_info.scl_launch_config {
498            if !scl_config.game_args.trim().is_empty() {
499                if let Ok(game_args) = shell_words::split(&scl_config.game_args) {
500                    for arg in game_args {
501                        args.push(arg);
502                    }
503                } else {
504                    args.push(scl_config.game_args.to_owned());
505                }
506            }
507        }
508
509        let java_path = if let Some(scl_config) = &cfg.version_info.scl_launch_config {
510            if scl_config.java_path.is_empty() {
511                cfg.java_path.to_owned()
512            } else {
513                scl_config.java_path.to_owned()
514            }
515        } else {
516            cfg.java_path.to_owned()
517        };
518
519        let wrapper_path = cfg
520            .version_info
521            .scl_launch_config
522            .as_ref()
523            .map(|x| x.wrapper_path.to_owned())
524            .unwrap_or_default();
525
526        let mut cmd = if wrapper_path.is_empty() {
527            Command::new(&java_path)
528        } else {
529            let mut cmd = Command::new(&wrapper_path);
530
531            let wrapper_args = cfg
532                .version_info
533                .scl_launch_config
534                .as_ref()
535                .map(|x| x.wrapper_args.to_owned())
536                .unwrap_or_default();
537
538            if !wrapper_args.is_empty() {
539                cmd.arg(wrapper_args);
540            }
541
542            cmd.arg(&java_path);
543            cmd
544        };
545
546        cmd.args(&args);
547        cmd.current_dir(get_game_directory(&cfg));
548        #[cfg(target_os = "windows")]
549        {
550            cmd.env("APPDATA", get_game_directory(&cfg));
551        }
552        cmd.env("FORMAT_MESSAGES_PATTERN_DISABLE_LOOKUPS", "true");
553
554        args.insert(0, java_path.to_owned());
555
556        Ok(Self {
557            cmd,
558            game_dir: get_game_directory(&cfg),
559            java_path,
560            args,
561            process: None,
562        })
563    }
564
565    /// 以 Builder 模式设置启动程序的标准输入方式
566    ///
567    /// 详情请参考 [`std::process::Command::stdin`]
568    pub fn stdin(mut self, cfg: impl Into<std::process::Stdio>) -> Self {
569        self.set_stdin(cfg);
570        self
571    }
572
573    /// 以 Builder 模式设置启动程序的标准输出方式
574    ///
575    /// 详情请参考 [`std::process::Command::stdout`]
576    pub fn stdout(mut self, cfg: impl Into<std::process::Stdio>) -> Self {
577        self.set_stdout(cfg);
578        self
579    }
580
581    /// 以 Builder 模式设置启动程序的标准错误输出方式
582    ///
583    /// 详情请参考 [`std::process::Command::stderr`]
584    pub fn stderr(mut self, cfg: impl Into<std::process::Stdio>) -> Self {
585        self.set_stderr(cfg);
586        self
587    }
588
589    /// 设置启动程序的标准输入方式
590    ///
591    /// 详情请参考 [`std::process::Command::stdin`]
592    pub fn set_stdin(&mut self, cfg: impl Into<std::process::Stdio>) {
593        self.cmd.stdin(cfg);
594    }
595
596    /// 设置启动程序的标准输出方式
597    ///
598    /// 详情请参考 [`std::process::Command::stdout`]
599    pub fn set_stdout(&mut self, cfg: impl Into<std::process::Stdio>) {
600        self.cmd.stdout(cfg);
601    }
602
603    /// 设置启动程序的标准错误输出方式
604    ///
605    /// 详情请参考 [`std::process::Command::stderr`]
606    pub fn set_stderr(&mut self, cfg: impl Into<std::process::Stdio>) {
607        self.cmd.stderr(cfg);
608    }
609
610    /// 拿出参数,参数数组的第一个成员为提供的 Java 执行文件
611    pub fn take_args(self) -> Vec<String> {
612        self.args
613    }
614
615    /// 拿出 Command
616    pub fn take_cmd(self) -> Command {
617        self.cmd
618    }
619
620    /// 启动游戏,并返回进程 ID
621    pub async fn launch(&mut self) -> DynResult<u32> {
622        #[cfg(windows)]
623        {
624            use inner_future::process::windows::*;
625            self.cmd.creation_flags(0x08000000);
626        }
627        let c = match self.cmd.spawn() {
628            Ok(c) => c,
629            Err(e) => {
630                if e.kind() == std::io::ErrorKind::NotFound {
631                    anyhow::bail!("启动游戏时发生错误:找不到 Java 执行文件,请确认你的 Java 文件是否存在 {:?}", e)
632                } else {
633                    anyhow::bail!("启动游戏时发生错误 {:?}", e)
634                }
635            }
636        };
637        let pid = c.id();
638        self.process = Some(c);
639        Ok(pid)
640    }
641
642    /// 如果游戏进程还在运行,则尝试停止游戏进程
643    pub fn stop(&mut self) -> DynResult {
644        if let Some(mut p) = self.process.take() {
645            p.kill()?;
646        }
647        Ok(())
648    }
649}
650
651impl Display for Client {
652    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
653        let running = if self.process.is_some() {
654            "running"
655        } else {
656            "idle"
657        };
658        write!(f, "[MCClient {} args={:?}]", running, self.args)
659    }
660}