1use 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
20pub const LOG4J_PATCH: &[u8] = include_bytes!("../assets/log4j-patch-agent-1.0.jar");
26
27#[derive(Debug, Clone)]
29pub struct ClientConfig {
30 pub auth: AuthMethod,
32 pub version_info: VersionInfo,
34 pub version_type: String,
36 pub custom_java_args: Vec<String>,
38 pub custom_args: Vec<String>,
40 pub java_path: String,
42 pub max_mem: u32,
44 pub recheck: bool,
46}
47
48pub struct Client {
50 pub cmd: Command,
52 pub game_dir: String,
54 pub java_path: String,
56 pub args: Vec<String>,
58 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 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 mut args = Vec::<String>::with_capacity(64);
125
126 let meta = parse_inheritsed_meta(&cfg).await;
128
129 cfg.version_info.meta = Some(meta.to_owned());
130
131 let mut variables: HashMap<&'static str, String> = HashMap::with_capacity(19);
133 variables.insert("${library_directory}", {
134 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 let lib_base_path = variables
147 .get("${library_directory}")
148 .unwrap()
149 .replace('/', std::path::MAIN_SEPARATOR_STR);
150 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 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 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 }
220 }
221 }
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 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 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), 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 args.push("-Dlog4j2.formatMsgNoLookups=true".into());
374
375 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 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 args.push(format!(
426 "-Djava.library.path={}",
427 variables.get("${natives_directory}").unwrap()
428 ));
429 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 args.push("-cp".into());
440 args.push(variables.get("${classpath}").unwrap().to_owned());
441 }
442
443 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 args.remove(exist_arg);
453 }
454 true
455 } else {
456 false
457 }
458 }
459
460 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 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 }
492 }
493 }
494 }
495
496 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 pub fn stdin(mut self, cfg: impl Into<std::process::Stdio>) -> Self {
569 self.set_stdin(cfg);
570 self
571 }
572
573 pub fn stdout(mut self, cfg: impl Into<std::process::Stdio>) -> Self {
577 self.set_stdout(cfg);
578 self
579 }
580
581 pub fn stderr(mut self, cfg: impl Into<std::process::Stdio>) -> Self {
585 self.set_stderr(cfg);
586 self
587 }
588
589 pub fn set_stdin(&mut self, cfg: impl Into<std::process::Stdio>) {
593 self.cmd.stdin(cfg);
594 }
595
596 pub fn set_stdout(&mut self, cfg: impl Into<std::process::Stdio>) {
600 self.cmd.stdout(cfg);
601 }
602
603 pub fn set_stderr(&mut self, cfg: impl Into<std::process::Stdio>) {
607 self.cmd.stderr(cfg);
608 }
609
610 pub fn take_args(self) -> Vec<String> {
612 self.args
613 }
614
615 pub fn take_cmd(self) -> Command {
617 self.cmd
618 }
619
620 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 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}