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
@@ -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