安装 Fabric 与 Quilt
Fabric 和 Quilt 的安装逻辑完全一样,差别只在 meta 服务器和 API 版本号。协议是同一套,我们复用了同一条合并管线。
安装流程
装一个加载器版本分五步,Quilt 走的是同一条路:
- 确定原版基底——加载器的 profile 只是一层叠加,没有 client jar、没有资源、没有原版那些库,这些全部要由原版提供。所以合并之前,必须先拿到原版版本 JSON。
- 取加载器版本——从 meta 拉该 MC 版本可用的 loader 列表,选哪个版本的规则见加载器版本怎么选。
- 取 profile——按 MC 版本 + loader 版本请求 profile,得到一份「不完整的版本 JSON」。接口和返回结构见下一节。
- 与原版合并——
MergeVersions把 mainClass、库、game/jvm 参数叠到原版上,合并结果落成一个独立版本,不再保留inheritsFrom。 - 下载内容——与装原版共用
InstallVersionContentAsync:client jar、库(包括 Fabric 自己的 Maven 库)、资源、natives,全部进同一个下载批次。
产物落在 versions/<id>/,<id> 是 profile 给的加载器版本 id,形如 fabric-loader-0.16.9-1.20.1。装第二份时 UniqueVersionId 会把它改成 fabric-loader-0.16.9-1.20.1-2。
后面几节展开这五步里真正容易出错的地方:profile 怎么来、合并时参数为什么必须插到最前、Maven 库为什么不能只认 Mojang 那种形状。
profile 从哪来
https://meta.fabricmc.net/v2/versions/loader/{mcVersion}/{loaderVersion}/profile/json
https://meta.quiltmc.org/v3/versions/loader/{mcVersion}/{loaderVersion}/profile/json返回的是 launcher-meta 格式,结构上就是一份「不完整的版本 JSON」:
{
"id": "fabric-loader-0.16.9-1.20.1",
"inheritsFrom": "1.20.1",
"mainClass": "net.fabricmc.loader.impl.launch.knot.KnotClient",
"arguments": { "game": [], "jvm": ["-DFabricMcEmu= net.minecraft.client.main.Main "] },
"libraries": [
{ "name": "net.fabricmc:fabric-loader:0.16.9", "url": "https://maven.fabricmc.net/" },
{ "name": "net.fabricmc:intermediary:1.20.1", "url": "https://maven.fabricmc.net/" }
]
}inheritsFrom 指向原版版本号——profile 自己没有 client jar、没有资源、没有原版那些库,它只是一层「叠加」。
怎么和原版合并
先把原版版本 JSON 读出来,再用 MergeVersions 合并:
public static MinecraftVersion MergeVersions(MinecraftVersion parent, MinecraftVersion child)
{
parent.MainClass = string.IsNullOrEmpty(child.MainClass) ? parent.MainClass : child.MainClass;
parent.Libraries.AddRange(child.Libraries);
if (child.Arguments is not null)
{
parent.Arguments ??= new VersionArguments();
parent.Arguments.Game.AddRange(child.Arguments.Game);
parent.Arguments.Jvm.InsertRange(0, child.Arguments.Jvm);
}
parent.InheritsFrom = null;
return parent;
}规则是:mainClass 由子版本覆盖(Fabric 要用 KnotClient 而不是 net.minecraft.client.main.Main),库追加,game 参数追加。
Jvm 参数必须插到最前
parent.Arguments.Jvm.InsertRange(0, child.Arguments.Jvm) —— 子版本的 JVM 参数插到原版参数之前,不是追加到后面。
AddRange 会把 Fabric 的参数排到原版那一长串 -D 参数之后。原版参数里带 --add-opens/-Djava.library.path 这类开关,同名开关 JVM 取最后一个,所以插在后面等于被原版覆盖。放到最前,加载器的参数才先生效。
同理,mainClass 也必须是子版本的值——net.fabricmc.loader.impl.launch.knot.KnotClient。
合并完的 JSON 直接写成一个独立版本(不是 inheritsFrom 链),落在 versions/<id>/<id>.json,然后走和原版一样的 InstallVersionContentAsync 下 jar、库、资源。
两条库来源
这是最容易踩的地方。profile 里的库条目没有 downloads.artifact,只有 Maven 坐标:
{ "name": "net.fabricmc:fabric-loader:0.16.9", "url": "https://maven.fabricmc.net/" }而 Mojang 版本 JSON 里的库是另一种形状:
{
"name": "com.mojang:logging:1.1.1",
"downloads": { "artifact": { "url": "...", "sha1": "...", "size": 21434, "path": "com/mojang/logging/1.1.1/logging-1.1.1.jar" } }
}只认后者的话,fabric-loader、intermediary、sponge-mixin 这些 jar 既不会被下载,也不会进 classpath——启动时表现为「找不到主类」(net.fabricmc.loader.impl.launch.knot.KnotClient 找不到)。
所以 MavenArtifacts.Resolve 把两种来源归一:
public static DownloadableFile? Resolve(Library lib)
{
if (lib.Downloads?.Artifact is { Url: not "" } artifact) return artifact;
if (string.IsNullOrWhiteSpace(lib.Name) || string.IsNullOrWhiteSpace(lib.Url)) return null;
if (!TryBuildPath(lib.Name, out var path)) return null;
...
}坐标到路径的转换是 Maven 标准布局:
group:artifact:version[:classifier][@ext]
→ group/artifact/version/artifact-version[-classifier].extnet.fabricmc:fabric-loader:0.16.9 → net/fabricmc/fabric-loader/0.16.9/fabric-loader-0.16.9.jar,落到 .minecraft/libraries/ 下。启动时拼 classpath 也用同一个 Resolve(GameLauncher 里),所以装的时候和跑的时候解析规则一致,不会出现「装了但 classpath 里没有」。
同一套逻辑也管着 classpath
GameLauncher 拼 classpath 时用的是 version.Libraries.Select(MavenArtifacts.Resolve),然后过滤掉不存在的文件并保序去重(NeoForge 的版本 JSON 有重复库条目)。安装和启动共用一个解析函数,是这类「两处各写一遍」最容易出 bug 的地方。
Quilt 的差异
Quilt 的 meta 是 v3,profile 结构和 Fabric v2 同构,只是库坐标换成 org.quiltmc:*,仓库也换成 https://maven.quiltmc.org/repository/release/:
{
"name": "org.quiltmc:quilt-loader:0.24.0",
"url": "https://maven.quiltmc.org/repository/release/"
}它的 profile 里有一条 Fabric 没有的 org.quiltmc:hashed:1.20.1——这是 Quilt 自己的映射产物,替代 Fabric 的 intermediary。它同样靠 Maven 坐标解析,所以走同一条路。
另外注意 profile 里 arguments 只有 game,没有 jvm。合并时 child.Arguments 非 null 所以会进分支,但 Jvm 是空列表,InsertRange(0, 空) 不影响任何东西。
副本场景
装完的目录名是版本 id。同名版本要装第二份时,UniqueVersionId 会给出 1.20.1-2:
var id = targetId ?? UniqueVersionId(dotMc, profile.Id);合并后的 JSON 里 id 直接写成新 id,所以 Fabric 副本天然自洽。但原版安装走的是另一条路——它把 Mojang 下来的 JSON 原样落盘,里面的 id 还是 1.20.1,需要 InstanceLayout.PatchJsonId 改写:
if (!string.Equals(version.Id, versionId, StringComparison.Ordinal))
InstanceLayout.PatchJsonId(jsonPath, versionId);PatchJsonId 顺手在 JSON 里记一份 starcloudBaseVersion,把原版版本号留下来——整合包实例没有 inheritsFrom 字段,不记的话改名之后就没法知道它是哪个 MC 版本了。详见版本隔离与实例档案。
别只看一种库来源
调试「找不到主类」时,先看 versions/<id>/<id>.json 里的 libraries 有没有 fabric-loader 条目,再看 .minecraft/libraries/net/fabricmc/fabric-loader/<版本>/ 下的 jar 是否存在。两处缺一,都是 MavenArtifacts.Resolve 这一层没走通。