Merge branch 'feat/m126-loaderstr-unify-loader-kind-stable'
This commit is contained in:
commit
217c8ee19c
6 files changed
+443
-172
No files matched your search
@@ -8,8 +8,12 @@
|
||||
//!
|
||||
//! 每一行实例带一个 `loader` 对象:`{"kind": <稳定 id>, "version": <版本段>}`。它是
|
||||
//! **`version_id` 的解析结果**,解析本身不在这里——[`unlucky_core::game::InstanceLoader::parse`]
|
||||
//! 是全仓唯一的 id → 加载器入口(M122 收敛)。CLI 只做「种类 → 稳定 id」的翻译,**不重写
|
||||
//! 任何 id 匹配**:那份规则一旦两处各算一遍就会漂(M120 的「刚装的 Fabric 显示成原版」)。
|
||||
//! 是全仓唯一的 id → 加载器入口(M122 收敛)。种类 → 稳定 id 的词表也不在本 crate 的代码里:
|
||||
//! [`unlucky_core::game::LoaderKind::as_str`] 是生产代码里 id 的**源头表**(M126 收敛),这里
|
||||
//! 直接调它,**连翻译层都没有**——词表与 id 匹配一样,两处各算一遍就会漂(M120 的「刚装的
|
||||
//! Fabric 显示成原版」)。生产代码里唯一的逐字副本是 `cli::help` 里 `ull help instances` 的
|
||||
//! 词表行(会打印给用户),由本模块的 `help_vocabulary_tests::help_lists_every_stable_id`
|
||||
//! 按 `ALL` 遍历守着。
|
||||
//!
|
||||
//! ## 约定一:`loader` 恒存在,原版是 `{"kind":"vanilla","version":""}`
|
||||
//!
|
||||
@@ -44,7 +48,7 @@ use std::path::PathBuf;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use unlucky_core::game::install::instance::read_profiles as read_profiles_from_layout;
|
||||
use unlucky_core::game::{InstanceLoader, LoaderKind, ProfileList};
|
||||
use unlucky_core::game::{InstanceLoader, ProfileList};
|
||||
|
||||
use crate::context::Ctx;
|
||||
use crate::error::CliError;
|
||||
@@ -128,33 +132,45 @@ fn option_path(path: Option<&PathBuf>) -> Option<String> {
|
||||
|
||||
/// 版本 id → `loader` 对象(`{"kind": …, "version": …}`)。
|
||||
///
|
||||
/// 解析走 [`InstanceLoader::parse`](全仓唯一入口);这里**不写任何 id 匹配**,只把解析结果
|
||||
/// 翻成 JSON。原版同样给出对象(`kind = vanilla`、`version = ""`),不缺键、不为 `null`
|
||||
/// ——见模块文档的「约定一」。
|
||||
/// 解析走 [`InstanceLoader::parse`](全仓唯一入口);种类 → 稳定 id 走
|
||||
/// [`unlucky_core::game::LoaderKind::as_str`](词表的源头,M126)。这里既**不写 id
|
||||
/// 匹配**,也**不写词表**:词表的源头只在 core(副本见模块文档)。原版同样给出对象
|
||||
/// (`kind = vanilla`、`version = ""`),不缺键、不为 `null`——见模块文档的「约定一」。
|
||||
fn loader_json(version_id: &str) -> Value {
|
||||
let parsed = InstanceLoader::parse(version_id);
|
||||
json!({
|
||||
"kind": loader_kind_id(parsed.kind),
|
||||
"kind": parsed.kind.as_str(),
|
||||
"version": parsed.version,
|
||||
})
|
||||
}
|
||||
|
||||
/// 加载器种类 → 稳定 id。
|
||||
///
|
||||
/// 词表与 `--loader` 同源(`vanilla` / `fabric` / `quilt` / `forge` / `neoforge`),另加两个
|
||||
/// **只能识别、不能安装**的 `optifine` / `liteloader`。穷尽 match:core 加一支就编不过,
|
||||
/// 不会静默漏报成 `vanilla`。
|
||||
fn loader_kind_id(kind: LoaderKind) -> &'static str {
|
||||
match kind {
|
||||
LoaderKind::Vanilla => "vanilla",
|
||||
LoaderKind::Fabric => "fabric",
|
||||
LoaderKind::Quilt => "quilt",
|
||||
LoaderKind::Forge => "forge",
|
||||
LoaderKind::NeoForge => "neoforge",
|
||||
LoaderKind::OptiFine => "optifine",
|
||||
LoaderKind::LiteLoader => "liteloader",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// `ull help instances` 的词表行是**对外文档契约**,不许成为第二个说假话的地方(r2)。
|
||||
///
|
||||
/// 单列在这里而不是塞进 `instances/tests.rs`:那几个断言钉的是本模块文档承诺的词表,与命令
|
||||
/// 行为无关(`tests.rs` 的职责是「跑真入口看输出」),而且这样 help 的契约与被承诺的那段
|
||||
/// 文档就在同一个文件里。
|
||||
#[cfg(test)]
|
||||
mod help_vocabulary_tests {
|
||||
use unlucky_core::game::LoaderKind;
|
||||
|
||||
/// help 的词表必须覆盖**每一支**稳定 id(含原版与两家只继承、不安装的)。
|
||||
///
|
||||
/// 抓得住:help 的词表行被改字或删项(`neoforge` → `neo_forge`);**新增一家加载器而忘了
|
||||
/// 更新 help**——下面按 [`LoaderKind::ALL`] 遍历而不是手抄七项,所以新分支漏写必红。
|
||||
/// 抓不住:措辞与排版(只做子串断言);「kind 是识别口径不是安装口径」那段语义说明是否
|
||||
/// 仍然准确,只能靠人读。
|
||||
#[test]
|
||||
fn help_lists_every_stable_id() {
|
||||
let topic = crate::cli::help::topic("instances").expect("help instances 存在");
|
||||
for kind in LoaderKind::ALL {
|
||||
assert!(
|
||||
topic.contains(kind.as_str()),
|
||||
"help instances 的词表行漏了 `{}`:\n{topic}",
|
||||
kind.as_str()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,26 +62,49 @@ pub enum LoaderKind {
|
||||
}
|
||||
|
||||
impl LoaderKind {
|
||||
/// 解析命令行里的加载器名(大小写不敏感;`neo-forge` 是 Modrinth 的另一种拼法)。
|
||||
/// 解析命令行 / 整合包清单里的加载器名(大小写不敏感)。
|
||||
///
|
||||
/// 七个 id 的匹配**委派**给 [`crate::game::LoaderKind::parse`](词表的源头,M126),
|
||||
/// 本方法只多收一个**输入别名** `neo-forge`:那是 Modrinth 的拼法(mrpack
|
||||
/// `dependencies` 的键、CF 包里也有这种写法),**只进不出**——[`Self::as_str`] 绝不产出
|
||||
/// 它。识别得出但本启动器装不了的几家(原版 / OptiFine / LiteLoader)返回 `None`。
|
||||
#[must_use]
|
||||
pub fn parse(raw: &str) -> Option<Self> {
|
||||
match raw.trim().to_ascii_lowercase().as_str() {
|
||||
"fabric" => Some(Self::Fabric),
|
||||
"quilt" => Some(Self::Quilt),
|
||||
"neoforge" | "neo-forge" => Some(Self::NeoForge),
|
||||
"forge" => Some(Self::Forge),
|
||||
_ => None,
|
||||
let wanted = raw.trim();
|
||||
if wanted.eq_ignore_ascii_case("neo-forge") {
|
||||
return Some(Self::NeoForge);
|
||||
}
|
||||
Self::from_core(crate::game::LoaderKind::parse(wanted)?)
|
||||
}
|
||||
|
||||
/// core 的宽种类 → 本模块的四家(`None` = 识别得出但本启动器装不了)。
|
||||
///
|
||||
/// 穷尽 match:core 加一支就编不过,逼作者决定「它能不能装」。
|
||||
const fn from_core(kind: crate::game::LoaderKind) -> Option<Self> {
|
||||
match kind {
|
||||
crate::game::LoaderKind::Fabric => Some(Self::Fabric),
|
||||
crate::game::LoaderKind::Quilt => Some(Self::Quilt),
|
||||
crate::game::LoaderKind::NeoForge => Some(Self::NeoForge),
|
||||
crate::game::LoaderKind::Forge => Some(Self::Forge),
|
||||
crate::game::LoaderKind::Vanilla
|
||||
| crate::game::LoaderKind::OptiFine
|
||||
| crate::game::LoaderKind::LiteLoader => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 稳定字符串 id(进版本 id、JSON 输出与日志,**不要改名**)。
|
||||
/// 稳定字符串 id(进版本 id、JSON 输出与日志)。
|
||||
///
|
||||
/// 词表**不在本模块**:[`crate::game::LoaderKind::as_str`] 是「种类 → 对外稳定 id」的源头
|
||||
/// (M126)。这里只做**结构映射**(一个 id 字面量都没有)——安装侧拼出来的
|
||||
/// `<mc>-<kind>-<loader>` 必须能被 [`crate::game::InstanceLoader::parse`] 认回来,委派让
|
||||
/// 「两边各写一份、改一边忘了另一边」这件事物理上做不到。
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Fabric => "fabric",
|
||||
Self::Quilt => "quilt",
|
||||
Self::NeoForge => "neoforge",
|
||||
Self::Forge => "forge",
|
||||
Self::Fabric => crate::game::LoaderKind::Fabric.as_str(),
|
||||
Self::Quilt => crate::game::LoaderKind::Quilt.as_str(),
|
||||
Self::NeoForge => crate::game::LoaderKind::NeoForge.as_str(),
|
||||
Self::Forge => crate::game::LoaderKind::Forge.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ mod fabric;
|
||||
mod forge;
|
||||
mod maven;
|
||||
mod plan;
|
||||
mod vocabulary;
|
||||
|
||||
/// 裁剪过的 Fabric `/profile/json`(1.20.1 + fabric-loader 0.15.11)。
|
||||
pub(crate) const FABRIC_PROFILE: &str = r#"{
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//! 安装侧词表与 `crate::game` 词表的对接:委派有没有接错、别名有没有漏进产出。
|
||||
//!
|
||||
//! 词表本体(七个对外 id)在 `crate::game::loader`,这里只验**安装侧这一半**:
|
||||
//! [`LoaderKind::as_str`] 委派给 core,[`LoaderKind::parse`] 委派 core 的解析并只多收一个
|
||||
//! Modrinth 输入别名。为什么放在安装侧的测试目录:这一半的存在理由是「装出来的版本 id 得
|
||||
//! 能被认回来」,那正好是生成端与解析端的接缝。
|
||||
|
||||
use crate::addon::loader::LoaderKind;
|
||||
use crate::game::{InstanceLoader, LoaderKind as Wide};
|
||||
|
||||
/// `as_str` 是 core 词表的委派:四家装得上的一一对应,且绝不产出别名。
|
||||
///
|
||||
/// 抓得住:委派接错支(`Fabric` 映到 `Quilt`);有人把 `as_str` 改回手写字面量并写错;
|
||||
/// 有人让 `as_str` 产出 `neo-forge`(别名只进不出);生成端 `<mc>-<kind>-<loader>` 与解析端
|
||||
/// 脱节(装出来的 id 认不回同一家)。
|
||||
/// 抓不住:core 词表的字面值(那是 `game::loader` 的往返测试与 presenter 契约测试的职责)。
|
||||
#[test]
|
||||
fn install_side_ids_are_delegated_to_the_core_vocabulary() {
|
||||
let cases = [
|
||||
(LoaderKind::Fabric, Wide::Fabric),
|
||||
(LoaderKind::Quilt, Wide::Quilt),
|
||||
(LoaderKind::NeoForge, Wide::NeoForge),
|
||||
(LoaderKind::Forge, Wide::Forge),
|
||||
];
|
||||
for (install, wide) in cases {
|
||||
assert_eq!(
|
||||
install.as_str(),
|
||||
wide.as_str(),
|
||||
"{install:?} 的 id 没走 core 词表"
|
||||
);
|
||||
assert_ne!(install.as_str(), "neo-forge", "别名只进不出");
|
||||
assert_eq!(LoaderKind::parse(install.as_str()), Some(install), "反查");
|
||||
|
||||
let id = install.version_id("1.20.4", "0.16.5");
|
||||
let parsed = InstanceLoader::parse(&id);
|
||||
assert_eq!(parsed.kind.as_str(), install.as_str(), "`{id}` 认错了家");
|
||||
assert_eq!(parsed.version, "0.16.5", "`{id}` 的版本段");
|
||||
}
|
||||
}
|
||||
|
||||
/// 别名策略:`neo-forge` 只作为**输入**被接受(Modrinth 的拼法),产出永远是 `neoforge`。
|
||||
///
|
||||
/// 抓得住:别名被搬进词表(`as_str` 开始产出 `neo-forge`);别名只认小写;安装侧顺手接受了
|
||||
/// 本启动器装不了的几家(原版 / OptiFine / LiteLoader)。
|
||||
/// 抓不住:`neo-forge` 之外 Modrinth 以后可能再发明的拼法——没有样本可写。
|
||||
#[test]
|
||||
fn modrinth_alias_is_input_only() {
|
||||
for raw in ["neo-forge", "NEO-FORGE", " neo-forge "] {
|
||||
assert_eq!(
|
||||
LoaderKind::parse(raw),
|
||||
Some(LoaderKind::NeoForge),
|
||||
"`{raw}`"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
LoaderKind::NeoForge.as_str(),
|
||||
"neoforge",
|
||||
"产出不许带连字符"
|
||||
);
|
||||
for raw in ["vanilla", "optifine", "liteloader", "", "rift"] {
|
||||
assert_eq!(LoaderKind::parse(raw), None, "`{raw}` 不该被安装侧接受");
|
||||
}
|
||||
}
|
||||
|
||||
/// [`LoaderKind::all`] 是安装侧唯一还手写的清单(CF id 前缀匹配、CLI `--loader` 提示用),
|
||||
/// 它与 [`LoaderKind::from_core`] 认可的集合必须一致——不重抄字面量,直接对映射本身。
|
||||
///
|
||||
/// 抓得住:往 core 加一家**能装**的加载器,`from_core` 补了却忘了 `all()`(CLI 提示会漏一家、
|
||||
/// CF id 不认识它的前缀);反过来 `all()` 里多一家而 `from_core` 说装不了。
|
||||
/// 抓不住:`all()` 的顺序(那是 CLI 提示的展示序,没有对外契约意义)。
|
||||
#[test]
|
||||
fn installable_list_matches_the_from_core_mapping() {
|
||||
let mut derived: Vec<LoaderKind> = Wide::ALL
|
||||
.into_iter()
|
||||
.filter_map(LoaderKind::from_core)
|
||||
.collect();
|
||||
derived.sort_unstable();
|
||||
let mut listed = LoaderKind::all().to_vec();
|
||||
listed.sort_unstable();
|
||||
assert_eq!(
|
||||
listed, derived,
|
||||
"`LoaderKind::all()` 必须正好是 `from_core` 认可的那几家"
|
||||
);
|
||||
}
|
||||
+177
-117
@@ -1,77 +1,145 @@
|
||||
//! 版本 id → 加载器:**全仓唯一**的 id 解析入口(M122)。
|
||||
//! 版本 id → 加载器:**全仓唯一**的 id 解析入口(M122),外加「种类 → 对外稳定 id」的命名词表
|
||||
//! **源头**(M126,见 [`LoaderKind::as_str`];副本只有一处,见下)。
|
||||
//!
|
||||
//! # 为什么要有这一个模块
|
||||
//! # 为什么要这一个模块
|
||||
//!
|
||||
//! 「这个实例装的什么加载器」在 UI 上是一句话,在代码里却有两个可能的来源:版本 JSON 的
|
||||
//! `libraries[]`(权威但要多读一次盘)与**版本 id 的字符串**(磁贴/列表最省事的那条)。
|
||||
//! 只要有人按 id 猜,就必须只有**一处**在猜——两处各算一遍,迟早一边先漂,而用户看到的
|
||||
//! 是「我刚装的 Fabric 显示成原版」这种信任问题(M120 复审发现的老 bug:解析端只认
|
||||
//! `fabric-loader-…`,认不出本启动器自己生成的 `1.20.4-fabric-0.16.5`)。
|
||||
//! 「这个实例装的什么加载器」在 UI 上是一句话,代码里却有两个来源:版本 JSON 的 `libraries[]`
|
||||
//! (权威但要多读一次盘)与**版本 id 的字符串**(磁贴/列表最省事的那条)。只要有人按 id 猜就
|
||||
//! 必须只有**一处**在猜——两处各算一遍迟早一边先漂,用户看到的就是「我刚装的 Fabric 显示成
|
||||
//! 原版」(M120 复审的老 bug:解析端只认 `fabric-loader-…`,认不出自家生成的
|
||||
//! `1.20.4-fabric-0.16.5`)。所以 presenter 的实例页、CLI 的实例总览、以后任何「按 id 认加载
|
||||
//! 器」的地方一律调 [`InstanceLoader::parse`],**不许**再写一份标记匹配。生成端
|
||||
//! ([`crate::addon::loader::LoaderKind::version_id`] 拼 `<mc>-<kind>-<loader>`)与解析端的一致
|
||||
//! 性由 [`tests::own_version_ids_parse_back_to_their_loader`] 钉住(**从生成端取 id**,不手抄);
|
||||
//! OptiFine / LiteLoader 本启动器不能装(见 `presenter::host::install`),只能继承进来。
|
||||
//!
|
||||
//! 因此:presenter 的实例页、CLI 的实例总览、以后任何「按 id 认加载器」的地方,
|
||||
//! 一律调 [`InstanceLoader::parse`]。**不许**在别处再写一份标记匹配。
|
||||
//! # 命名词表只有一张表(M126)
|
||||
//!
|
||||
//! # id 的生成端(真相的另一半)
|
||||
//! 契约(0.2.0 已发布形状,**不许改名**):`vanilla` / `fabric` / `quilt` / `forge` /
|
||||
//! `neoforge` / `optifine` / `liteloader`。**生产代码里这份词表有两处,都要改**:① 下面那次
|
||||
//! `loader_kind_vocab!` 展开——枚举、`ALL`、`as_str`、`has_marker` 全从同一张表长出来,所以
|
||||
//! 「加了一支却漏进清单」物理上做不到(理由见宏的注释);② 会打印给用户看的
|
||||
//! `crates/cli/src/cli/help.rs` 里 `ull help instances` 的词表行——**逐字副本**、没有委派,
|
||||
//! 由 CLI 的 `help_lists_every_stable_id`(按 `ALL` 遍历)守着。安装侧与 presenter 的 `as_str` /
|
||||
//! `parse` 都是**委派**,CLI 的 `loader.kind` 直接调;唯一不进词表的字面量是安装侧收的输入别名
|
||||
//! `neo-forge`(Modrinth 拼法,只进不出)。
|
||||
//!
|
||||
//! 本启动器自己装的加载器,id 由 [`crate::addon::loader::LoaderKind::version_id`] 拼成
|
||||
//! `<mc>-<kind>-<loader>`:
|
||||
//! 加一家加载器要动:① `loader_kind_vocab!` 表里加一行(id + 认不认标记)→ 枚举 / `ALL` /
|
||||
//! `as_str` / `has_marker` / 解析顺序一起长出来,漏不了;② presenter 的 `to_core`·`from_core`
|
||||
//! **编不过**(逼你决定 DTO 怎么显示它),能装的话安装侧 `as_str`·`from_core` 也各差一支;
|
||||
//! ③ 契约测试**会红**:presenter 的 `loader_kind_ids_are_the_released_contract` 拿 core 的 `ALL`
|
||||
//! 与独立手写的七个 id 做**集合比较**,CLI 的 `help_lists_every_stable_id` 按 `ALL` 遍历 help
|
||||
//! 词表,`json_rows_*` 钉真二进制输出面(0.2.0 已发出去过,所以这步是确认而不是可选)。
|
||||
//!
|
||||
//! | 加载器 | 真实 id 样本 |
|
||||
//! |--------|--------------|
|
||||
//! | Fabric | `1.20.4-fabric-0.16.5` |
|
||||
//! | Quilt | `1.20.4-quilt-0.27.0`(beta 版如 `1.20.1-quilt-0.20.0-beta.9`) |
|
||||
//! | Forge | `1.20.1-forge-47.2.0` |
|
||||
//! | NeoForge | `1.21.1-neoforge-21.1.80` |
|
||||
//!
|
||||
//! 生成端与解析端的一致性由 [`tests::own_version_ids_parse_back_to_their_loader`] 钉住:
|
||||
//! 那条测试**从生成端取 id**,不是手抄字符串。OptiFine / LiteLoader 本启动器不能装
|
||||
//! (见 `presenter::host::install` 的范围说明),只能从别的启动器继承进来,口径见下。
|
||||
//! ⚠️ 分工要看准:**编译器只拦「漏 match」**,新种类在消费方用 `=> Vanilla` 糊过去编得过。
|
||||
//! ③ 那条集合断言拦的是**对外词表**,但**拦不住显示路径**:`presenter/src/host/game.rs::dto_kind`
|
||||
//! 里糊一支 `=> Vanilla` 并把契约与 help 补齐后,三个 crate 全绿而磁贴静默显示「原版」——那处
|
||||
//! 目前**没有守卫**(已知缺口,另行记账)。**改名**同理:往返测试绿,只有 ③ 会红(故意再抄)。
|
||||
//!
|
||||
//! # 解析规则(按整段前缀匹配,大小写不敏感,**最靠左的标记胜**)
|
||||
//!
|
||||
//! 1. 把 id 按 `-` 切段,从左往右找第一段,它的**前缀**是某个加载器标记
|
||||
//! (`neoforge` / `fabric` / `quilt` / `forge` / `optifine` / `liteloader`);
|
||||
//! 标记后面紧跟的 `loader` 段(`fabric-loader-…`)、或同一段里的 `_loader` 尾巴,
|
||||
//! 都算标记的一部分而不是版本段;
|
||||
//! (`neoforge` / `fabric` / `quilt` / `forge` / `optifine` / `liteloader`);紧跟的 `loader`
|
||||
//! 段(`fabric-loader-…`)或同段里的 `_loader` 尾巴算标记的一部分,不是版本段;
|
||||
//! 2. 该段里标记之后剩下的部分,加上它右边的所有段,按 `-` 拼回版本段
|
||||
//! (`_` 之类的原样保留,所以 `1.13.2-OptiFine_HD_U_E6` 的版本是 `HD_U_E6`);
|
||||
//! 3. 例外:版本段有 **≥2 段** 且**最后一段**长得像 MC 版本(`1.20.1` 这种纯数字点号)时
|
||||
//! 砍掉最后一段——那是老 HMCL 的 `fabric-loader-<loader>-<mc>` 写法。一段都不剩的
|
||||
//! `0.16.5` 不会被砍;`0.20.0-beta.9` 的末段不是数字,也不会被砍;
|
||||
//! 4. 一个标记都没找到 ⇒ 原版(版本段空串)。
|
||||
//!
|
||||
//! # 认得出、也认不出的
|
||||
//!
|
||||
//! 认得出:上面四种自家 id;老 HMCL 的 `fabric-loader-0.16.5-1.20.1` /
|
||||
//! `quilt-loader-0.24.0-1.20.1`;CF 风格的 `forge-47.2.0`;PCL 的
|
||||
//! `1.13.2-OptiFine_HD_U_E6` 与 `1.12.2-LiteLoader`(LiteLoader 的 id 里没有版本段,
|
||||
//! 拿到空串是正常的)。
|
||||
//!
|
||||
//! 认不出:把加载器信息只写进版本 JSON、id 里一点痕迹都没有的实例(例如整包导入时把
|
||||
//! 版本重命名成 `我的整合包`)——那种只能去读 `libraries[]`,不在本模块的能力范围内,
|
||||
//! 结果会诚实地返回「原版」。
|
||||
//! 3. 例外:版本段 **≥2 段**且末段像 MC 版本(`1.20.1` 这种纯数字点号)时砍掉末段——老 HMCL 的
|
||||
//! `fabric-loader-<loader>-<mc>` 写法;`0.16.5`(只剩一段)与 `0.20.0-beta.9` 不受影响;
|
||||
//! 4. 一个标记都没找到 ⇒ 原版(版本段空串)。只把加载器写进版本 JSON、id 里没痕迹的实例
|
||||
//! (整包导入时被重命名成 `我的整合包` 那种)会**诚实**返回「原版」——那不等于没装加载器。
|
||||
|
||||
/// 实例面向的加载器种类(比 [`crate::addon::loader::LoaderKind`] 宽)。
|
||||
/// 一张表生成整份词表:枚举、[`LoaderKind::ALL`]、`as_str`、`has_marker` 一起长出来。
|
||||
///
|
||||
/// 与那个类型的区别:`addon::loader::LoaderKind` 只描述**本启动器能装**的四家(装不了就
|
||||
/// 没有下载路线,放进枚举里会逼出一堆 `unreachable!`);本类型面向**识别与展示**,所以
|
||||
/// 多出「原版」这一支,以及两家只能继承进来的第三方加载器。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum LoaderKind {
|
||||
/// 原版(id 里没有任何加载器标记)
|
||||
Vanilla,
|
||||
/// Fabric
|
||||
Fabric,
|
||||
/// Quilt
|
||||
Quilt,
|
||||
/// Forge
|
||||
Forge,
|
||||
/// NeoForge
|
||||
NeoForge,
|
||||
/// OptiFine(第三方,本启动器不装,只继承)
|
||||
OptiFine,
|
||||
/// LiteLoader(第三方,本启动器不装,只继承)
|
||||
LiteLoader,
|
||||
/// 为什么非要用宏:Rust 没有「枚举有哪几支」的编译期反射,手写枚举 + 手写清单时「加了一支却
|
||||
/// 忘了进清单」既不报错也不会被测试抓住(测试同样从清单出发),新加载器被**静默**当成「认不
|
||||
/// 出」;让两者从同一张表展开,这件事在物理上不可能发生。表里的顺序就是标记的匹配优先级;
|
||||
/// 带 `marker` 的条目参与 id 识别,不带的(原版)不参与。
|
||||
macro_rules! loader_kind_vocab {
|
||||
(
|
||||
$(#[$meta:meta])*
|
||||
$vis:vis enum $name:ident {
|
||||
$(
|
||||
$(#[$variant_doc:meta])*
|
||||
$variant:ident => $id:literal $(, $marker:ident)?;
|
||||
)+
|
||||
}
|
||||
) => {
|
||||
$(#[$meta])*
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
$vis enum $name {
|
||||
$(
|
||||
$(#[$variant_doc])*
|
||||
$variant,
|
||||
)+
|
||||
}
|
||||
|
||||
impl $name {
|
||||
/// 全部种类,**顺序即标记的匹配优先级**;原版没有标记(由 `has_marker` 跳过)。
|
||||
pub const ALL: [Self; loader_kind_vocab!(@count $($variant)+)] =
|
||||
[$(Self::$variant,)+];
|
||||
|
||||
/// 对外稳定 id(`--json` 的 `loader.kind`、GUI 词表、`--loader` 的取值)。
|
||||
///
|
||||
/// 这是**生产代码里 id 的源头表**(0.2.0 已发布契约):别处要么委派到这里,要么是逐字
|
||||
/// 副本(`ull help instances` 的词表行,由 CLI 的 `help_lists_every_stable_id` 守着)。
|
||||
/// 穷尽 match:加一支就编不过。
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
$(Self::$variant => $id,)+
|
||||
}
|
||||
}
|
||||
|
||||
/// 这一支会不会从版本 id 的标记里认出来(原版不会)。穷尽 match:加一支必须表态。
|
||||
#[must_use]
|
||||
pub const fn has_marker(self) -> bool {
|
||||
match self {
|
||||
$(Self::$variant => loader_kind_vocab!(@marker $($marker)?),)+
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
(@marker marker) => { true };
|
||||
(@marker) => { false };
|
||||
(@count) => { 0 };
|
||||
(@count $head:ident $($tail:ident)*) => { 1 + loader_kind_vocab!(@count $($tail)*) };
|
||||
}
|
||||
|
||||
loader_kind_vocab! {
|
||||
/// 实例面向的加载器种类(比 [`crate::addon::loader::LoaderKind`] 宽)。
|
||||
///
|
||||
/// 那个类型只描述**本启动器能装**的四家(装不了就没有下载路线);本类型面向**识别与展示**,
|
||||
/// 多出「原版」这一支,以及两家只能继承进来的第三方加载器。
|
||||
pub enum LoaderKind {
|
||||
/// 原版(id 里没有任何加载器标记)
|
||||
Vanilla => "vanilla";
|
||||
/// NeoForge(必须排在 `Forge` 前:两者都是段前缀匹配,顺序写死能防以后宽成「包含」)
|
||||
NeoForge => "neoforge", marker;
|
||||
/// Fabric
|
||||
Fabric => "fabric", marker;
|
||||
/// Quilt
|
||||
Quilt => "quilt", marker;
|
||||
/// Forge
|
||||
Forge => "forge", marker;
|
||||
/// OptiFine(第三方,本启动器不装,只继承)
|
||||
OptiFine => "optifine", marker;
|
||||
/// LiteLoader(第三方,本启动器不装,只继承)
|
||||
LiteLoader => "liteloader", marker;
|
||||
}
|
||||
}
|
||||
|
||||
impl LoaderKind {
|
||||
/// 稳定 id → 种类([`Self::as_str`] 的反查;整串全等、大小写不敏感、不认前缀与别名)。
|
||||
///
|
||||
/// 别和 [`InstanceLoader::parse`] 搞混:那个认的是**版本 id**(切段、认前缀、给版本段)。
|
||||
/// 别名 `neo-forge` 只有安装侧收,见 [`crate::addon::loader::LoaderKind::parse`]。
|
||||
#[must_use]
|
||||
pub fn parse(raw: &str) -> Option<Self> {
|
||||
let wanted = raw.trim();
|
||||
Self::ALL
|
||||
.into_iter()
|
||||
.find(|kind| kind.as_str().eq_ignore_ascii_case(wanted))
|
||||
}
|
||||
}
|
||||
|
||||
/// 一个版本 id 解析出来的加载器:种类 + 版本段(原版为空串)。
|
||||
@@ -83,24 +151,10 @@ pub struct InstanceLoader {
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
/// 标记 → 种类,**顺序即优先级**(同一段里先命中的算数)。
|
||||
///
|
||||
/// `neoforge` 必须排在 `forge` 前面:两者都是「段前缀」匹配,`neoforge…` 段若先按
|
||||
/// `forge` 判,`neoforge` 里没有 `forge` 前缀不会误判,但把顺序写死能防以后有人把匹配
|
||||
/// 改成「包含」时踩坑。
|
||||
const MARKERS: [(LoaderKind, &str); 6] = [
|
||||
(LoaderKind::NeoForge, "neoforge"),
|
||||
(LoaderKind::Fabric, "fabric"),
|
||||
(LoaderKind::Quilt, "quilt"),
|
||||
(LoaderKind::Forge, "forge"),
|
||||
(LoaderKind::OptiFine, "optifine"),
|
||||
(LoaderKind::LiteLoader, "liteloader"),
|
||||
];
|
||||
|
||||
impl InstanceLoader {
|
||||
/// 从版本 id 解析加载器(纯字符串计算,不读盘)。
|
||||
///
|
||||
/// 规则见模块文档;认不出加载器标记时返回 [`LoaderKind::Vanilla`] + 空版本段。
|
||||
/// 规则见模块文档;认不出标记时返回 [`LoaderKind::Vanilla`] + 空版本段。
|
||||
#[must_use]
|
||||
pub fn parse(version_id: &str) -> Self {
|
||||
let segments: Vec<&str> = version_id.trim().split('-').collect();
|
||||
@@ -132,19 +186,20 @@ impl InstanceLoader {
|
||||
}
|
||||
}
|
||||
|
||||
/// 这一段的**前缀**是哪个加载器标记;命中时连「标记之后剩下的部分」一起给出。
|
||||
/// 这一段的**前缀**是哪个加载器标记;命中时连它后面剩下的部分一起给出。
|
||||
///
|
||||
/// 标记串 = [`LoaderKind::as_str`],顺序 = [`LoaderKind::ALL`](即优先级),认不认标记由
|
||||
/// [`LoaderKind::has_marker`] 表决:**解析表没有独立字面量,也没有独立清单**。
|
||||
fn marker_of(segment: &str) -> Option<(LoaderKind, &str)> {
|
||||
for (kind, marker) in MARKERS {
|
||||
let Some(rest) = strip_prefix_ascii_ci(segment, marker) else {
|
||||
continue;
|
||||
};
|
||||
return Some((kind, strip_loader_suffix(rest)));
|
||||
for kind in LoaderKind::ALL.into_iter().filter(|kind| kind.has_marker()) {
|
||||
if let Some(rest) = strip_prefix_ascii_ci(segment, kind.as_str()) {
|
||||
return Some((kind, strip_loader_suffix(rest)));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 去掉标记后面可选的 `loader` 后缀(`fabric-loader` 的版本段要从 `0.16.5` 起算,不是
|
||||
/// 从 `loader-0.16.5` 起算)。分隔符 `-` / `_` 一并吞掉。
|
||||
/// 去掉标记后面可选的 `loader` 后缀(版本段从 `0.16.5` 起算,不是 `loader-0.16.5`)。
|
||||
fn strip_loader_suffix(rest: &str) -> &str {
|
||||
let trimmed = trim_separators(rest);
|
||||
match strip_prefix_ascii_ci(trimmed, "loader") {
|
||||
@@ -177,11 +232,8 @@ fn join_version(parts: &[&str]) -> String {
|
||||
kept.join("-")
|
||||
}
|
||||
|
||||
/// 这一段长得像不像 MC 版本(`1.20` / `1.20.1` / `1.7.10`:全数字 + 至少一个点)。
|
||||
///
|
||||
/// 只用于「砍 HMCL 尾巴」那一条:它要求版本段前面还有内容(`≥2` 段),所以 `0.16.5`
|
||||
/// 这种单独的加载器版本不会被当成 MC 版本砍掉。`0.20.0-beta.9` 的末段 `beta.9` 不是纯
|
||||
/// 数字点号,也不会被砍。
|
||||
/// 这一段长得像不像 MC 版本(`1.20.1` 这种全数字 + 点号);只用于「砍 HMCL 尾巴」(要求前面
|
||||
/// 还有内容,所以 `0.16.5` 与 `0.20.0-beta.9` 都不受影响)。
|
||||
fn looks_like_mc_version(text: &str) -> bool {
|
||||
text.contains('.')
|
||||
&& text
|
||||
@@ -195,12 +247,9 @@ mod tests {
|
||||
|
||||
/// 自家 id 的真值表:**从生成端取 id**,不手抄。
|
||||
///
|
||||
/// 抓得住:生成端换了 id 形状(改了 [`crate::addon::loader::LoaderKind::version_id`]
|
||||
/// 的分隔符/顺序)而解析端没跟——这条会红;以及解析端只认老 HMCL 口径、认不出自家
|
||||
/// `-fabric-` / `-quilt-`(M122 修的那个 bug,改回旧口径这条必红)。
|
||||
/// 抓不住:加载器版本里带 `-` 时版本段被截断的**同类**情形——`0.16.5-beta.1` 与
|
||||
/// `0.20.0-beta.9` 两个样本是专门为它写的;Forge/NeoForge 的版本号在真实世界里不带
|
||||
/// `-`,所以那边没有样本,将来真出现也只能靠人补。
|
||||
/// 抓得住:生成端换了 id 形状而解析端没跟、解析端退回只认老 HMCL 口径(M122 的 bug)。
|
||||
/// 抓不住:加载器版本自己带 `-` 的**其他**情形(`0.16.5-beta.1` / `0.20.0-beta.9` 两个
|
||||
/// 样本专为它写;Forge/NeoForge 的真实版本号不带 `-`,出现了只能靠人补样本)。
|
||||
#[test]
|
||||
fn own_version_ids_parse_back_to_their_loader() {
|
||||
use crate::addon::loader::LoaderKind as InstallKind;
|
||||
@@ -238,17 +287,11 @@ mod tests {
|
||||
|
||||
/// 第三方前缀(OptiFine / LiteLoader):它们**不是** `<mc>-<kind>-<ver>` 口径。
|
||||
///
|
||||
/// 样本来源(本仓库里可查的真代码/真仓库):
|
||||
/// - PCL:`PCL/Plain Craft Launcher 2/Modules/Minecraft/ModDownload.vb:441`
|
||||
/// `Entry.Inherit & "-OptiFine_" & …` ⇒ `1.13.2-OptiFine_HD_U_E6`;
|
||||
/// - PCL:`PCL/Plain Craft Launcher 2/Pages/PageDownload/ModDownloadLib.vb:793`
|
||||
/// `DownloadInfo.Inherit & "-LiteLoader"` ⇒ `1.12.2-LiteLoader`(**没有版本段**);
|
||||
/// - `_pre1` 尾巴来自 OptiFine 自己的 preview 命名(PCL 的 `preview_OptiFine_…` jar 名)。
|
||||
///
|
||||
/// 抓得住:`optifine` / `liteloader` 的标记被当成「必须带 `-`」或「必须小写」
|
||||
/// (旧口径 `split_once("optifine")` 对 `OptiFine_…` 大小写敏感、对 `_` 不认);
|
||||
/// 抓不住:OptiFine 的 maven 坐标形式(`optifine:OptiFine:1.12.2_HD_U_C7`)——那不是
|
||||
/// 版本 id,出现在 id 里时按下面「版本段」的规则处理,未做特判。
|
||||
/// 样本来源:PCL 的 `Modules/Minecraft/ModDownload.vb:441`(`-OptiFine_…`)与
|
||||
/// `Pages/PageDownload/ModDownloadLib.vb:793`(`-LiteLoader`,**没有版本段**);`_pre1`
|
||||
/// 尾巴来自 OptiFine 自己的 preview 命名。
|
||||
/// 抓得住:标记被当成「必须带 `-`」或「必须小写」(旧口径 `split_once("optifine")`)。
|
||||
/// 抓不住:OptiFine 的 maven 坐标形式(`optifine:OptiFine:1.12.2_HD_U_C7`),未做特判。
|
||||
#[test]
|
||||
fn third_party_prefixes_are_recognized() {
|
||||
let cases = [
|
||||
@@ -269,14 +312,12 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// 老 HMCL / CF 口径(`fabric-loader-<loader>-<mc>`、`forge-<ver>`),本轮不制造新写法,
|
||||
/// 但存量实例还会按它们出现,行为不许退化。
|
||||
/// 老 HMCL / CF 口径(`fabric-loader-<loader>-<mc>`、`forge-<ver>`):存量实例还会按它们
|
||||
/// 出现,行为不许退化。
|
||||
///
|
||||
/// 抓得住:砍 MC 尾巴的规则被删(版本段会变成 `0.16.5-1.20.1`);以及 `loader` 后缀被
|
||||
/// 当成版本段的开头(会变成 `loader-0.16.5`)。
|
||||
/// 抓不住:`forge-47.2.0-1.20.1` 这类「forge 也带 MC 尾巴」的写法——见下面那行断言,
|
||||
/// 末段会被砍掉,但这是本测试**手工声明**的口径,不是从代码里查到的真实格式
|
||||
/// (本仓库的老 HMCL 分支已改成「清单 + patch」,不再生成 per-loader 版本 id)。
|
||||
/// 抓得住:砍 MC 尾巴的规则被删(版本段变成 `0.16.5-1.20.1`)、`loader` 后缀被当成版本段。
|
||||
/// 抓不住:`forge-47.2.0-1.20.1` 是**手工声明**的口径,不是查到的真实格式(本仓库的老
|
||||
/// HMCL 分支已改成「清单 + patch」)。
|
||||
#[test]
|
||||
fn foreign_loader_style_ids_still_parse() {
|
||||
let cases = [
|
||||
@@ -299,11 +340,9 @@ mod tests {
|
||||
|
||||
/// 反向锚:普通版本 id(原版、快照、预发布)**不许**被认成加载器。
|
||||
///
|
||||
/// 抓得住:解析端退化成「只要有 id 就当加载器」或标记匹配放宽成「包含子串」
|
||||
/// (`1.20.4-pre1` 之类一旦被某个标记咬住就会红——这条列出的样本都不含标记,真正的
|
||||
/// 子串误伤要另外构造,见下条断言的注释)。
|
||||
/// 抓不住:**含加载器名字样的原版 id**(例如快照 `23w45a` 没事,但假设将来出现
|
||||
/// `1.20.4-forge` 这样的原版 id,只能靠人的判断——本模块按标记优先,会认成 Forge)。
|
||||
/// 抓得住:解析端退化成「有 id 就当加载器」、匹配放宽成「包含子串」。
|
||||
/// 抓不住:**含加载器名字样的原版 id**(真出现 `1.20.4-forge` 这种原版 id 会被认成
|
||||
/// Forge——本模块按标记优先)。
|
||||
#[test]
|
||||
fn plain_version_ids_stay_vanilla() {
|
||||
for id in [
|
||||
@@ -334,4 +373,25 @@ mod tests {
|
||||
assert_eq!(parsed.kind, LoaderKind::Vanilla, "id `{id}`");
|
||||
}
|
||||
}
|
||||
|
||||
/// 往返锚:**每个对外 id 喂回解析都要认成它自己**,标记顺序也得留优先级。
|
||||
///
|
||||
/// 抓得住:标记匹配退化(段前缀变整段全等、`loader` 后缀/MC 尾巴规则被改坏)、标记串与词表
|
||||
/// 脱节、`neoforge` 被排到 `forge` 后面。抓不住:**改名**——词表与解析同源、改名自洽,本条
|
||||
/// 是绿的;抓改名是 presenter 的 `loader_kind_ids_are_the_released_contract`。
|
||||
#[test]
|
||||
fn every_stable_id_parses_back_to_its_kind() {
|
||||
for kind in LoaderKind::ALL {
|
||||
let id = kind.as_str();
|
||||
assert_eq!(LoaderKind::parse(id), Some(kind), "反查 `{id}`");
|
||||
let parsed = InstanceLoader::parse(&format!("1.20.4-{id}-0.16.5"));
|
||||
assert_eq!(parsed.kind, kind, "`{id}` 认成了 {:?}", parsed.kind);
|
||||
let expected = if kind.has_marker() { "0.16.5" } else { "" };
|
||||
assert_eq!(parsed.version, expected, "`{id}` 的版本段");
|
||||
}
|
||||
let order: Vec<LoaderKind> = LoaderKind::ALL.into_iter().collect();
|
||||
let neoforge = order.iter().position(|kind| *kind == LoaderKind::NeoForge);
|
||||
let forge = order.iter().position(|kind| *kind == LoaderKind::Forge);
|
||||
assert!(neoforge < forge, "`neoforge` 要在 `forge` 前:{order:?}");
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use unlucky_core::game::LoaderKind as CoreLoaderKind;
|
||||
|
||||
/// 实例标识:core::game 生成的稳定 id,app 视为不透明字符串。
|
||||
pub type InstanceId = String;
|
||||
|
||||
@@ -40,31 +42,58 @@ impl LoaderKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// 稳定 id(`vanilla` / `fabric` / …;GUI 与 CLI 的 `--loader` 同词表)。
|
||||
pub fn as_str(self) -> &'static str {
|
||||
/// 本 DTO 种类 → core 的宽种类。
|
||||
///
|
||||
/// **结构映射,一个 id 字面量都没有**:词表只有一个真相源
|
||||
/// ([`CoreLoaderKind::as_str`]),本 crate 写不进第二份。加一支 DTO 种类这里编不过。
|
||||
const fn to_core(self) -> CoreLoaderKind {
|
||||
match self {
|
||||
Self::Vanilla => "vanilla",
|
||||
Self::Fabric => "fabric",
|
||||
Self::Quilt => "quilt",
|
||||
Self::Forge => "forge",
|
||||
Self::NeoForge => "neoforge",
|
||||
Self::OptiFine => "optifine",
|
||||
Self::LiteLoader => "liteloader",
|
||||
Self::Vanilla => CoreLoaderKind::Vanilla,
|
||||
Self::Fabric => CoreLoaderKind::Fabric,
|
||||
Self::Quilt => CoreLoaderKind::Quilt,
|
||||
Self::Forge => CoreLoaderKind::Forge,
|
||||
Self::NeoForge => CoreLoaderKind::NeoForge,
|
||||
Self::OptiFine => CoreLoaderKind::OptiFine,
|
||||
Self::LiteLoader => CoreLoaderKind::LiteLoader,
|
||||
}
|
||||
}
|
||||
|
||||
/// core 的宽种类 → 本 DTO 种类([`Self::to_core`] 的反向,同样不含字面量)。
|
||||
///
|
||||
/// core 给 `game::LoaderKind` 加一支、而 DTO 还没跟上时,这里编不过——这正是要的:
|
||||
/// UI 不许「新加载器默默显示成原版」。
|
||||
const fn from_core(kind: CoreLoaderKind) -> Self {
|
||||
match kind {
|
||||
CoreLoaderKind::Vanilla => Self::Vanilla,
|
||||
CoreLoaderKind::Fabric => Self::Fabric,
|
||||
CoreLoaderKind::Quilt => Self::Quilt,
|
||||
CoreLoaderKind::Forge => Self::Forge,
|
||||
CoreLoaderKind::NeoForge => Self::NeoForge,
|
||||
CoreLoaderKind::OptiFine => Self::OptiFine,
|
||||
CoreLoaderKind::LiteLoader => Self::LiteLoader,
|
||||
}
|
||||
}
|
||||
|
||||
/// 稳定 id(`vanilla` / `fabric` / …;GUI 与 `--json` 同词表)。
|
||||
///
|
||||
/// **委派**给 [`CoreLoaderKind::as_str`](M126):词表的源头表在 core(0.2.0 已发布契约;
|
||||
/// 另一处逐字副本是 `ull help instances` 的词表行),这里只剩一次结构映射。
|
||||
#[must_use]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
self.to_core().as_str()
|
||||
}
|
||||
|
||||
/// 从稳定 id 解析(未知给 `None`,由调用方决定报错文案)。
|
||||
///
|
||||
/// 七个 id 的匹配委派给 [`CoreLoaderKind::parse`];本方法只额外收 `none` / 空串这两个
|
||||
/// **GUI 输入别名**(表单里「不装加载器」的写法)——它们不是对外输出的 id,也绝不参与
|
||||
/// [`Self::as_str`]。
|
||||
pub fn parse(raw: &str) -> Option<Self> {
|
||||
Some(match raw.trim().to_ascii_lowercase().as_str() {
|
||||
"vanilla" | "none" | "" => Self::Vanilla,
|
||||
"fabric" => Self::Fabric,
|
||||
"quilt" => Self::Quilt,
|
||||
"forge" => Self::Forge,
|
||||
"neoforge" => Self::NeoForge,
|
||||
"optifine" => Self::OptiFine,
|
||||
"liteloader" => Self::LiteLoader,
|
||||
_ => return None,
|
||||
})
|
||||
let wanted = raw.trim();
|
||||
if wanted.is_empty() || wanted.eq_ignore_ascii_case("none") {
|
||||
return Some(Self::Vanilla);
|
||||
}
|
||||
Some(Self::from_core(CoreLoaderKind::parse(wanted)?))
|
||||
}
|
||||
|
||||
/// 向导里的可选加载器(顺序即展示顺序)。
|
||||
@@ -252,6 +281,64 @@ mod tests {
|
||||
assert_eq!(LoaderKind::parse("unknown"), None);
|
||||
}
|
||||
|
||||
/// 对外契约:七个 id 在测试里**独立抄一遍**(0.2.0 已发布形状),并与 core 的 `ALL` 做
|
||||
/// **集合比较**——这是对外词表的守卫(与 CLI 的 help 守卫互补;显示路径
|
||||
/// `presenter::host::game::dto_kind` 目前没有守卫,见 `game::loader` 的模块文档)。
|
||||
///
|
||||
/// 为什么不是编译器管:编译期只拦得到「漏 match」,而新种类在三处消费方用最省事的
|
||||
/// `=> Vanilla` 糊过去**编得过**,那时新加载器会静默显示成原版(`game::loader` 模块文档
|
||||
/// 第 ③ 步)。真正拦住它的是下面第 ② 段的集合断言。
|
||||
///
|
||||
/// 抓得住:改词表(`neoforge` → `neo_forge`);**新增一家加载器而没主动更新这份契约**
|
||||
/// (core 的 `ALL` 多一支 ⇒ 集合不等);DTO 委派接错支(`OptiFine` 映到 `LiteLoader`);
|
||||
/// `parse` 收进不该有的别名。
|
||||
/// 抓不住:core 的解析规则(那是 `game::loader` 的
|
||||
/// `every_stable_id_parses_back_to_its_kind` 等测试的职责),以及真二进制输出面的 JSON
|
||||
/// (CLI 的 `json_rows_expose_loader_kind_and_version`)。
|
||||
#[test]
|
||||
fn loader_kind_ids_are_the_released_contract() {
|
||||
const CONTRACT: [(LoaderKind, &str); 7] = [
|
||||
(LoaderKind::Vanilla, "vanilla"),
|
||||
(LoaderKind::Fabric, "fabric"),
|
||||
(LoaderKind::Quilt, "quilt"),
|
||||
(LoaderKind::Forge, "forge"),
|
||||
(LoaderKind::NeoForge, "neoforge"),
|
||||
(LoaderKind::OptiFine, "optifine"),
|
||||
(LoaderKind::LiteLoader, "liteloader"),
|
||||
];
|
||||
|
||||
// ① DTO 这一层:每支的 id、反查、大小写。
|
||||
for (kind, id) in CONTRACT {
|
||||
assert_eq!(kind.as_str(), id, "{kind:?} 的稳定 id 变了");
|
||||
assert_eq!(LoaderKind::parse(id), Some(kind), "`{id}` 认不回来");
|
||||
let upper = id.to_ascii_uppercase();
|
||||
assert_eq!(LoaderKind::parse(&upper), Some(kind), "大写 `{upper}`");
|
||||
}
|
||||
|
||||
// ② core 的词表 ↔ 这份契约:**集合相等**才过(多一支、少一支、改一个字都红)。
|
||||
let mut actual: Vec<&str> = CoreLoaderKind::ALL
|
||||
.iter()
|
||||
.map(|kind| kind.as_str())
|
||||
.collect();
|
||||
actual.sort_unstable();
|
||||
let mut expected: Vec<&str> = CONTRACT.iter().map(|(_, id)| *id).collect();
|
||||
expected.sort_unstable();
|
||||
assert_eq!(
|
||||
actual, expected,
|
||||
"core 的 `LoaderKind::ALL` 必须逐字等于对外契约:加一家加载器时先改这里 + help"
|
||||
);
|
||||
|
||||
// GUI 输入别名(不是对外 id):不装加载器有三种写法;别名与拼法都不许混进词表。
|
||||
for alias in ["none", "", " "] {
|
||||
assert_eq!(
|
||||
LoaderKind::parse(alias),
|
||||
Some(LoaderKind::Vanilla),
|
||||
"`{alias}`"
|
||||
);
|
||||
}
|
||||
assert_eq!(LoaderKind::parse("neo-forge"), None, "别名只归安装侧");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn progress_ratio_handles_unknown_total() {
|
||||
assert_eq!(Progress { done: 1, total: 0 }.ratio(), 0.0);
|
||||
|
||||
Reference in new issue
Block a user