refactor(core): M126 r1 返工:词表改由宏单表生成,安装侧解析收口

复审 r1 的三条:

P1(CI 会红):`const MARKERS: [(LoaderKind, &'static str); 6]` 触发
clippy::redundant_static_lifetimes(`error: constants have by default a 'static
lifetime`)。返工直接**删掉 MARKERS**:标记串就取 `LoaderKind::as_str()`,候选顺序取
`LoaderKind::ALL`,认不认标记看 `has_marker()`——解析表既没有独立字面量,也没有独立清单。

P2-1:「漏加 ALL 不会被任何东西拦住」是真话,之前的 doc 覆盖声明是假的。返工用
`loader_kind_vocab!` 宏从**一张表**一次生成枚举 / `ALL` / `as_str` / `has_marker`:
「加了一支却忘了进清单」在物理上不可能(没有第二份清单可写)。尺寸锚随之删除(`[Self; N]`
的宽度本来就由表决定),`parse` 留在宏外的独立 impl 里。

P2-2:「全仓唯一一份 id 字面量」原先不成立——安装侧 `LoaderKind::parse` 还手写了一份反向
匹配表,并带一个只进不出的 `neo-forge` 别名。返工让 install 侧 `parse` **委派**
`core::game::LoaderKind::parse`,只额外收 `neo-forge`(Modrinth 拼法)这一个输入别名,
并新增 `addon::loader::tests::vocabulary` 覆盖「委派没接错 + 别名只进不出 + 装不了的几家
返回 None」。

测试:core 的 `markers_cover_every_kind_that_has_one` 换成
`every_stable_id_parses_back_to_its_kind`(往返 + 顺序优先级),仍是「改名不可见、只有契约
测试能抓」的分工,doc 里写明。

门禁:fmt OK;clippy -p unlucky-core -p unlucky-presenter -p unlucky-cli --all-targets
-- -D warnings 干净;三 crate 测试 core lib 401 / presenter 220 / cli 41 全 ok。
文件行数:loader.rs 397(≤400)。
This commit is contained in:
AskaEth committed 2026-11-05 18:08:57 +08:00
1 parent be37c17962
commit a322cfcbfe
4 files changed
+219 -138

No files matched your search

+25 -7
View File
@@ -62,15 +62,33 @@ 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,
}
}
@@ -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,63 @@
//! 安装侧词表与 `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}` 不该被安装侧接受");
}
}