fix(core): M122 版本 id → 加载器只留一份解析,并认全自家 id 口径
`presenter::host::game::loader_from_id` 只认 `fabric-loader-…` / `quilt-loader-…`, 而本启动器自己生成的 id 是 `<mc>-<kind>-<loader>`(`1.20.4-fabric-0.16.5` / `1.20.4-quilt-0.27.0`),于是刚装好的 Fabric / Quilt 实例在实例页显示成「原版」。 - 新增 `core::game::loader`:`InstanceLoader::parse` 是全仓唯一的 id 解析入口 (按 `-` 切段、段前缀匹配标记、跳过独立的 `loader` 段、砍老 HMCL 的 MC 尾巴)。 覆盖自家四种口径(生成端 `addon::loader::LoaderKind::version_id`)、老 HMCL / CF 口径、OptiFine / LiteLoader 第三方前缀;标记按整段前缀匹配,不再用「包含子串」。 - presenter 只留 DTO 翻译:`dto_kind` 穷尽 match(`dto/` 里仍不许出现 core 类型)。 - 测试:真值表**从生成端取 id**,反向锚(普通版本 id 不被认成加载器),映射单射, 以及端到端快照(`instances()` 的磁贴数据对四个加载器各自的版本号与徽标)。
This commit is contained in:
1 parent
829bd9043c
commit
081ce65596
3 files changed
+505
-36
No files matched your search
@@ -0,0 +1,337 @@
|
||||
//! 版本 id → 加载器:**全仓唯一**的 id 解析入口(M122)。
|
||||
//!
|
||||
//! # 为什么要有这一个模块
|
||||
//!
|
||||
//! 「这个实例装的什么加载器」在 UI 上是一句话,在代码里却有两个可能的来源:版本 JSON 的
|
||||
//! `libraries[]`(权威但要多读一次盘)与**版本 id 的字符串**(磁贴/列表最省事的那条)。
|
||||
//! 只要有人按 id 猜,就必须只有**一处**在猜——两处各算一遍,迟早一边先漂,而用户看到的
|
||||
//! 是「我刚装的 Fabric 显示成原版」这种信任问题(M120 复审发现的老 bug:解析端只认
|
||||
//! `fabric-loader-…`,认不出本启动器自己生成的 `1.20.4-fabric-0.16.5`)。
|
||||
//!
|
||||
//! 因此:presenter 的实例页、CLI 的实例总览、以后任何「按 id 认加载器」的地方,
|
||||
//! 一律调 [`InstanceLoader::parse`]。**不许**在别处再写一份标记匹配。
|
||||
//!
|
||||
//! # id 的生成端(真相的另一半)
|
||||
//!
|
||||
//! 本启动器自己装的加载器,id 由 [`crate::addon::loader::LoaderKind::version_id`] 拼成
|
||||
//! `<mc>-<kind>-<loader>`:
|
||||
//!
|
||||
//! | 加载器 | 真实 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` 的范围说明),只能从别的启动器继承进来,口径见下。
|
||||
//!
|
||||
//! # 解析规则(按整段前缀匹配,大小写不敏感,**最靠左的标记胜**)
|
||||
//!
|
||||
//! 1. 把 id 按 `-` 切段,从左往右找第一段,它的**前缀**是某个加载器标记
|
||||
//! (`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[]`,不在本模块的能力范围内,
|
||||
//! 结果会诚实地返回「原版」。
|
||||
|
||||
/// 实例面向的加载器种类(比 [`crate::addon::loader::LoaderKind`] 宽)。
|
||||
///
|
||||
/// 与那个类型的区别:`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,
|
||||
}
|
||||
|
||||
/// 一个版本 id 解析出来的加载器:种类 + 版本段(原版为空串)。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InstanceLoader {
|
||||
/// 加载器种类
|
||||
pub kind: LoaderKind,
|
||||
/// 加载器版本段(id 里标记之后的那一截);原版或 id 里没写版本时为空串
|
||||
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`] + 空版本段。
|
||||
#[must_use]
|
||||
pub fn parse(version_id: &str) -> Self {
|
||||
let segments: Vec<&str> = version_id.trim().split('-').collect();
|
||||
for (index, segment) in segments.iter().enumerate() {
|
||||
let Some((kind, rest)) = marker_of(segment) else {
|
||||
continue;
|
||||
};
|
||||
// `fabric-loader-0.16.5-1.20.1` 按 `-` 切完是 `["fabric", "loader", …]`,
|
||||
// `loader` 自己占一段;版本段要从它后面算,否则会读出 `loader-0.16.5`。
|
||||
let mut next = index + 1;
|
||||
while segments
|
||||
.get(next)
|
||||
.is_some_and(|segment| segment.eq_ignore_ascii_case("loader"))
|
||||
{
|
||||
next += 1;
|
||||
}
|
||||
let mut parts: Vec<&str> = Vec::new();
|
||||
if !rest.is_empty() {
|
||||
parts.push(rest);
|
||||
}
|
||||
parts.extend(segments.iter().skip(next).copied());
|
||||
let version = join_version(&parts);
|
||||
return Self { kind, version };
|
||||
}
|
||||
Self {
|
||||
kind: LoaderKind::Vanilla,
|
||||
version: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 这一段的**前缀**是哪个加载器标记;命中时连「标记之后剩下的部分」一起给出。
|
||||
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)));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 去掉标记后面可选的 `loader` 后缀(`fabric-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") {
|
||||
Some(after) => trim_separators(after),
|
||||
None => trimmed,
|
||||
}
|
||||
}
|
||||
|
||||
/// 掐头去尾的 `-` / `_`(`OptiFine_HD_U_E6` 的标记后面紧跟的是 `_`)。
|
||||
fn trim_separators(text: &str) -> &str {
|
||||
text.trim_matches(['-', '_'])
|
||||
}
|
||||
|
||||
/// 大小写不敏感的 `strip_prefix`(用的是 ASCII 折叠,版本 id 里不会出现非 ASCII 标记)。
|
||||
fn strip_prefix_ascii_ci<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
|
||||
let head = text.get(..prefix.len())?;
|
||||
if head.eq_ignore_ascii_case(prefix) {
|
||||
text.get(prefix.len()..)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 把标记右边剩下的段拼成版本段,并按「老 HMCL 的 `<loader>-<mc>` 尾巴」规则砍掉 MC 版本。
|
||||
fn join_version(parts: &[&str]) -> String {
|
||||
let mut kept = parts.to_vec();
|
||||
if kept.len() >= 2 && kept.last().is_some_and(|last| looks_like_mc_version(last)) {
|
||||
kept.pop();
|
||||
}
|
||||
kept.join("-")
|
||||
}
|
||||
|
||||
/// 这一段长得像不像 MC 版本(`1.20` / `1.20.1` / `1.7.10`:全数字 + 至少一个点)。
|
||||
///
|
||||
/// 只用于「砍 HMCL 尾巴」那一条:它要求版本段前面还有内容(`≥2` 段),所以 `0.16.5`
|
||||
/// 这种单独的加载器版本不会被当成 MC 版本砍掉。`0.20.0-beta.9` 的末段 `beta.9` 不是纯
|
||||
/// 数字点号,也不会被砍。
|
||||
fn looks_like_mc_version(text: &str) -> bool {
|
||||
text.contains('.')
|
||||
&& text
|
||||
.split('.')
|
||||
.all(|part| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 自家 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 的版本号在真实世界里不带
|
||||
/// `-`,所以那边没有样本,将来真出现也只能靠人补。
|
||||
#[test]
|
||||
fn own_version_ids_parse_back_to_their_loader() {
|
||||
use crate::addon::loader::LoaderKind as InstallKind;
|
||||
|
||||
let cases: [(InstallKind, LoaderKind, &str, &str); 6] = [
|
||||
(InstallKind::Fabric, LoaderKind::Fabric, "1.20.4", "0.16.5"),
|
||||
(
|
||||
InstallKind::Fabric,
|
||||
LoaderKind::Fabric,
|
||||
"1.20.4",
|
||||
"0.16.5-beta.1",
|
||||
),
|
||||
(InstallKind::Quilt, LoaderKind::Quilt, "1.20.4", "0.27.0"),
|
||||
(
|
||||
InstallKind::Quilt,
|
||||
LoaderKind::Quilt,
|
||||
"1.20.1",
|
||||
"0.20.0-beta.9",
|
||||
),
|
||||
(InstallKind::Forge, LoaderKind::Forge, "1.20.1", "47.2.0"),
|
||||
(
|
||||
InstallKind::NeoForge,
|
||||
LoaderKind::NeoForge,
|
||||
"1.21.1",
|
||||
"21.1.80",
|
||||
),
|
||||
];
|
||||
for (install, kind, mc, loader_version) in cases {
|
||||
let id = install.version_id(mc, loader_version);
|
||||
let parsed = InstanceLoader::parse(&id);
|
||||
assert_eq!(parsed.kind, kind, "id `{id}` 认成了 {:?}", parsed.kind);
|
||||
assert_eq!(parsed.version, loader_version, "id `{id}` 的版本段");
|
||||
}
|
||||
}
|
||||
|
||||
/// 第三方前缀(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 里时按下面「版本段」的规则处理,未做特判。
|
||||
#[test]
|
||||
fn third_party_prefixes_are_recognized() {
|
||||
let cases = [
|
||||
("1.13.2-OptiFine_HD_U_E6", LoaderKind::OptiFine, "HD_U_E6"),
|
||||
("1.20.1-OptiFine_HD_U_I6", LoaderKind::OptiFine, "HD_U_I6"),
|
||||
(
|
||||
"1.20.1-OptiFine_HD_U_I6_pre1",
|
||||
LoaderKind::OptiFine,
|
||||
"HD_U_I6_pre1",
|
||||
),
|
||||
("1.20.1-OptiFine", LoaderKind::OptiFine, ""),
|
||||
("1.12.2-LiteLoader", LoaderKind::LiteLoader, ""),
|
||||
];
|
||||
for (id, kind, version) in cases {
|
||||
let parsed = InstanceLoader::parse(id);
|
||||
assert_eq!(parsed.kind, kind, "id `{id}` 认成了 {:?}", parsed.kind);
|
||||
assert_eq!(parsed.version, version, "id `{id}` 的版本段");
|
||||
}
|
||||
}
|
||||
|
||||
/// 老 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)。
|
||||
#[test]
|
||||
fn foreign_loader_style_ids_still_parse() {
|
||||
let cases = [
|
||||
("fabric-loader-0.16.5-1.20.1", LoaderKind::Fabric, "0.16.5"),
|
||||
("quilt-loader-0.24.0-1.20.1", LoaderKind::Quilt, "0.24.0"),
|
||||
(
|
||||
"quilt-loader-0.24.0-beta.9-1.20.1",
|
||||
LoaderKind::Quilt,
|
||||
"0.24.0-beta.9",
|
||||
),
|
||||
("forge-47.2.0", LoaderKind::Forge, "47.2.0"),
|
||||
("forge-47.2.0-1.20.1", LoaderKind::Forge, "47.2.0"),
|
||||
];
|
||||
for (id, kind, version) in cases {
|
||||
let parsed = InstanceLoader::parse(id);
|
||||
assert_eq!(parsed.kind, kind, "id `{id}` 认成了 {:?}", parsed.kind);
|
||||
assert_eq!(parsed.version, version, "id `{id}` 的版本段");
|
||||
}
|
||||
}
|
||||
|
||||
/// 反向锚:普通版本 id(原版、快照、预发布)**不许**被认成加载器。
|
||||
///
|
||||
/// 抓得住:解析端退化成「只要有 id 就当加载器」或标记匹配放宽成「包含子串」
|
||||
/// (`1.20.4-pre1` 之类一旦被某个标记咬住就会红——这条列出的样本都不含标记,真正的
|
||||
/// 子串误伤要另外构造,见下条断言的注释)。
|
||||
/// 抓不住:**含加载器名字样的原版 id**(例如快照 `23w45a` 没事,但假设将来出现
|
||||
/// `1.20.4-forge` 这样的原版 id,只能靠人的判断——本模块按标记优先,会认成 Forge)。
|
||||
#[test]
|
||||
fn plain_version_ids_stay_vanilla() {
|
||||
for id in [
|
||||
"1.20.4",
|
||||
"1.20",
|
||||
"1.7.10",
|
||||
"23w45a",
|
||||
"1.20.4-rc1",
|
||||
"1.20.1-pre1",
|
||||
"",
|
||||
" ",
|
||||
] {
|
||||
let parsed = InstanceLoader::parse(id);
|
||||
assert_eq!(parsed.kind, LoaderKind::Vanilla, "id `{id}`");
|
||||
assert!(parsed.version.is_empty(), "id `{id}` 的版本段应为空");
|
||||
}
|
||||
}
|
||||
|
||||
/// 标记按**整段前缀**匹配,不是「包含」:段里出现加载器名字样但不在段首的,不算。
|
||||
///
|
||||
/// 抓得住:匹配规则从「段前缀」放宽成「段内包含」(`myforge-pack` 会被认成 Forge)。
|
||||
/// 抓不住:段首就是标记的误伤(`forgepack-1.0` 会认成 Forge)——这条有意不拦,因为
|
||||
/// 段首命中正是 OptiFine 的 `OptiFine_HD_U_E6` 与 CF 的 `forge-47.2.0` 依赖的形状。
|
||||
#[test]
|
||||
fn markers_only_match_at_segment_start() {
|
||||
for id in ["1.20.1-myforge-pack", "1.20.1-somequiltish"] {
|
||||
let parsed = InstanceLoader::parse(id);
|
||||
assert_eq!(parsed.kind, LoaderKind::Vanilla, "id `{id}`");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
//! 外加内嵌补充清单 [`unlisted`];
|
||||
//! - 版本 JSON 与继承:[`VersionJson`]、[`resolve`]、[`merge`]、[`apply_patches`];
|
||||
//! - 参数与库:[`Arguments`] / [`Environment`] / [`Rule`]、[`Library`] / [`MavenCoordinate`];
|
||||
//! - 版本 id → 加载器:[`loader`]([`InstanceLoader::parse`],全仓唯一解析入口);
|
||||
//! - 能力位:[`capability`](版本 JSON 库表 → `has_vulkan_backend`);
|
||||
//! - 实例:目录布局 [`GameLayout`]、档案 [`ProfileList`] / [`InstanceProfile`] / [`ProfileId`];
|
||||
//! - 发布:快照仓库 [`Store`] / [`Snapshot`] / [`Draft`]。
|
||||
@@ -35,6 +36,7 @@ pub mod isolation;
|
||||
pub mod java;
|
||||
pub mod layout;
|
||||
pub mod library;
|
||||
pub mod loader;
|
||||
pub mod manifest;
|
||||
pub mod profiles;
|
||||
pub mod snapshot;
|
||||
@@ -55,6 +57,7 @@ pub use isolation::{
|
||||
};
|
||||
pub use layout::GameLayout;
|
||||
pub use library::{Library, MavenCoordinate};
|
||||
pub use loader::{InstanceLoader, LoaderKind};
|
||||
pub use manifest::{AssetIndexRef, DownloadInfo, Downloads, JavaVersion, Logging, VersionJson};
|
||||
pub use profiles::{InstanceProfile, ProfileId, ProfileList, PROFILE_SCHEMA_VERSION};
|
||||
pub use snapshot::{Draft, Snapshot, Store};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! 实例快照:`profiles.json` + 版本 JSON + 会话索引 + 实例设置 → [`InstanceSummary`]。
|
||||
//!
|
||||
//! 「磁贴上的每个字」都在这里翻译完:版本号取自继承链根、加载器取自版本 id 前缀、
|
||||
//! 最后游玩时间取自会话索引、隔离档位取自实例设置(缺省跟全局 `instances.mode`)。
|
||||
//! 「磁贴上的每个字」都在这里翻译完:版本号取自继承链根、加载器取自版本 id(解析在
|
||||
//! [`unlucky_core::game::loader`],本文件只做 DTO 翻译)、最后游玩时间取自会话索引、
|
||||
//! 隔离档位取自实例设置(缺省跟全局 `instances.mode`)。
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
@@ -72,31 +73,31 @@ pub fn describe_version(env: &HostEnv, version_id: &str) -> (String, LoaderInfo)
|
||||
(current, loader)
|
||||
}
|
||||
|
||||
/// 版本 id → 加载器(HMCL 同款命名口径:`1.20.1-forge-47.2.0` / `fabric-loader-0.16.5-1.20.1`)。
|
||||
/// 版本 id → 加载器。
|
||||
///
|
||||
/// 解析本身**不在这里**:真相源是 [`unlucky_core::game::loader`](生成端
|
||||
/// `addon::loader::LoaderKind::version_id` 拼的是 `<mc>-<kind>-<loader>`,如
|
||||
/// `1.20.4-fabric-0.16.5`)。本函数只做两件事:调 core 的解析、把 core 的种类翻成 DTO
|
||||
/// 种类(`dto` 里不许出现 core 类型,翻译只能发生在 host 这条边界上)。
|
||||
pub fn loader_from_id(version_id: &str) -> LoaderInfo {
|
||||
let lower = version_id.to_ascii_lowercase();
|
||||
let grab = |marker: &str| -> String {
|
||||
lower
|
||||
.split_once(marker)
|
||||
.map(|(_, rest)| rest.trim_start_matches('-').to_string())
|
||||
.unwrap_or_default()
|
||||
};
|
||||
let (kind, version) = if lower.contains("neoforge") {
|
||||
(LoaderKind::NeoForge, grab("neoforge-"))
|
||||
} else if lower.contains("fabric-loader") {
|
||||
(LoaderKind::Fabric, grab("fabric-loader-"))
|
||||
} else if lower.contains("quilt-loader") {
|
||||
(LoaderKind::Quilt, grab("quilt-loader-"))
|
||||
} else if lower.contains("forge") {
|
||||
(LoaderKind::Forge, grab("forge-"))
|
||||
} else if lower.contains("optifine") {
|
||||
(LoaderKind::OptiFine, grab("optifine"))
|
||||
} else if lower.contains("liteloader") {
|
||||
(LoaderKind::LiteLoader, grab("liteloader-"))
|
||||
} else {
|
||||
(LoaderKind::Vanilla, String::new())
|
||||
};
|
||||
LoaderInfo { kind, version }
|
||||
let parsed = unlucky_core::game::InstanceLoader::parse(version_id);
|
||||
LoaderInfo {
|
||||
kind: dto_kind(parsed.kind),
|
||||
version: parsed.version,
|
||||
}
|
||||
}
|
||||
|
||||
/// core 的加载器种类 → DTO 种类(穷尽 match:core 加一支,这里就编不过)。
|
||||
fn dto_kind(kind: unlucky_core::game::LoaderKind) -> LoaderKind {
|
||||
match kind {
|
||||
unlucky_core::game::LoaderKind::Vanilla => LoaderKind::Vanilla,
|
||||
unlucky_core::game::LoaderKind::Fabric => LoaderKind::Fabric,
|
||||
unlucky_core::game::LoaderKind::Quilt => LoaderKind::Quilt,
|
||||
unlucky_core::game::LoaderKind::Forge => LoaderKind::Forge,
|
||||
unlucky_core::game::LoaderKind::NeoForge => LoaderKind::NeoForge,
|
||||
unlucky_core::game::LoaderKind::OptiFine => LoaderKind::OptiFine,
|
||||
unlucky_core::game::LoaderKind::LiteLoader => LoaderKind::LiteLoader,
|
||||
}
|
||||
}
|
||||
|
||||
/// 实例的隔离档位(实例文件优先,缺省显示全局档位)。
|
||||
@@ -200,22 +201,29 @@ pub fn short_path(path: &Path) -> String {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 版本 id → DTO 加载器的接口面(解析在 core,这里只验翻译没走样)。
|
||||
///
|
||||
/// 抓得住:`dto_kind` 把某个种类翻错/翻塌(例如 Quilt 写成 Forge),以及 core 解析端
|
||||
/// 退回老口径(`fabric-loader-…` 才认、自家 `1.20.4-fabric-0.16.5` 认不出)——后两条
|
||||
/// 正是 M122 的 bug 面。
|
||||
/// 抓不住:core 解析规则本身(真值表在 `unlucky_core::game::loader` 的测试里,那边才是
|
||||
/// 真相源);也抓不住「版本号为空时不显示品牌名」那条(在 `LoaderInfo::badge` 与
|
||||
/// app-iced 的 `loader_badge`,M120 已修,别在这里重复)。
|
||||
#[test]
|
||||
fn loader_parsing_covers_all_shapes() {
|
||||
let cases = [
|
||||
("1.20.4", LoaderKind::Vanilla, ""),
|
||||
(
|
||||
"fabric-loader-0.16.5-1.20.1",
|
||||
LoaderKind::Fabric,
|
||||
"0.16.5-1.20.1",
|
||||
),
|
||||
(
|
||||
"quilt-loader-0.24.0-1.20.1",
|
||||
LoaderKind::Quilt,
|
||||
"0.24.0-1.20.1",
|
||||
),
|
||||
// 自家口径(生成端 `LoaderKind::version_id`):`<mc>-<kind>-<loader>`
|
||||
("1.20.4-fabric-0.16.5", LoaderKind::Fabric, "0.16.5"),
|
||||
("1.20.4-quilt-0.27.0", LoaderKind::Quilt, "0.27.0"),
|
||||
("1.20.1-forge-47.2.0", LoaderKind::Forge, "47.2.0"),
|
||||
("1.21.1-neoforge-21.1.80", LoaderKind::NeoForge, "21.1.80"),
|
||||
// 老 HMCL 口径(存量实例还会带着它们)
|
||||
("fabric-loader-0.16.5-1.20.1", LoaderKind::Fabric, "0.16.5"),
|
||||
("quilt-loader-0.24.0-1.20.1", LoaderKind::Quilt, "0.24.0"),
|
||||
// 第三方口径
|
||||
("1.13.2-OptiFine_HD_U_E6", LoaderKind::OptiFine, "HD_U_E6"),
|
||||
("1.12.2-LiteLoader", LoaderKind::LiteLoader, ""),
|
||||
];
|
||||
for (id, kind, version) in cases {
|
||||
let info = loader_from_id(id);
|
||||
@@ -225,6 +233,127 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// 边界翻译不许把两个种类塌成一个(七个 arm 手写,复制粘贴最容易在这里出事)。
|
||||
///
|
||||
/// 抓得住:`dto_kind` 里某个 arm 抄错(`Quilt => LoaderKind::Fabric` 之类)——种类集合
|
||||
/// 会少一个、出现重复;`Vanilla` 只能由 `Vanilla` 来,所以「认成原版」不会被错翻掩盖。
|
||||
/// 抓不住:core 端把 id 认成错的种类(那是 core 真值表的活)。
|
||||
#[test]
|
||||
fn dto_kind_is_injective_over_all_core_kinds() {
|
||||
use unlucky_core::game::LoaderKind as Core;
|
||||
let all = [
|
||||
Core::Vanilla,
|
||||
Core::Fabric,
|
||||
Core::Quilt,
|
||||
Core::Forge,
|
||||
Core::NeoForge,
|
||||
Core::OptiFine,
|
||||
Core::LiteLoader,
|
||||
];
|
||||
let mapped: Vec<LoaderKind> = all.iter().copied().map(dto_kind).collect();
|
||||
for (index, kind) in mapped.iter().enumerate() {
|
||||
assert!(
|
||||
!mapped[..index].contains(kind),
|
||||
"core 的 {:?} 与前面的种类翻成了同一个 DTO 种类 {kind:?}",
|
||||
all[index]
|
||||
);
|
||||
}
|
||||
assert_eq!(dto_kind(Core::Vanilla), LoaderKind::Vanilla);
|
||||
}
|
||||
|
||||
/// 端到端(数据层):真装出来的四个加载器实例,走 [`instances`] 的快照各自带对加载器。
|
||||
///
|
||||
/// 造的是本启动器**真实产物**的形状:`profiles.json` 里 `versionId` 是自家 id,
|
||||
/// `versions/<id>/<id>.json` 里 `inheritsFrom` 指向原版层(生成端见
|
||||
/// `unlucky_core::addon::loader`)。原版版本号由此取自继承链根,不是从 id 里切的。
|
||||
///
|
||||
/// 抓得住:`describe_version` ↔ `loader_from_id` ↔ `instances` 这条链上任何一环把加载器
|
||||
/// 丢掉(磁贴会显示「原版」——M122 的用户可见症状);以及继承链根取错(磁贴的版本号)。
|
||||
/// 抓不住:实例页真出图(走 app-iced 的视图模型与 `loader_badge`,本 crate 不渲染);
|
||||
/// 也抓不住「真下载安装了 Fabric/Quilt/Forge/NeoForge」——这里只伪造落盘结果。
|
||||
#[test]
|
||||
fn own_installed_instances_snapshot_carries_their_loader() {
|
||||
let dir = std::env::temp_dir().join(format!("ull-host-game-own-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
let env = HostEnv::at_root(&dir);
|
||||
std::fs::create_dir_all(env.layout.config()).expect("config 目录");
|
||||
|
||||
let cases = [
|
||||
(
|
||||
"1.20.4-fabric-0.16.5",
|
||||
"1.20.4",
|
||||
LoaderKind::Fabric,
|
||||
"Fabric 0.16.5",
|
||||
),
|
||||
(
|
||||
"1.20.4-quilt-0.27.0",
|
||||
"1.20.4",
|
||||
LoaderKind::Quilt,
|
||||
"Quilt 0.27.0",
|
||||
),
|
||||
(
|
||||
"1.20.1-forge-47.2.0",
|
||||
"1.20.1",
|
||||
LoaderKind::Forge,
|
||||
"Forge 47.2.0",
|
||||
),
|
||||
(
|
||||
"1.21.1-neoforge-21.1.80",
|
||||
"1.21.1",
|
||||
LoaderKind::NeoForge,
|
||||
"NeoForge 21.1.80",
|
||||
),
|
||||
];
|
||||
for (version_id, mc_version, _, _) in cases {
|
||||
for (id, inherits) in [(mc_version, None), (version_id, Some(mc_version))] {
|
||||
let json = match inherits {
|
||||
Some(parent) => format!(r#"{{"id":"{id}","inheritsFrom":"{parent}"}}"#),
|
||||
None => format!(r#"{{"id":"{id}"}}"#),
|
||||
};
|
||||
let path = env.game.version_json(id);
|
||||
std::fs::create_dir_all(path.parent().expect("版本目录")).expect("版本目录");
|
||||
std::fs::write(&path, json).expect("写版本 JSON");
|
||||
}
|
||||
}
|
||||
|
||||
let profiles = ProfileList {
|
||||
profiles: cases
|
||||
.iter()
|
||||
.map(
|
||||
|(version_id, _, _, _)| unlucky_core::game::InstanceProfile {
|
||||
id: unlucky_core::game::ProfileId::generate(),
|
||||
name: format!("装好的 {version_id}"),
|
||||
version_id: (*version_id).to_string(),
|
||||
game_dir: None,
|
||||
running_dir: None,
|
||||
java_path: None,
|
||||
created_at: None,
|
||||
last_played_at: None,
|
||||
extra: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
..ProfileList::default()
|
||||
};
|
||||
unlucky_core::game::install::instance::write_profiles(&env.layout, &profiles)
|
||||
.expect("写清单");
|
||||
|
||||
let snapshot = instances(&env);
|
||||
assert_eq!(snapshot.len(), cases.len());
|
||||
for ((version_id, mc_version, kind, badge), tile) in cases.iter().zip(&snapshot) {
|
||||
assert_eq!(tile.version_id.as_str(), *version_id);
|
||||
assert_eq!(
|
||||
tile.mc_version.as_str(),
|
||||
*mc_version,
|
||||
"{version_id} 的版本号取错"
|
||||
);
|
||||
assert_eq!(tile.loader.kind, *kind, "{version_id} 的加载器");
|
||||
assert_eq!(tile.loader.badge().as_str(), *badge, "{version_id} 的徽标");
|
||||
assert!(tile.version_present, "{version_id} 的载荷应当存在");
|
||||
}
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolation_write_then_read_round_trips() {
|
||||
let dir = std::env::temp_dir().join(format!("ull-host-game-{}", std::process::id()));
|
||||
|
||||
Reference in new issue
Block a user