scl_core/version/
structs.rs

1//! 所有的启动器元数据结构都在这里
2
3use 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/// 一个针对系统的规则
25#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
26pub struct OSRule {
27    /// 系统名称,通常是直接和 [`crate::utils::TARGET_OS`] 比对即可
28    #[serde(default)]
29    pub name: String,
30    /// 系统版本号,依照平台而定
31    #[serde(default)]
32    pub version: String,
33    /// 系统架构,通常是直接和 [`crate::utils::NATIVE_ARCH_LAZY`] 比对即可
34    #[serde(default)]
35    pub arch: String,
36}
37
38/// 一个规则
39#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
40pub struct ApplyRule {
41    /// 规则的类型,通常是 `allow` 或者 `disallow`
42    pub action: String,
43    /// 规则需要满足的操作系统类型
44    #[serde(default)]
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub os: Option<OSRule>,
47    /// 规则需要满足的一些特殊情况
48    ///
49    /// 但是 SCL 并没有使用这部分来做判断
50    #[serde(default)]
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub features: Option<Map<String, bool>>,
53}
54
55/// 一个用于检查规则是否满足条件的特质
56///
57/// [`ApplyRule`] 实现了这个特质
58pub trait Allowed {
59    /// 判断当前情况是否满足该规则
60    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; // 可能会有不允许的情况,继续寻找
96                        continue;
97                    }
98                }
99            }
100            should_push
101        }
102    }
103}
104
105/// 特殊指派的参数
106#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
107pub struct SpecificalArgument {
108    /// 添加此参数需要满足的条件
109    #[serde(default)]
110    #[serde(skip_serializing_if = "Vec::is_empty")]
111    pub rules: Vec<ApplyRule>,
112    /// 需要添加的参数
113    #[serde(skip_serializing_if = "Vec::is_empty")]
114    #[serde(deserialize_with = "string_or_seq")]
115    pub value: Vec<String>,
116}
117
118/// 游戏启动的一个参数
119#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
120#[serde(untagged)]
121pub enum Argument {
122    /// 无条件的参数,必须附加
123    Common(String),
124    /// 有条件的参数,需要根据条件附加
125    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/// 游戏启动参数
143#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]
144#[serde(default)]
145pub struct Arguments {
146    /// 用于添加到主类参数后面的游戏参数
147    pub game: Vec<Argument>,
148    /// 用于添加到 Class Path 前面的 JVM 参数
149    pub jvm: Vec<Argument>,
150}
151
152/// 素材索引信息
153#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
154pub struct AssetIndex {
155    /// 素材的索引文件 ID,通常是当前的游戏版本号
156    pub id: String,
157    /// 素材的索引文件 SHA1 摘要
158    pub sha1: String,
159    /// 素材的索引文件大小,以字节为单位
160    pub size: u32,
161    /// 所有素材的总计大小,以字节为单位
162    #[serde(rename = "totalSize")]
163    pub total_size: u32,
164    /// 素材的索引文件的下载链接
165    pub url: String,
166}
167
168/// 一个下载项目结构
169#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
170pub struct DownloadItem {
171    /// 如果提供,则是下载的安装路径
172    #[serde(default)]
173    pub path: String,
174    /// 该下载项目的文件 SHA1 摘要值
175    pub sha1: String,
176    /// 该下载项目的文件大小,以字节为单位
177    pub size: usize,
178    /// 该下载项目的下载链接,如果不提供则应该都是 Maven 的仓库路径,开头加上镜像源链接下载即可
179    pub url: String,
180}
181
182/// 依赖库的下载信息结构
183#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
184pub struct LibraryDownload {
185    /// 需要下载的包,通常是 JAR 文件
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub artifact: Option<DownloadItem>,
188    /// 旧版原生库的分类下载信息
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub classifiers: Option<Map<String, DownloadItem>>,
191}
192
193/// 一个依赖库结构
194///
195/// 通过访问 [`Library::rules`] 并调用 [`Allowed::is_allowed`] 来确认此依赖是否需要被下载/添加
196#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
197pub struct Library {
198    /// 添加这个依赖库需要满足的规则
199    #[serde(default)]
200    #[serde(skip_serializing_if = "Vec::is_empty")]
201    pub rules: Vec<ApplyRule>,
202    /// 需要下载的依赖库项目
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub downloads: Option<LibraryDownload>,
205    /// 旧版本的依赖下载链接,常见于一些模组加载器里
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub url: Option<String>,
208    /// 旧版本的依赖原生库项目,新版本将直接把原生库放在了 [`Library::downloads`] 项目里直接作为 Class Path 的一部分导入了。
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub natives: Option<Map<String, String>>,
211    /// 这个依赖库的名称,通常是包名和版本号
212    pub name: String,
213}
214
215/// 日志配置文件的下载信息
216#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
217pub struct LoggingFile {
218    /// 日志配置的 ID
219    pub id: String,
220    /// 日志配置文件的 SHA1 摘要
221    pub sha1: String,
222    /// 日志配置文件的大小,以字节为单位
223    pub size: u32,
224    /// 日志配置文件的下载链接
225    pub url: String,
226}
227
228/// 日志处理方式,通常是 Log4J 的相关配置
229///
230/// 不过 SCL 并没有用这里的东西
231#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
232pub struct LoggingConfig {
233    /// 日志相关参数
234    pub argument: String,
235    /// 日志输出类型
236    #[serde(rename = "type")]
237    pub logger_type: String,
238    /// 日志配置文件的下载结构
239    pub file: LoggingFile,
240}
241
242/// SCL 的独立版本设置,此处的信息会
243#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]
244#[serde(default)]
245pub struct SCLLaunchConfig {
246    /// 最大内存,单位 MB,如不提供则为自动
247    pub max_mem: Option<usize>,
248    /// Java 运行时路径
249    pub java_path: String,
250    /// 是否使用版本独立
251    pub game_independent: bool,
252    /// 设定游戏窗口标题
253    pub window_title: String,
254    /// 额外的 JVM 参数,将会附加到 Class Path 前面
255    pub jvm_args: String,
256    /// 额外的游戏参数,将会附加到参数末尾
257    pub game_args: String,
258    /// 包装器执行文件路径,对于某些 Linux 用户有用,用于指定 Java 前的执行文件
259    pub wrapper_path: String,
260    /// 包装器执行文件参数,将会附加到包装器执行文件后
261    pub wrapper_args: String,
262}
263
264/// 版本元数据里的日志方式
265#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
266pub struct Logging {
267    /// 针对客户端的日志处理方式
268    pub client: Option<LoggingConfig>,
269}
270
271/// 新版本的 Java 版本元数据
272///
273/// 如果存在则可以根据此数据选择对应的 Java 运行时
274#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
275pub struct JavaVersion {
276    /// 该 Java 运行时版本在官方启动器中的代号
277    ///
278    /// 通过该代号可以找到官方使用的 JVM 运行时
279    pub component: String,
280    /// 该 Java 运行时的主要版本号
281    #[serde(rename = "majorVersion")]
282    pub major_version: u8,
283}
284
285/// 版本元数据
286#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
287pub struct VersionMeta {
288    /// 继承某个版本
289    #[serde(default)]
290    #[serde(rename = "inheritsFrom")]
291    pub inherits_from: String,
292    /// 1.12 以前的继承版本
293    #[serde(default)]
294    #[serde(rename = "clientVersion")]
295    pub client_version: String,
296    #[serde(default)]
297    #[serde(rename = "javaVersion")]
298    /// 新版本的 Java 版本元数据
299    ///
300    /// 如果存在则可以根据此数据选择对应的 Java 运行时
301    pub java_version: Option<JavaVersion>,
302    /// 游戏启动参数
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub arguments: Option<Arguments>,
305    /// 游戏启动参数 旧版本
306    #[serde(default)]
307    #[serde(rename = "minecraftArguments")]
308    pub minecraft_arguments: String,
309    /// 资源索引元数据
310    #[serde(rename = "assetIndex")]
311    #[serde(skip_serializing_if = "Option::is_none")]
312    pub asset_index: Option<AssetIndex>,
313    /// 该版本需要下载的主要游戏文件信息
314    #[serde(skip_serializing_if = "Option::is_none")]
315    pub downloads: Option<Map<String, DownloadItem>>,
316    /// 该版本所需的依赖库列表
317    #[serde(default)]
318    pub libraries: Vec<Library>,
319    /// 该版本元数据的日志输出配置
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub logging: Option<Logging>,
322    /// 该版本元数据的主类
323    #[serde(rename = "mainClass")]
324    pub main_class: String,
325    /// 该版本元数据的主类列表,因为有可能存在多个主类 JAR
326    #[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    /// 根据元数据判断需要的最低 Java 运行时版本
354    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/// 版本元数据结构
407///
408/// 当提供了 [`VersionInfo::version_base`] 和 [`VersionInfo::version`]
409/// 两个字段的信息后可使用 [`VersionInfo::load`] 来读取其他数据
410#[derive(Debug, Clone, Default)]
411pub struct VersionInfo {
412    /// 版本文件夹主目录
413    pub version_base: String,
414    /// 版本名称
415    pub version: String,
416    /// 读取成功之后的元数据
417    pub meta: Option<VersionMeta>,
418    /// 启动器配置
419    pub scl_launch_config: Option<SCLLaunchConfig>,
420    /// 猜测的版本类型
421    pub version_type: VersionType,
422    /// 猜测的原始 Minecraft 版本类型
423    pub minecraft_version: MinecraftVersion,
424    /// 猜测的所需最低 Java 版本,1.17 之后一般为 16+,其余的为 8+
425    pub required_java: u8,
426}
427
428impl VersionInfo {
429    /// 根据 [`VersionInfo::version_base`] 和 [`VersionInfo::version`] 读取版本元数据信息
430    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                    // 加载启动器设置
449                    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}'))?; // 去掉可能存在的 BOM
451                    self.scl_launch_config = Some(scl_config);
452                }
453                // 解析元文件,提取数据
454                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}'))?; // 去掉可能存在的 BOM
457                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    /// 删除版本文件夹,约等于删除整个版本
480    ///
481    /// 但是注意本操作不会清理 assets 文件夹和 libraries 文件夹的内容
482    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    /// 重命名版本,如果目标版本名没有已有版本则会尝试重命名版本文件夹到该名称下
491    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    /// 保存元数据和独立版本设置
519    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            // 这个不是刚需了
527            // if !jar_path.is_file() {
528            //     anyhow::bail!("版本 Jar 文件缺失")
529            // }
530            if !meta_path.is_file() {
531                anyhow::bail!("版本 JSON 元数据文件缺失")
532            } else {
533                // 解析元文件,提取数据
534                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    /// 根据元数据猜测版本的种类
577    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                    // 也有可能是 QuiltMC
584                    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                    // 我们要优先 Forge 和 Fabric
591                    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    /// 一个指向版本主目录的位置
607    ///
608    /// 如果没有开启版本独立,则会返回类似 `.minecraft` 的文件夹路径
609    ///
610    /// 如果开启版本独立,则会返回类似 `.minecraft/versions/版本名称` 的文件夹路径
611    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            // 版本独立
619            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    /// 读取该版本下的所有模组信息
632    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    /// 根据版本实际情况获取最佳的最大内存用量,单位为 MB
659    ///
660    /// 代码参考自 <https://github.com/Hex-Dragon/PCL2/blob/f1310f18fda13b79b7a6189f02df15cd8300b28d/Plain%20Craft%20Launcher%202/Pages/PageSetup/PageSetupLaunch.xaml.vb#L327>
661    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        // 预分配内存,阶段一,0 ~ T1,100%
676        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        // 预分配内存,阶段二,T1 ~ T2,80%
685        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        // 预分配内存,阶段三,T2 ~ T3,60%
693        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        // 预分配内存,阶段四,T3 ~ T3 * 2,40%
701        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    /// 读取该版本下的所有世界存档信息
712    ///
713    /// 目前只是简单的扫一遍文件夹而已
714    ///
715    /// TODO: 做准确的解析过滤
716    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    /// 读取该版本下的所有资源包信息
727    ///
728    /// 目前只是简单的扫一遍文件夹而已
729    ///
730    /// TODO: 做准确的解析过滤
731    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/// 一个世界存档的信息结构
752#[derive(Debug)]
753pub struct WorldSave {
754    // TODO
755}
756
757/// 一个资源包的信息结构
758#[derive(Debug)]
759pub struct ResourcesPack {
760    // TODO
761}
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}