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

+53 -9
View File
@@ -1,7 +1,8 @@
//! `ull install <version> --loader <kind> [--loader-version <v>]`:加载器层的编排。
//!
//! ```text
//! LoaderSource::resolve(官方 meta:版本列表 → profile JSON / installer profile)
//! 目标解析:<version> 位是原版版本 → 叠原版;是已装的加载器版本 id → 叠在它上面(仅 LiteLoader)
//! → LoaderSource::resolve(官方 meta:版本列表 → profile JSON / installer profile / LiteLoader 版本表)
//! → LoaderInstallPlan::build(纯计算:库清单 + loader 版本 JSON 落点)
//! → [--dry-run 到此为止,但多带一段 processors 计划]
//! → 原版已装?否 → vanilla::plan_and_install(先装原版)
@@ -13,16 +14,22 @@
//! 「先装原版」是**硬前置**:loader 版本 JSON 只声明 `inheritsFrom`,jar 与资源都在原版那层,
//! 原版缺失时启动链必然断裂。已装则跳过(`is_version_installed` 判据 = 版本 JSON 在盘上)。
//!
//! **父版本**:`<version>` 位写成加载器版本 id(`1.12.2-forge-14.23.5.2860`)时,它就是父版本,
//! 原版版本从盘上的继承链读出来(`addon::loader::parent`)。只有 LiteLoader 接受这种叠法
//! (它靠 `--tweakClass` 追加生效);别家**具名拒绝**,不会默默改成叠原版。
//!
//! processors 段的顺序也是硬约束:**先跑 processors,再写版本 JSON**。版本清单存在是
//! "这版装完了"的判据,抢在 processors 前写会留下一个启动必崩却被报成已安装的实例。
use unlucky_core::addon::loader::{
LoaderError, LoaderInstallPlan, LoaderKind, LoaderPlanInput, LoaderSource,
parent, LoaderError, LoaderInstallPlan, LoaderKind, LoaderPlanInput, LoaderSource,
};
use unlucky_core::download::blocking::block_on;
use unlucky_core::download::cache::now_ms;
use unlucky_core::game::arguments::Environment;
use unlucky_core::game::install::execute;
use unlucky_core::game::loader::{InstanceLoader, LoaderKind as WideLoaderKind};
use unlucky_core::game::GameLayout;
use super::report_loader::ProcessorSection;
use super::{
@@ -38,11 +45,11 @@ use crate::output::Report;
///
/// # Errors
///
/// 用法错误、加载器未知/不支持、元数据读取失败、原版装不上、库下载失败、
/// installer processors 失败、实例登记失败。
/// 用法错误、加载器未知/不支持、父版本缺失或组合不支持、元数据读取失败、原版装不上、
/// 库下载失败、installer processors 失败、实例登记失败。
pub(super) fn run(ctx: &Ctx, args: &InstallArgs, kind: LoaderKind) -> Result<Report, CliError> {
let version = args.version.trim().to_string();
if version.is_empty() {
let requested = args.version.trim().to_string();
if requested.is_empty() {
return Err(CliError::usage(
"install 需要版本 id:%prog% install <version> --loader fabric",
));
@@ -54,10 +61,17 @@ pub(super) fn run(ctx: &Ctx, args: &InstallArgs, kind: LoaderKind) -> Result<Rep
let meta_cache = layout.meta_cache();
let now = now_ms();
let (version, parent) = resolve_target(ctx, &game, &requested, kind)?;
let release = block_on(async {
let source = LoaderSource::new(&engine, &meta_cache);
source
.resolve(kind, &version, args.loader_version.as_deref())
.resolve_on(
kind,
&version,
args.loader_version.as_deref(),
parent.as_deref(),
)
.await
})
.map_err(|error: LoaderError| map_loader_error(&error, ctx))?;
@@ -73,8 +87,9 @@ pub(super) fn run(ctx: &Ctx, args: &InstallArgs, kind: LoaderKind) -> Result<Rep
layout: &game,
release: &release,
environment: &Environment::host(),
vanilla_installed: game.is_version_installed(&version),
});
parent_installed: game.is_version_installed(&release.parent_version_id),
})
.map_err(|error| map_loader_error(&error, ctx))?;
let processor_plan = loader_processors::plan(ctx, &release)?;
if args.dry_run {
@@ -162,3 +177,32 @@ fn map_loader_error(error: &LoaderError, ctx: &Ctx) -> CliError {
};
CliError::user(error.kind().as_str(), error.message().to_string()).with_hint(hint)
}
/// `<version>` 位 → (原版版本, 父版本 id)。
///
/// 两种写法(`InstallArgs.version` 的文档口径允许版本 id):
/// - 原版版本(`1.12.2`)→ 叠原版,父版本 = 原版;
/// - **已装的加载器版本 id**(`1.12.2-forge-14.23.5.2860`)→ 叠在它上面,原版版本从盘上的
/// 继承链读出来。只有 LiteLoader 接受这种叠法;别家给具名错误,不静默改成叠原版。
fn resolve_target(
ctx: &Ctx,
game: &GameLayout,
requested: &str,
kind: LoaderKind,
) -> Result<(String, Option<String>), CliError> {
if InstanceLoader::parse(requested).kind == WideLoaderKind::Vanilla {
return Ok((requested.to_string(), None));
}
if kind != LoaderKind::LiteLoader {
return Err(CliError::usage(format!(
"`{requested}` 是一个已装加载器版本 id:只有 LiteLoader 支持叠在别的加载器层上,{} 只能叠在原版上",
kind.label()
))
.with_hint(
"换成原版版本号:`%prog% install 1.20.1 --loader forge`;或先 `%prog% instances list` 看已装版本",
));
}
let mc_version =
parent::root_version_id(game, requested).map_err(|error| map_loader_error(&error, ctx))?;
Ok((mc_version, Some(requested.to_string())))
}
+20 -5
View File
@@ -4,6 +4,7 @@
//! --loader 缺省 → vanilla:M27 原版安装(resolve → plan → execute → register)
//! --loader fabric|quilt → addon::loader:原版(已装则跳)→ loader 版本 JSON → 补 loader 库 → register
//! --loader neoforge|forge → 同上;需要 installer processors 的版本再跑 M35 执行器(依赖库 → java → 版本 JSON)
//! --loader liteloader → 官方版本表 → 从零合成 launchwrapper 清单(不跑任何外部进程)
//! ```
//!
//! CLI 只做四件事:建下载引擎、把计划/结果排版成人读与 `--json` 两种形态
@@ -22,6 +23,7 @@ use std::sync::Arc;
use unlucky_core::addon::loader::LoaderKind;
use unlucky_core::download::{DownloadEngine, DownloadErrorKind};
use unlucky_core::game::install::{InstallError, InstallReport, Registration};
use unlucky_core::game::LoaderKind as WideLoaderKind;
use crate::cli::InstallArgs;
use crate::context::Ctx;
@@ -33,9 +35,14 @@ pub(super) const CONCURRENCY: usize = 16;
/// 执行 `install`:按 `--loader` 分发。
///
/// "认不出" 与 "认得但装不了" 是两件事,错误码也分开(`loader_unknown` / `loader_unsupported`):
/// `optifine` 属于后者——core 的识别词表里有它(`ull help instances` 也这么写),只是安装管线
/// 没接(要跑外部 `optifine.Patcher` + 原地改 zip)。两者混成一句"未知加载器"会让用户以为
/// 是自己的版本 id 写错了。
///
/// # Errors
///
/// 用法错误、加载器未知、版本不存在、网络失败、校验不符、写盘失败、实例清单损坏。
/// 用法错误、加载器未知/认得但装不了、版本不存在、网络失败、校验不符、写盘失败、实例清单损坏。
pub fn run(ctx: &Ctx, args: &InstallArgs) -> Result<Report, CliError> {
match args
.loader
@@ -46,10 +53,18 @@ pub fn run(ctx: &Ctx, args: &InstallArgs) -> Result<Report, CliError> {
None | Some("vanilla") | Some("none") => vanilla::run(ctx, args),
Some(raw) => match LoaderKind::parse(raw) {
Some(kind) => loader::run(ctx, args, kind),
None => Err(
CliError::user("loader_unknown", format!("未知加载器 `{raw}`"))
.with_hint("--loader 取值:vanilla / fabric / quilt / neoforge / forge"),
),
None => Err(match WideLoaderKind::parse(raw) {
Some(WideLoaderKind::OptiFine) => CliError::user(
"loader_unsupported",
"optifine 认得出但还没接入安装管线(要跑外部 Patcher 并原地改 zip)",
)
.with_hint(
"先用官方安装器装好,再 `%prog% instances list` 确认它已在;或用 fabric / quilt / neoforge / forge / liteloader",
),
_ => CliError::user("loader_unknown", format!("未知加载器 `{raw}`")).with_hint(
"--loader 取值:vanilla / fabric / quilt / neoforge / forge / liteloader(optifine 认得出但装不了)",
),
}),
},
}
}
@@ -334,6 +334,7 @@ mod tests {
let release = LoaderRelease {
kind: LoaderKind::Fabric,
mc_version: "1.20.1".to_string(),
parent_version_id: "1.20.1".to_string(),
loader_version: "0.15.11".to_string(),
version_id: "1.20.1-fabric-0.15.11".to_string(),
version_json: VersionJson::from_json_str(
@@ -349,8 +350,9 @@ mod tests {
layout: &layout,
release: &release,
environment: &Environment::host(),
vanilla_installed: true,
parent_installed: true,
})
.expect("计划应可组")
}
/// **M216 r2 P2-2**:装加载器时「先装原版」那一步替换了共享 `.minecraft` 里的原版清单,
@@ -372,7 +374,6 @@ mod tests {
}),
warnings: vec!["已替换版本清单 /g/.minecraft/versions/1.20.1/1.20.1.json".to_string()],
};
let text = human_lines(false, &plan, None, Some(&vanilla), None, &[], "n", None).join("\n");
assert!(
text.contains("已替换版本清单"),