1use std::{
4 collections::BTreeMap as Map,
5 fmt,
6 marker::PhantomData,
7 path::{Path, PathBuf},
8};
9
10use inner_future::stream::StreamExt;
11use serde::{
12 de::{self, SeqAccess, Visitor},
13 Deserialize, Deserializer, Serialize,
14};
15
16use super::VersionType;
17use crate::{
18 package::PackageName,
19 prelude::*,
20 semver::MinecraftVersion,
21 utils::{get_full_path, NATIVE_ARCH_LAZY},
22};
23
24#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
26pub struct OSRule {
27 #[serde(default)]
29 pub name: String,
30 #[serde(default)]
32 pub version: String,
33 #[serde(default)]
35 pub arch: String,
36}
37
38#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
40pub struct ApplyRule {
41 pub action: String,
43 #[serde(default)]
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub os: Option<OSRule>,
47 #[serde(default)]
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub features: Option<Map<String, bool>>,
53}
54
55pub trait Allowed {
59 fn is_allowed(&self) -> bool;
61}
62
63impl Allowed for [ApplyRule] {
64 fn is_allowed(&self) -> bool {
65 if self.is_empty() {
66 true
67 } else {
68 let mut should_push = false;
69 for rule in self {
70 if rule.action == "disallow" {
71 if let Some(os) = &rule.os {
72 if !os.name.is_empty()
73 && os.name != crate::utils::TARGET_OS
74 && !os.arch.is_empty()
75 && os.arch != NATIVE_ARCH_LAZY.as_ref()
76 {
77 continue;
78 } else {
79 break;
80 }
81 } else {
82 continue;
83 }
84 } else if rule.action == "allow" {
85 if let Some(os) = &rule.os {
86 if (!os.name.is_empty() && os.name != crate::utils::TARGET_OS)
87 || (!os.arch.is_empty() && os.arch != NATIVE_ARCH_LAZY.as_ref())
88 {
89 continue;
90 } else {
91 should_push = true;
92 break;
93 }
94 } else {
95 should_push = true; continue;
97 }
98 }
99 }
100 should_push
101 }
102 }
103}
104
105#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
107pub struct SpecificalArgument {
108 #[serde(default)]
110 #[serde(skip_serializing_if = "Vec::is_empty")]
111 pub rules: Vec<ApplyRule>,
112 #[serde(skip_serializing_if = "Vec::is_empty")]
114 #[serde(deserialize_with = "string_or_seq")]
115 pub value: Vec<String>,
116}
117
118#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
120#[serde(untagged)]
121pub enum Argument {
122 Common(String),
124 Specify(SpecificalArgument),
126}
127
128#[test]
129fn argument_test() {
130 let text = serde_json::from_str::<Argument>(r#""test""#).unwrap();
131 assert_eq!(text, Argument::Common("test".into()));
132 let specify = serde_json::from_str::<Argument>(r#"{"rules":[],"value":"test"}"#).unwrap();
133 assert_eq!(
134 specify,
135 Argument::Specify(SpecificalArgument {
136 rules: vec![],
137 value: vec!["test".into()]
138 })
139 );
140}
141
142#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]
144#[serde(default)]
145pub struct Arguments {
146 pub game: Vec<Argument>,
148 pub jvm: Vec<Argument>,
150}
151
152#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
154pub struct AssetIndex {
155 pub id: String,
157 pub sha1: String,
159 pub size: u32,
161 #[serde(rename = "totalSize")]
163 pub total_size: u32,
164 pub url: String,
166}
167
168#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
170pub struct DownloadItem {
171 #[serde(default)]
173 pub path: String,
174 pub sha1: String,
176 pub size: usize,
178 pub url: String,
180}
181
182#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
184pub struct LibraryDownload {
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub artifact: Option<DownloadItem>,
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub classifiers: Option<Map<String, DownloadItem>>,
191}
192
193#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
197pub struct Library {
198 #[serde(default)]
200 #[serde(skip_serializing_if = "Vec::is_empty")]
201 pub rules: Vec<ApplyRule>,
202 #[serde(skip_serializing_if = "Option::is_none")]
204 pub downloads: Option<LibraryDownload>,
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub url: Option<String>,
208 #[serde(skip_serializing_if = "Option::is_none")]
210 pub natives: Option<Map<String, String>>,
211 pub name: String,
213}
214
215#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
217pub struct LoggingFile {
218 pub id: String,
220 pub sha1: String,
222 pub size: u32,
224 pub url: String,
226}
227
228#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
232pub struct LoggingConfig {
233 pub argument: String,
235 #[serde(rename = "type")]
237 pub logger_type: String,
238 pub file: LoggingFile,
240}
241
242#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]
244#[serde(default)]
245pub struct SCLLaunchConfig {
246 pub max_mem: Option<usize>,
248 pub java_path: String,
250 pub game_independent: bool,
252 pub window_title: String,
254 pub jvm_args: String,
256 pub game_args: String,
258 pub wrapper_path: String,
260 pub wrapper_args: String,
262}
263
264#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
266pub struct Logging {
267 pub client: Option<LoggingConfig>,
269}
270
271#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
275pub struct JavaVersion {
276 pub component: String,
280 #[serde(rename = "majorVersion")]
282 pub major_version: u8,
283}
284
285#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
287pub struct VersionMeta {
288 #[serde(default)]
290 #[serde(rename = "inheritsFrom")]
291 pub inherits_from: String,
292 #[serde(default)]
294 #[serde(rename = "clientVersion")]
295 pub client_version: String,
296 #[serde(default)]
297 #[serde(rename = "javaVersion")]
298 pub java_version: Option<JavaVersion>,
302 #[serde(skip_serializing_if = "Option::is_none")]
304 pub arguments: Option<Arguments>,
305 #[serde(default)]
307 #[serde(rename = "minecraftArguments")]
308 pub minecraft_arguments: String,
309 #[serde(rename = "assetIndex")]
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub asset_index: Option<AssetIndex>,
313 #[serde(skip_serializing_if = "Option::is_none")]
315 pub downloads: Option<Map<String, DownloadItem>>,
316 #[serde(default)]
318 pub libraries: Vec<Library>,
319 #[serde(skip_serializing_if = "Option::is_none")]
321 pub logging: Option<Logging>,
322 #[serde(rename = "mainClass")]
324 pub main_class: String,
325 #[serde(skip)]
327 pub main_jars: Vec<String>,
328}
329
330impl VersionMeta {
331 pub(crate) fn fix_libraries(&mut self) {
332 for library in &mut self.libraries {
333 if library.rules.is_allowed()
334 && library.downloads.is_none()
335 && library.natives.is_none()
336 {
337 if let Ok(p) = library.name.parse::<PackageName>() {
338 let p = p.to_maven_jar_path("");
339 library.downloads = Some(LibraryDownload {
340 artifact: Some(DownloadItem {
341 path: p,
342 sha1: "".into(),
343 size: 0,
344 url: "".into(),
345 }),
346 classifiers: None,
347 })
348 }
349 }
350 }
351 }
352
353 pub fn required_java_version(&self) -> u8 {
355 if let Some(java_version) = &self.java_version {
356 java_version.major_version
357 } else if let Some(assets) = &self.asset_index {
358 if let Ok((_, ver)) = crate::semver::parse_version(&assets.id) {
359 ver.required_java_version()
360 } else {
361 8
362 }
363 } else if !self.inherits_from.is_empty() {
364 if let Ok((_, ver)) = crate::semver::parse_version(&self.inherits_from) {
365 ver.required_java_version()
366 } else {
367 8
368 }
369 } else {
370 8
371 }
372 }
373}
374
375impl std::ops::AddAssign for VersionMeta {
376 fn add_assign(&mut self, data: VersionMeta) {
377 self.main_class = data.main_class.to_owned();
378 self.minecraft_arguments = data.minecraft_arguments;
379 self.libraries.extend_from_slice(&data.libraries);
380 self.main_jars.extend_from_slice(&data.main_jars);
381 if let Some(downloads) = &mut data.downloads.to_owned() {
382 if let Some(self_downloads) = &mut self.downloads {
383 self_downloads.append(downloads);
384 } else {
385 self.downloads = Some(downloads.to_owned());
386 }
387 }
388 if let Some(arguments) = &data.arguments {
389 if let Some(self_arguments) = &mut self.arguments {
390 for a in arguments.jvm.iter() {
391 self_arguments.jvm.push(a.to_owned());
392 }
393 for a in arguments.game.iter() {
394 self_arguments.game.push(a.to_owned());
395 }
396 } else {
397 self.arguments = Some(arguments.to_owned())
398 }
399 }
400 if let Some(logging) = &data.logging {
401 self.logging = Some(logging.to_owned());
402 }
403 }
404}
405
406#[derive(Debug, Clone, Default)]
411pub struct VersionInfo {
412 pub version_base: String,
414 pub version: String,
416 pub meta: Option<VersionMeta>,
418 pub scl_launch_config: Option<SCLLaunchConfig>,
420 pub version_type: VersionType,
422 pub minecraft_version: MinecraftVersion,
424 pub required_java: u8,
426}
427
428impl VersionInfo {
429 pub async fn load(&mut self) -> DynResult {
431 let version_base_path = Path::new(&self.version_base);
432 if version_base_path.is_dir() {
433 let jar_path = version_base_path
434 .join(&self.version)
435 .join(format!("{}.jar", &self.version));
436 let meta_path = version_base_path
437 .join(&self.version)
438 .join(format!("{}.json", &self.version));
439 let scl_config_path = version_base_path.join(&self.version).join(".scl.json");
440 if !meta_path.is_file() {
441 anyhow::bail!(
442 "该版本 {} (游戏文件夹:{}) 缺失元数据文件",
443 &self.version,
444 &self.version_base
445 )
446 } else {
447 if scl_config_path.is_file() {
448 let data = inner_future::fs::read_to_string(scl_config_path).await?;
450 let scl_config = serde_json::from_str(data.trim_start_matches('\u{feff}'))?; self.scl_launch_config = Some(scl_config);
452 }
453 let data = inner_future::fs::read_to_string(meta_path).await?;
455 let mut meta: VersionMeta =
456 serde_json::from_str(data.trim_start_matches('\u{feff}'))?; if jar_path.is_file() {
458 meta.main_jars.push(get_full_path(jar_path));
459 }
460 self.required_java = meta.required_java_version();
461 if let Some(assets) = &meta.asset_index {
462 if let Ok((_, ver)) = crate::semver::parse_version(&assets.id) {
463 self.minecraft_version = ver;
464 }
465 } else if !meta.inherits_from.is_empty() {
466 if let Ok((_, ver)) = crate::semver::parse_version(&meta.inherits_from) {
467 self.minecraft_version = ver;
468 }
469 }
470 self.meta = Some(meta);
471 self.version_type = self.guess_version_type();
472 Ok(())
473 }
474 } else {
475 anyhow::bail!("游戏文件夹 {} 不是正确的文件夹", &self.version_base)
476 }
477 }
478
479 pub async fn delete(self) {
483 let version_base_path = Path::new(&self.version_base);
484 if version_base_path.is_dir() {
485 let version_path = version_base_path.join(&self.version);
486 let _ = inner_future::fs::remove_dir_all(version_path).await;
487 }
488 }
489
490 pub async fn rename_version(&mut self, new_version_name: &str) -> DynResult {
492 let version_base_path = Path::new(&self.version_base);
493 if version_base_path.is_dir() {
494 let version_path = version_base_path.join(&self.version);
495 let version_jar_path = version_path.join(format!("{}.jar", self.version));
496 let version_json_path = version_path.join(format!("{}.json", self.version));
497 let new_version_path = version_base_path.join(new_version_name);
498 let new_version_jar_path = version_path.join(format!("{new_version_name}.jar"));
499 let new_version_json_path = version_path.join(format!("{new_version_name}.json"));
500 if new_version_path.is_dir() {
501 anyhow::bail!("目标版本名称已存在")
502 } else {
503 if version_jar_path.is_file() {
504 inner_future::fs::rename(version_jar_path, new_version_jar_path).await?;
505 }
506 if version_json_path.is_file() {
507 inner_future::fs::rename(version_json_path, new_version_json_path).await?
508 };
509 inner_future::fs::rename(version_path, new_version_path).await?;
510 self.version = new_version_name.to_owned();
511 Ok(())
512 }
513 } else {
514 anyhow::bail!("文件夹不存在")
515 }
516 }
517
518 pub async fn save(&self) -> DynResult {
520 let version_base_path = Path::new(&self.version_base);
521 if version_base_path.is_dir() {
522 let meta_path = version_base_path
523 .join(&self.version)
524 .join(format!("{}.json", &self.version));
525 let scl_config_path = version_base_path.join(&self.version).join(".scl.json");
526 if !meta_path.is_file() {
531 anyhow::bail!("版本 JSON 元数据文件缺失")
532 } else {
533 if let Some(meta) = &self.meta {
535 let file = std::fs::OpenOptions::new()
536 .truncate(true)
537 .write(true)
538 .open(meta_path);
539 if let Ok(file) = file {
540 match serde_json::to_writer(file, meta) {
541 Ok(_) => {}
542 Err(err) => {
543 anyhow::bail!("元数据解析失败:{}", err)
544 }
545 }
546 } else {
547 anyhow::bail!("无法打开版本元数据文件")
548 }
549 }
550 if let Some(scl_config) = &self.scl_launch_config {
551 let file = std::fs::OpenOptions::new()
552 .truncate(true)
553 .create(true)
554 .write(true)
555 .open(scl_config_path);
556 if let Ok(file) = file {
557 match serde_json::to_writer(file, scl_config) {
558 Ok(_) => {}
559 Err(err) => {
560 anyhow::bail!("SCL 配置文件写入失败:{}", err)
561 }
562 }
563 } else {
564 anyhow::bail!("无法打开 SCL 配置文件")
565 }
566 } else if scl_config_path.is_file() {
567 inner_future::fs::remove_file(scl_config_path).await?;
568 }
569 Ok(())
570 }
571 } else {
572 anyhow::bail!("版本文件夹未找到")
573 }
574 }
575
576 pub fn guess_version_type(&self) -> VersionType {
578 let mut has_optifine = false;
579 let mut has_fabric = false;
580 if let Some(meta) = &self.meta {
581 for lib in &meta.libraries {
582 if lib.name.starts_with("net.fabricmc:") {
583 has_fabric = true;
585 } else if lib.name.starts_with("net.minecraftforge:") {
586 return VersionType::Forge;
587 } else if lib.name.starts_with("org.quiltmc:") {
588 return VersionType::QuiltMC;
589 } else if lib.name.starts_with("optifine:") {
590 has_optifine = true;
592 }
593 }
594 if has_fabric {
595 VersionType::Fabric
596 } else if has_optifine {
597 VersionType::Optifine
598 } else {
599 VersionType::Vanilla
600 }
601 } else {
602 VersionType::Unknown
603 }
604 }
605
606 pub fn version_path(&self) -> PathBuf {
612 if self
613 .scl_launch_config
614 .as_ref()
615 .map(|x| x.game_independent)
616 .unwrap_or(false)
617 {
618 let mut result = PathBuf::new();
620 result.push(&self.version_base);
621 result.push(&self.version);
622 result
623 } else {
624 let mut result = PathBuf::new();
625 result.push(&self.version_base);
626 result.pop();
627 result
628 }
629 }
630
631 pub async fn get_mods(&self) -> DynResult<Vec<super::mods::Mod>> {
633 let mods_path = self.version_path().join("mods");
634 if !mods_path.is_dir() {
635 return Ok(vec![]);
636 }
637 let mut files = inner_future::fs::read_dir(mods_path).await?;
638 let mut results = vec![];
639 while let Some(file) = files.try_next().await? {
640 if file.path().is_file()
641 && file
642 .path()
643 .file_name()
644 .map(|x| x.to_string_lossy().ends_with(".jar"))
645 == Some(true)
646 || file
647 .path()
648 .file_name()
649 .map(|x| x.to_string_lossy().ends_with(".jar.disabled"))
650 == Some(true)
651 {
652 results.push(super::mods::Mod::from_path(file.path()).await?);
653 }
654 }
655 Ok(results)
656 }
657
658 pub async fn get_automated_maxium_memory(&self) -> u64 {
662 let mem_status = crate::utils::get_mem_status();
663 let mut free = dbg!(mem_status.free as i64);
664 let mods = self.get_mods().await.unwrap_or_default();
665 let (mem_min, mem_t1, mem_t2, mem_t3) = if !mods.is_empty() {
666 (
667 400 + mods.len() as i64 * 7,
668 1500 + mods.len() as i64 * 10,
669 3000 + mods.len() as i64 * 17,
670 6000 + mods.len() as i64 * 34,
671 )
672 } else {
673 (300, 1500, 2500, 4000)
674 };
675 let mut result = 0;
677 let mem_delta = mem_t1;
678 free = (free - 100).max(0);
679 result += free.min(mem_delta);
680 free -= mem_delta + 100;
681 if free < 100 {
682 return result.max(mem_min) as _;
683 }
684 let mem_delta = mem_t2 - mem_t1;
686 free = (free - 100).max(0);
687 result += ((free as f64 * 0.8) as i64).min(mem_delta);
688 free -= ((mem_delta as f64 / 0.8) as i64) + 100;
689 if free < 100 {
690 return result.max(mem_min) as _;
691 }
692 let mem_delta = mem_t2 - mem_t1;
694 free = (free - 200).max(0);
695 result += ((free as f64 * 0.6) as i64).min(mem_delta);
696 free -= ((mem_delta as f64 / 0.6) as i64) + 200;
697 if free < 100 {
698 return result.max(mem_min) as _;
699 }
700 let mem_delta = mem_t3;
702 free = (free - 300).max(0);
703 result += ((free as f64 * 0.4) as i64).min(mem_delta);
704 free -= ((mem_delta as f64 / 0.4) as i64) + 300;
705 if free < 100 {
706 return result.max(mem_min) as _;
707 }
708 result.max(mem_min) as _
709 }
710
711 pub async fn get_saves(&self) -> DynResult<Vec<WorldSave>> {
717 let saves_path = self.version_path().join("saves");
718 let mut result = Vec::new();
719 let mut files = inner_future::fs::read_dir(&saves_path).await?;
720 while let Some(_file) = files.try_next().await? {
721 result.push(WorldSave {})
722 }
723 Ok(result)
724 }
725
726 pub async fn get_resources_packs(&self) -> DynResult<Vec<ResourcesPack>> {
732 let resourcepacks_path = self.version_path().join("resourcepacks");
733 let texturepacks_path = self.version_path().join("texturepacks");
734 let mut result = Vec::new();
735 if resourcepacks_path.is_dir() {
736 let mut files = inner_future::fs::read_dir(&resourcepacks_path).await?;
737 while let Some(_file) = files.try_next().await? {
738 result.push(ResourcesPack {})
739 }
740 }
741 if texturepacks_path.is_dir() {
742 let mut files = inner_future::fs::read_dir(&texturepacks_path).await?;
743 while let Some(_file) = files.try_next().await? {
744 result.push(ResourcesPack {})
745 }
746 }
747 Ok(result)
748 }
749}
750
751#[derive(Debug)]
753pub struct WorldSave {
754 }
756
757#[derive(Debug)]
759pub struct ResourcesPack {
760 }
762
763fn string_or_seq<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
764where
765 D: Deserializer<'de>,
766{
767 struct StringOrVec(PhantomData<Vec<String>>);
768
769 impl<'de> Visitor<'de> for StringOrVec {
770 type Value = Vec<String>;
771
772 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
773 formatter.write_str("string or list of strings")
774 }
775
776 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
777 where
778 E: de::Error,
779 {
780 Ok(vec![value.into()])
781 }
782
783 fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
784 where
785 S: SeqAccess<'de>,
786 {
787 Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq))
788 }
789 }
790
791 deserializer.deserialize_any(StringOrVec(PhantomData))
792}