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

+174
View File
@@ -0,0 +1,174 @@
//! 解析 → 计划 → 落盘 → 继承解析 → 启动参数顺序(两条父链:原版 / Forge)。
use unlucky_core::addon::loader::{LoaderInstallPlan, LoaderKind, LoaderPlanInput};
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;
use unlucky_core::game::layout::GameLayout;
use unlucky_core::launch::command_builder::tokenize;
use crate::support::{
engine, liteloader_release, prefill, put_version, read_version, TempDir, FORGE_1122,
VANILLA_1122,
};
/// 原版父链:装完能落盘,清单指向原版,`arguments.game` 带 tweakClass。
#[test]
fn installs_over_vanilla_and_writes_the_manifest() {
let temp = TempDir::new("vanilla");
let layout = GameLayout::new(temp.path());
put_version(&layout, "1.12.2", VANILLA_1122);
let release = liteloader_release("1.12.2");
assert_eq!(release.version_id, "1.12.2-liteloader-1.12.2-SNAPSHOT");
let plan = LoaderInstallPlan::build(LoaderPlanInput {
layout: &layout,
release: &release,
environment: &Environment::host(),
parent_installed: true,
})
.expect("父版本已装,计划应可组");
assert!(!plan.vanilla_install_needed);
assert!(plan.install.client_jar().is_none(), "jar 归原版层");
// 三个库:launchwrapper(Mojang 库仓)+ asm-all(Central)+ 本体 jar(官方直链)。
let names: Vec<&str> = plan.files().iter().map(|file| file.name.as_str()).collect();
assert_eq!(names.len(), 3, "{names:?}");
let loader_jar = plan
.files()
.iter()
.find(|file| file.name == "com.mumfrey:liteloader:1.12.2-SNAPSHOT")
.expect("缺本体 jar");
assert!(
loader_jar
.url
.ends_with("/1.12.2-SNAPSHOT/liteloader-1.12.2-20171128.144431-4-release.jar"),
"本体直链:{}",
loader_jar.url
);
let asm = plan
.files()
.iter()
.find(|file| file.name.contains("asm-all"))
.expect("缺 asm-all");
assert!(
asm.url.starts_with("https://repo1.maven.org/maven2/"),
"asm-all 只能从 Central 取:{}",
asm.url
);
// 全部落点先铺好占位文件(库无 sha1/size → 存在即信),执行段只写版本 JSON。
let engine = engine(temp.path());
let prefilled = prefill(&plan.install);
assert_eq!(prefilled, 3);
let report = block_on(execute(&engine, &plan.install, 4, None)).expect("执行应成功");
assert!(report.is_complete(), "{:?}", report.failures(3));
let written = read_version(&layout, &release.version_id);
assert_eq!(written.inherits_from.as_deref(), Some("1.12.2"));
assert_eq!(
written.main_class.as_deref(),
Some("net.minecraft.launchwrapper.Launch")
);
assert!(
written.minecraft_arguments.is_none(),
"loader 层不许写 minecraftArguments:那会顶掉父链"
);
}
/// Forge 父链:**父链的 FMLTweaker 必须还在**,LiteLoader 的 tweakClass 追加在它后面。
///
/// 这条是 M218 SURVEY-p1 的「准入条件」:靠 `--tweakClass` 生效,就要求继承时
/// `arguments.game` 是追加、`minecraftArguments` 不被子层覆盖。启动参数顺序照
/// `core::launch::game_args` 的第 24 项(legacy 子串)→ 第 25 项(`arguments.game`)复现,
/// 真实 argv 由 `tests/launch/cmdline_snapshot.rs` 钉住。
#[test]
fn stacks_on_forge_and_keeps_the_forge_tweak_class() {
let temp = TempDir::new("forge");
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", FORGE_1122);
let release = liteloader_release("1.12.2-forge-14.23.5.2860");
assert!(!release.parent_is_vanilla());
let plan = LoaderInstallPlan::build(LoaderPlanInput {
layout: &layout,
release: &release,
environment: &Environment::host(),
parent_installed: true,
})
.expect("父版本已装,计划应可组");
assert_eq!(plan.install.jar_id, "1.12.2", "jar 仍取原版那层");
let engine = engine(temp.path());
prefill(&plan.install);
let report = block_on(execute(&engine, &plan.install, 4, None)).expect("执行应成功");
assert!(report.is_complete(), "{:?}", report.failures(3));
let installed = read_version(&layout, &release.version_id);
let mut source = std::collections::BTreeMap::new();
source.insert("1.12.2".to_string(), read_version(&layout, "1.12.2"));
source.insert(
"1.12.2-forge-14.23.5.2860".to_string(),
read_version(&layout, "1.12.2-forge-14.23.5.2860"),
);
source.insert(release.version_id.clone(), installed);
let resolved = resolve(&source, &source[&release.version_id]).expect("三层继承应可解析");
// ① 父链的 legacy 参数整串还在(子层没写 minecraftArguments)。
let legacy = resolved
.minecraft_arguments
.as_deref()
.expect("Forge 的 minecraftArguments 不许被子层顶掉");
assert!(
legacy.contains("net.minecraftforge.fml.common.launcher.FMLTweaker"),
"Forge 的 tweakClass 丢了:{legacy}"
);
// ② 启动顺序:legacy(第 24 项)在前,arguments.game(第 25 项)在后。
let mut game: Vec<String> = tokenize(legacy);
game.extend(
resolved
.arguments
.as_ref()
.expect("arguments 应保留")
.expand(&Environment::host())
.game,
);
let forge_at = game
.iter()
.position(|token| token.contains("FMLTweaker"))
.expect("Forge tweakClass 不在游戏参数里");
let liteloader_at = game
.iter()
.position(|token| token == "com.mumfrey.liteloader.launch.LiteLoaderTweaker")
.expect("LiteLoader tweakClass 不在游戏参数里");
assert!(
forge_at < liteloader_at,
"launchwrapper 按出现顺序应用 tweak:Forge 必须在前,实际 {game:?}"
);
assert_eq!(game[liteloader_at - 1], "--tweakClass");
// ③ 主类与库顺序:Forge 的库在前(classpath 先到先得),LiteLoader 的在后。
assert_eq!(
resolved.main_class.as_deref(),
Some("net.minecraft.launchwrapper.Launch")
);
let libraries: Vec<&str> = resolved
.libraries
.as_deref()
.expect("libraries")
.iter()
.map(|library| library.name.as_str())
.collect();
let forge_lib = libraries
.iter()
.position(|name| name.starts_with("net.minecraftforge:forge:"))
.expect("缺 Forge 本体库");
let liteloader_lib = libraries
.iter()
.position(|name| name.starts_with("com.mumfrey:liteloader:"))
.expect("缺 LiteLoader 本体库");
assert!(forge_lib < liteloader_lib, "{libraries:?}");
}