feat(core): LiteLoader 安装路线(官方版本表 + 从零合成 launchwrapper 清单)

按 M218 SURVEY-p1 §4.1 的清单接上 LiteLoader 的安装路线,不动 OptiFine。

- core: 新增 addon::loader::liteloader(官方 versions.json 解析 + 合成清单)与
  addon::loader::parent(父版本链读盘定位原版版本);LoaderKind 加 LiteLoader;
  LoaderRelease 加 parent_version_id、LoaderPlanInput::vanilla_installed → parent_installed;
  LoaderInstallPlan::build 改返回 Result(父版本缺失具名报错);LoaderSource::resolve_on
  支持指定父版本(仅 LiteLoader,其它组合具名 unsupported)。
- 继承边界(本 mission 第一步核实):arguments.game 父在前子在后追加,minecraftArguments
  子覆盖父,启动顺序 legacy(24) → arguments.game(25)。所以 tweakClass 只写 arguments.game,
  绝不写 minecraftArguments(会顶掉 Forge 1.12.2 的 FMLTweaker)。
- maven/fabric 的 `_ =>` 通配改成穷尽分支:非 maven 族不再静默回落 Forge 的仓库;
  maven::{metadata_url,installer_url} 改返回 Option<String>。
- presenter: supported() 收 LiteLoader、wizard_choices 六选、dto::instances 按关注点拆成
  instances/{loader,runtime,create,tests}.rs(原文件 395 行,动手前先拆)。
- cli: --loader 区分「认不出」(loader_unknown) 与「认得但装不了」(loader_unsupported);
  <version> 位写加载器版本 id 时按父版本处理(只有 liteloader 接受);help 只改 install 段。
- app-iced: 下载页加载器 chip 加 LiteLoader(1.5.2~1.12.2,两头按官方没有构建拒),
  chip 清单改为委派 Loader::ALL。
- 测试: 真实响应的裁剪夹具(dl.liteloader.com 与 repo.mumfrey.com 的 maven-metadata)
  驱动「解析→计划→落盘→继承→启动参数顺序」全链;Forge 父链有回归(FMLTweaker 必须还在
  且排在 LiteLoader 之前);父版本缺失/组合不支持/取消/坏表各自具名错误。
This commit is contained in:
AskaEth committed 2026-11-10 19:34:09 +08:00
1 parent 307d29239a
commit 944b1610c8
37 files changed
+2427 -518

No files matched your search

+157
View File
@@ -0,0 +1,157 @@
//! 边界与失败面:父版本缺失、组合不支持、数据源形状坏、取消。
//!
//! 网络层的失败映射(`DownloadError` → `loader_network`)由 `core::download` 的引擎测试
//! 与既有加载器测试覆盖,这里**不联网**:外部源的真实可用性在 mission 报告里单独列
//! (含探测结果),不混进单元测试冒充。
use std::sync::atomic::AtomicBool;
use unlucky_core::addon::loader::liteloader::parse_versions;
use unlucky_core::addon::loader::{
LoaderErrorKind, LoaderInstallPlan, LoaderKind, LoaderPlanInput, LoaderSource,
};
use unlucky_core::download::blocking::block_on;
use unlucky_core::game::arguments::Environment;
use unlucky_core::game::inherit::resolve;
use unlucky_core::game::install::{execute_with_cancel, InstallErrorKind};
use unlucky_core::game::layout::GameLayout;
use crate::support::{
engine, liteloader_release, prefill, put_version, read_version, TempDir, VANILLA_1122,
};
/// 父版本不是原版、又没装在盘上 → **具名报错**,绝不默默改成叠原版。
#[test]
fn missing_loader_parent_is_a_named_error() {
let temp = TempDir::new("missing-parent");
let layout = GameLayout::new(temp.path());
put_version(&layout, "1.12.2", VANILLA_1122);
let release = liteloader_release("1.12.2-forge-14.23.5.2860");
let error = LoaderInstallPlan::build(LoaderPlanInput {
layout: &layout,
release: &release,
environment: &Environment::host(),
parent_installed: false,
})
.expect_err("父版本缺失必须报错");
assert_eq!(error.kind(), LoaderErrorKind::NotFound);
assert!(
error.message().contains("1.12.2-forge-14.23.5.2860"),
"错误里要点名缺的是哪个父版本:{}",
error.message()
);
assert!(error.hint().is_some(), "要给出下一步怎么办");
}
/// 父版本链:读盘定位原版版本 id(合成 loader id / 定位 jar 都用它)。
#[test]
fn parent_chain_walks_to_the_vanilla_root() {
let temp = TempDir::new("parent-chain");
let layout = GameLayout::new(temp.path());
put_version(&layout, "1.12.2", VANILLA_1122);
put_version(
&layout,
"1.12.2-forge-14.23.5.2860",
crate::support::FORGE_1122,
);
assert_eq!(
unlucky_core::addon::loader::parent::root_version_id(&layout, "1.12.2-forge-14.23.5.2860")
.expect("两层链应走得到根"),
"1.12.2"
);
assert_eq!(
unlucky_core::addon::loader::parent::root_version_id(&layout, "1.12.2").expect("根是自己"),
"1.12.2"
);
// 没装的父版本 → not_found(而不是猜一个原版版本号)。
let error = unlucky_core::addon::loader::parent::root_version_id(&layout, "9.9.9-forge-1.0.0")
.expect_err("缺父版本必须报错");
assert_eq!(error.kind(), LoaderErrorKind::NotFound);
}
/// 只有 LiteLoader 接受「父版本不是原版」;别家**具名拒绝**,不去发请求。
#[test]
fn only_liteloader_accepts_a_non_vanilla_parent() {
let temp = TempDir::new("combo-guard");
let engine = engine(temp.path());
let source = LoaderSource::new(&engine, temp.path());
let error = block_on(async {
source
.resolve_on(
LoaderKind::Forge,
"1.12.2",
Some("14.23.5.2860"),
Some("1.12.2-liteloader-1.12.2-SNAPSHOT"),
)
.await
})
.expect_err("Forge 叠在 LiteLoader 上必须被拒");
assert_eq!(error.kind(), LoaderErrorKind::Unsupported);
assert!(
error
.message()
.contains("1.12.2-liteloader-1.12.2-SNAPSHOT"),
"要说清是哪个组合:{}",
error.message()
);
}
/// 版本表形状坏 / 空表:各自给具名大类,不当成「没有版本」糊过去。
#[test]
fn broken_version_table_is_reported_distinctly() {
let shape = parse_versions(r#"{"versions": []}"#).expect_err("形状不符");
assert_eq!(shape.kind(), LoaderErrorKind::Metadata);
let empty = parse_versions(r#"{"meta": {}, "versions": {}}"#).expect_err("空表");
assert_eq!(empty.kind(), LoaderErrorKind::NotFound);
// 版本段缺失(记录没有 version):只剩 latest 别名 → 仍当空表处理。
let only_latest = r#"{"versions": {"1.7.10": {"artefacts": {"com.mumfrey:liteloader":
{"latest": {"tweakClass": "x", "version": "", "file": "liteloader.jar"}}}}}}"#;
let error = parse_versions(only_latest).expect_err("只有 http 别名时没有可用版本");
assert_eq!(error.kind(), LoaderErrorKind::NotFound);
}
/// 取消:执行段看标志就返回 `Cancelled`,**版本 JSON 不落盘**(不留半成品)。
#[test]
fn cancel_leaves_no_manifest() {
let temp = TempDir::new("cancel");
let layout = GameLayout::new(temp.path());
put_version(&layout, "1.12.2", VANILLA_1122);
let release = liteloader_release("1.12.2");
let plan = LoaderInstallPlan::build(LoaderPlanInput {
layout: &layout,
release: &release,
environment: &Environment::host(),
parent_installed: true,
})
.expect("计划应可组");
let engine = engine(temp.path());
prefill(&plan.install);
let cancel = AtomicBool::new(true);
let error = block_on(execute_with_cancel(
&engine,
&plan.install,
4,
None,
Some(&cancel),
))
.expect_err("取消必须返回错误");
assert_eq!(error.kind(), InstallErrorKind::Cancelled);
assert!(
!layout.version_json(&release.version_id).exists(),
"取消之后不许留下版本清单"
);
// 继承链在缺清单时也应明确报「父版本缺失」,而不是静默降级成原版。
let mut source = std::collections::BTreeMap::new();
source.insert("1.12.2".to_string(), read_version(&layout, "1.12.2"));
let error = resolve(&source, &release.version_json).expect_err("父版本缺失");
assert!(matches!(
error,
unlucky_core::game::GameError::MissingParent { .. }
));
}