安装 Forge 与 NeoForge
Forge 和 NeoForge 我们不自己解析版本 JSON,而是跑官方的安装器。安装器负责算出正确的 classpath、处理器(processors)和补丁后的客户端 jar——这套逻辑太复杂,重写没有意义。
代价是必须把环境准备到官方安装器能接受的样子,这一页讲的就是这些前置条件。
构建列表从哪来
Forge 的构建列表由两个来源合成。
一是全量构建,来自 maven 元数据:
https://maven.minecraftforge.net/net/minecraftforge/forge/maven-metadata.xml这是标准 Maven 元数据,<version> 标签里是构建号,形如 1.20.1-47.4.28。我们按 <mcVersion>- 前缀过滤,再排序。
二是推荐/最新标记,来自:
https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json它是一个扁平的 promos 字典:
{
"promos": {
"1.20.1-recommended": "47.4.28",
"1.20.1-latest": "47.4.28",
"1.8-recommended": "11.14.4.1563",
"1.8-latest": "11.14.4.1577"
}
}注意 recommended 和 latest 经常不是同一个。1.8 就是:最新是 11.14.4.1577,推荐是 11.14.4.1563。推荐的含义是「Forge 团队认为稳的那个」,最新可能刚推出来。所以界面上我们把它们标成两个不同的记号,推荐卡优先取 推荐,没有才退回 最新:
private static LoaderBuild? RecommendedOf(LoaderKind kind, List<LoaderBuild> builds) => kind switch
{
LoaderKind.Forge => builds.FirstOrDefault(b => b.Note == "推荐")
?? builds.FirstOrDefault(b => b.Note == "最新"),
_ => builds.FirstOrDefault(),
};构建号必须按点号判定
早先我们用 int.TryParse 判断构建号是否合法,结果现代构建号 47.4.5 解析失败,整个列表恒为空。
正确的判法是「首位是数字,其余字符只能是数字/字母/点/连字符」:
private static bool IsForgeBuildSuffix(string s)
{
if (s.Length == 0 || !char.IsAsciiDigit(s[0])) return false;
foreach (var c in s)
if (!char.IsAsciiDigit(c) && !char.IsAsciiLetter(c) && c != '.' && c != '-') return false;
return true;
}这样现代的 47.4.5 和旧的 10.13.4.1614-1.7.10(带 -1.7.10 后缀)都能通过。
排序也不能用字符串比较。CompareForgeBuilds 忽略 - 之后的后缀,逐段按数字比,段数多的更新:
static int[] Key(string v) =>
v.Split('-')[0].Split('.').Select(p => int.TryParse(p, out var n) ? n : 0).ToArray();注意 maven-metadata.xml 里的顺序不保证按时间,必须自己排。
NeoForge 的构件位置
NeoForge 的构建列表走另一套 API:
https://maven.neoforged.net/api/maven/versions/releases/net/neoforged/{artifact}artifact 是 forge 还是 neoforge,取决于 MC 版本:
- 1.20.1 的产物发布在 legacy 的
forge构件下,版本号形如1.20.1-47.1.106 - 其余版本在
neoforge构件下,版本号形如21.1.77(1.x 时代)或26.2.0.57(新命名)
前缀规则:
private static string? NeoForgePrefix(string mcVersion)
{
if (mcVersion == "1.20.1") return "1.20.1-47.1.";
var seg = mcVersion.StartsWith("1.", StringComparison.Ordinal) ? mcVersion[2..] : mcVersion;
...
return $"{seg}.";
}1.x 时代构件版本号以 MC 版本号开头(1.21 → 21.0.、1.20.4 → 20.4.),要剥掉前缀;2026 年起 NeoForge 直接以 MC 版本作为构件版本前缀(26.2 → 26.2.0.57),不用剥。剥的长度在 ListNeoForgeBuildsAsync 里算:
var skip = legacy ? mcVersion.Length + 1 : oldNaming ? prefix!.Length : 0;安装器直链同样分两种:
return mcVersion == "1.20.1"
? $"https://maven.neoforged.net/releases/net/neoforged/forge/{mcVersion}-{ver}/forge-{mcVersion}-{ver}-installer.jar"
: $"https://maven.neoforged.net/releases/net/neoforged/neoforge/{ver}/neoforge-{ver}-installer.jar";build 参数如果带了 MC 前缀会被自动剥离,所以界面上传什么都能拼对。beta 版标成 Beta,第一个非 beta 标成 最新稳定版。
跑安装器
下载到临时目录后执行:
psi.ArgumentList.Add("-jar");
psi.ArgumentList.Add(installerPath);
psi.ArgumentList.Add("--installClient");
psi.ArgumentList.Add(settings.DotMinecraft);--installClient 会把游戏目录作为参数传进去,安装器在里面写入 versions/<id>-forge-<build>/(含版本 JSON、处理器产物、库文件)和 libraries/ 下的依赖。
Java 大版本从原版版本 JSON 的 javaVersion.majorVersion 取(1.20.1 是 17,1.16.5 是 8)。本地没有满足的运行时时,EnsureJavaAsync 会自动从 Adoptium 下一个。
安装器的 stdout 逐行读出来当进度显示,stderr 留着在失败时拼进异常信息:
if (p.ExitCode != 0)
{
var err = await stdErrTask;
throw new InvalidOperationException(
$"安装器执行失败(exit {p.ExitCode}):{(err.Length > 0 ? err : lastLine)}");
}必须先预置 launcher_profiles.json
Forge 官方安装器启动后第一件事是读游戏目录里的 launcher_profiles.json。文件不存在就直接报
There is no minecraft launcher profile...然后退出码 1 退出。
PCL 和 HMCL 都在跑安装器前自动补这个文件,我们也这么做:
public static bool EnsureLauncherProfiles(string dotMinecraft)
{
var path = Path.Combine(dotMinecraft, "launcher_profiles.json");
if (File.Exists(path)) return false; // 只缺时才建,不覆盖用户已有的配置
...
File.WriteAllText(path, json, new System.Text.UTF8Encoding(false));必须是 UTF-8 无 BOM。Forge 用 Gson 解析,带 BOM 会直接解析失败——这是另一个更隐蔽的失败模式:不报「没有 profile」,而是抛一个 JSON 解析异常。
内容用最小合法结构即可,官方启动器读得懂,空 profiles 不影响启动:
{
"profiles": {},
"settings": {
"enableSnapshots": false,
"keepLauncherOpen": false,
"showGameLog": false,
"soundOn": false
},
"version": 3
}别忘了原版必须先装
Forge 安装器要读原版的版本 JSON 才能算出完整的 classpath。所以流程永远是「先装原版,再装 Forge」。我们把这条约束写进了 UI 的安装流程:原版装完才走加载器分支。
Forge 装出来的实例目录名是 <mc>-forge-<build>(NeoForge 是 <mc>-neoforge-<build>),和原版目录并列,不会覆盖原版。