Skip to content

安装 Fabric 与 Quilt ​

Fabric 和 Quilt 的安装逻辑完全一样,差别只在 meta 服务器和 API 版本号。协议是同一套,我们复用了同一条合并管线。

安装流程 ​

装一个加载器版本分五步,Quilt 走的是同一条路:

  1. 确定原版基底——加载器的 profile 只是一层叠加,没有 client jar、没有资源、没有原版那些库,这些全部要由原版提供。所以合并之前,必须先拿到原版版本 JSON。
  2. 取加载器版本——从 meta 拉该 MC 版本可用的 loader 列表,选哪个版本的规则见加载器版本怎么选。
  3. 取 profile——按 MC 版本 + loader 版本请求 profile,得到一份「不完整的版本 JSON」。接口和返回结构见下一节。
  4. 与原版合并——MergeVersions 把 mainClass、库、game/jvm 参数叠到原版上,合并结果落成一个独立版本,不再保留 inheritsFrom。
  5. 下载内容——与装原版共用 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」:

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 合并:

csharp
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 坐标:

json
{ "name": "net.fabricmc:fabric-loader:0.16.9", "url": "https://maven.fabricmc.net/" }

而 Mojang 版本 JSON 里的库是另一种形状:

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 把两种来源归一:

csharp
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].ext

net.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/:

json
{
  "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:

csharp
var id = targetId ?? UniqueVersionId(dotMc, profile.Id);

合并后的 JSON 里 id 直接写成新 id,所以 Fabric 副本天然自洽。但原版安装走的是另一条路——它把 Mojang 下来的 JSON 原样落盘,里面的 id 还是 1.20.1,需要 InstanceLayout.PatchJsonId 改写:

csharp
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 这一层没走通。

相关链接:安装原版、加载器版本怎么选、版本隔离与实例档案

基于 MIT 许可证(有修改)开源