refactor(core): M126 加载器稳定 id 词表收敛到 game::LoaderKind::as_str

M122 把「id → 加载器」的解析收到 core::game::loader::InstanceLoader::parse,但
「种类 → 对外稳定 id」的命名词表还散在多处,其中 install 侧那一份只靠一句「不要改名」
的注释约束,MARKERS 更是第四张无守卫字面量表。

- core::game::LoaderKind 新增 as_str(**全仓唯一一份 id 字面量**)、has_marker
  (穷尽 match:新种类必须表态认不认 id 标记)、parse(as_str 的反查)与 ALL(清单,
  顺序即标记优先级);MARKERS 的标记串改为取自 as_str,不再是独立字面量。
- addon::loader::LoaderKind::as_str 改成委派(结构映射,无字面量):安装侧拼出的
  <mc>-<kind>-<loader> 与解析侧从此同源。
- presenter::dto::instances::LoaderKind 的 as_str/parse 委派给 core(to_core/from_core
  是结构映射),删掉第四份词表;CLI 删掉 loader_kind_id 翻译层,直接调 core。
- 测试:core 新增 markers_cover_every_kind_that_has_one(MARKERS↔ALL↔has_marker↔as_str
  逐格对齐 + 按标记串 round-trip + neoforge 先于 forge);presenter 新增
  loader_kind_ids_are_the_released_contract(七个 id 逐字钉住 + 反查 + 别名隔离)。
- 对外输出未变:真二进制对同一组 7 类实例的 `instances list --json` 输出逐字节相同
  (md5 0341752658ca28e020ba48c31a51a124),人读输出同样逐字节相同。

file 行数:core/game/loader.rs 337→399(≤400 硬约束,文档按密度重排、事实未丢)。
This commit is contained in:
AskaEth committed 2026-11-05 18:08:56 +08:00
1 parent 84f85bbc79
commit be37c17962
4 files changed
+253 -134

No files matched your search

+10 -24
View File
@@ -8,8 +8,10 @@
//!
//! 每一行实例带一个 `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 显示成原版」)。
//!
//! ## 约定一:`loader` 恒存在,原版是 `{"kind":"vanilla","version":""}`
//!
@@ -44,7 +46,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 +130,17 @@ 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;