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

+13 -5
View File
@@ -22,8 +22,8 @@ pub const fn general() -> &'static str {
[--dry-run] 把已铺好的版本登记成实例(只读外部、不搬文件;缺省运行目录
落 ULL 数据区的独立目录;--dry-run 只检查不写盘)
install <version> [--loader X] [--loader-version V] [--name N] [--dry-run]
下载并安装版本本体(--loader fabric|quilt|neoforge|forge
时再叠一层加载器)→ 登记实例;--dry-run 只出计划
下载并安装版本本体(--loader fabric|quilt|neoforge|forge|
liteloader 时再叠一层加载器)→ 登记实例;--dry-run 只出计划
launch <instance> [--dry-run] [--account <name>] [--java <path>] [--username <name>]
[--max-memory <MB>] 启动实例;--dry-run 只打印组装后的命令行
pack import <file> [--target <dir>] [--name N] [--overwrite] [--no-register]
@@ -184,10 +184,18 @@ pub fn topic(name: &str) -> Option<&'static str> {
(profiles.json + config/instances/<id>.json)。已存在且校验通过的文件会跳过,
重复装同一版本不重复下载、不重复建实例。
--dry-run:只输出计划(文件清单 + 总大小 + 每类资源的源选择),不下载、不登记。
--loader:vanilla(缺省)/ fabric / quilt / neoforge / forge。装加载器时先生成
versions/<原版>-<加载器>-<版本>/<…>.json(inheritsFrom 原版 + 主类 + loader 库),
--loader:vanilla(缺省)/ fabric / quilt / neoforge / forge / liteloader。装加载器时先生成
versions/<原版>-<加载器>-<版本>/<…>.json(inheritsFrom 父版本 + 主类 + loader 库),
原版没装会先装原版,实例的 versionId 指向 loader 版本。
--loader-version:加载器版本;缺省取官方默认(Fabric 系首个稳定版,maven 系最新)。
optifine 认得出但**没接入安装管线**(要跑外部 Patcher 并原地改 zip),报 loader_unsupported;
其余认不出的拼法报 loader_unknown —— 两者不是一回事。
liteloader 的特殊之处:清单是从官方版本表**从零合成**的(mainClass = launchwrapper,
--tweakClass 追加在父链参数之后),不跑任何外部进程;不叠加载器时只占一个 loader 库。
<version> 位还可以写**已装的加载器版本 id**(`1.12.2-forge-14.23.5.2860 --loader liteloader`)
表示「叠在它上面」:原版版本从盘上的继承链读出来,父版本没装会具名报错(不会猜一个原版版本号)。
只有 liteloader 接受这种叠法,别家给 usage 错误说清「只能叠在原版上」。
--loader-version:加载器版本;缺省取官方默认(Fabric 系首个稳定版,maven 系最新,
LiteLoader 优先 release 分支、没有则最新快照)。
Forge 可写 47.2.0 或 1.20.1-47.2.0 两种形态。
NeoForge/Forge 里需要跑官方 installer processors 的版本会真跑一遍(M35 已实装):
缺 Java 时按 D23 的回退链自动装一个托管 JDK;--dry-run 只列出要跑的 processor 与依赖库。
+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("已替换版本清单"),