Skip to content

启动参数怎么构建 ​

参数构建集中在 GameLauncher.BuildArgsCore,桌面命令行和安卓进程内 JVM 启动共用这一份逻辑——安卓侧只是通过 BuildOverrides 换掉平台名、natives 目录和 classpath。

变量替换 ​

版本 JSON 里的参数写成 ${auth_player_name} 这样的占位符,构建时统一替换:

csharp
string Substitute(string s) =>
    Regex.Replace(s, @"\$\{([a-zA-Z0-9_]+)\}",
        m => vars.TryGetValue(m.Groups[1].Value, out var v) ? v : m.Value);

变量表二十多项,按用途分四类:

  • 身份:auth_player_name、auth_uuid、auth_access_token、auth_session(token:<token>:<uuid>)、auth_xuid、user_type
  • 路径:game_directory(GameRoot)、assets_root、game_assets、assets_index_name、natives_directory、library_directory、classpath
  • 客户端参数:version_name、version_type(默认 release)、launcher_name(Starcloud)、launcher_version
  • 窗口:resolution_width / resolution_height(默认 1280×720)

clientid 固定写 starcloud。user_properties 在有本机自定义皮肤时填一段 JSON(把回环皮肤服务的 URL 塞进 profile 的 textures 属性),没有皮肤时是 {}。

未知变量原样保留

查不到变量时返回 m.Value——把 ${foo} 原封不动塞进命令行。Java 会把它当成一个普通参数,报 "Unrecognized option"。好处是拼错时能一眼看出来而不是变成空串;代价是错误信息离根因远。

auth_access_token 是明文注入到命令行里的,所以后面讲日志时会强调它不落盘。

JVM 参数的完整顺序 ​

jvmArgs 列表是逐段追加的:

csharp
var jvmArgs = new List<string>
{
    "-Xms" + options.MinMemoryMb + "M",
    "-Xmx" + options.MaxMemoryMb + "M",
    $"-Djava.library.path={nativesDir}",
    "-Dorg.lwjgl.opengl.Display.allowSoftwareOpenGL=true",
};
if (options.UseBuiltinJvmArgs) jvmArgs.AddRange([...G1GC 那组...]);
jvmArgs.AddRange(options.ExtraJvmArgs.Where(a => !string.IsNullOrWhiteSpace(a)));
// authlib-injector:第三方登录注入
if (options.AgentJarPath is not null && options.AgentBase is not null)
    jvmArgs.Add($"-javaagent:{options.AgentJarPath}={options.AgentBase}");
// 版本 JSON 的 arguments.jvm(含 -cp)
foreach (var item in args.Jvm.Where(i => RulesAllowValue(i, ov?.OsName)))
    AppendValue(jvmArgs, item, Substitute);

三段分开:启动器自己的 → 用户的 → 版本 JSON 的。用户的 ExtraJvmArgs 夹在中间,意味着他可以覆盖启动器的默认(比如用 -XX:+UseZGC 替掉 G1),但版本 JSON 的参数优先级最低。

-Djava.library.path 指向 versions/<实例>/natives——Windows 的 LWJGL native 就在这里。

G1GC 那组参数分别管什么 ​

UseBuiltinJvmArgs 打开时追加八个参数:

csharp
"-XX:+UseG1GC",
"-XX:+UnlockExperimentalVMOptions",
"-XX:G1NewSizePercent=20",
"-XX:G1ReservePercent=20",
"-XX:MaxGCPauseMillis=50",
"-XX:G1HeapRegionSize=32M",
"-Dsun.rmi.dgc.server.gcInterval=2147483646",
"-Dfile.encoding=UTF-8",
  • +UseG1GC 选 G1 垃圾收集器。G1 把堆切成 region 收集,是现代 JVM 的默认;对 Minecraft 这种"大量短命对象"的负载,它比 Parallel GC 的停顿表现更稳。
  • +UnlockExperimentalVMOptions 解锁实验性参数开关。没有这行,后面三个 G1 参数会被 JVM 直接拒绝启动——所以它必须排在 G1NewSizePercent 前面。
  • G1NewSizePercent=20 调节年轻代初始占比,让 G1 更早开始回收而不是把 Eden 撑到很大。
  • G1ReservePercent=20 保留 20% 堆空间给 Humongous 对象(大数组、大 byte[])。老版本默认 10%,MC 的资源加载容易产生巨型对象,调高能减少 to-space exhausted。
  • MaxGCPauseMillis=50 是目标停顿,不是硬保证。G1 会为了达到 50ms 而更频繁地回收。
  • G1HeapRegionSize=32M 固定 region 大小。region 越大,G1 越能容忍大对象,但年轻代划分的粒度也越粗。
  • sun.rmi.dgc.server.gcInterval 把 RMI 的分布式垃圾回收周期拉到一个极大值(≈68 年)。MC 客户端用不上 RMI,这行是为了消掉那条周期性 GC 线程的日志噪音。
  • file.encoding=UTF-8 固定默认编码。中文路径或资源包出乱码时,第一个要怀疑的就是它。

新旧两代参数格式 ​

Minecraft 1.13 是分水岭。

1.13 之前只有 minecraftArguments,一个整字符串,空格分隔:

csharp
gameArgs.AddRange(version.MinecraftArguments
    .Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(Substitute));

1.13 及以后改成 arguments.game / arguments.jvm 两个数组,每一项是带 rules 的对象,值可以是字符串也可以是字符串数组:

csharp
foreach (var item in args.Game.Where(i => RulesAllowValue(i, ov?.OsName)))
    AppendValue(gameArgs, item, Substitute);

两代要同时处理:现代版本 JSON 里通常只有 arguments,旧版本只有 minecraftArguments,但也有整合包 JSON 两者混着写。

还有一个兜底:现代版本如果没有 minecraftArguments 字段,且 gameArgs 里找不到 --username,说明它缺了一整段必需参数(多半是手写或裁剪过的 JSON),这时补齐 --username / --version / --gameDir / --assetsDir / --assetIndex / --uuid / --accessToken / --userType / --versionType。

authlib-injector 必须在 -cp 之前 ​

第三方(Yggdrasil)账户需要 authlib-injector 改写认证请求,注入方式是 -javaagent::

csharp
// authlib-injector:第三方(Yggdrasil)外置登录注入,必须出现在 -cp 之前
jvmArgs.Add($"-javaagent:{options.AgentJarPath}={options.AgentBase}");

顺序错了 agent 不生效

-javaagent 必须在 -cp 之前。JVM 按顺序处理 classpath 与 agent:如果 classpath 里已经有类被加载,agent 的 premain 织入就来不及了,第三方登录会静默走到官方认证然后失败——表现是"提示登录成功但进不了服"。

代码里 -javaagent 追加在"版本 JSON 的 arguments.jvm"之前,而 -cp 来自 arguments.jvm,所以顺序天然是对的。但如果你自己重排 jvmArgs,先确认这一点。

缺 -cp 的兜底 ​

有些版本 JSON(尤其是 1.13 之前)根本没有 arguments.jvm,classpath 得启动器自己给:

csharp
if (!jvmArgs.Any(a => a.Equals("-cp", ...) || a.Equals("-classpath", ...)))
{
    jvmArgs.Add("-cp");
    jvmArgs.Add(classpath);
}

缺 -cp = 找不到主类

没有 -cp 的话 JVM 找不到 net.minecraft.client.main.Main,直接抛 Could not find or load main class。这个兜底覆盖两种情况——整个 arguments.jvm 缺失,或者 arguments.jvm 存在但里面没有 -cp——两种都安全。

窗口参数只对 ≥1.13 注入 ​

csharp
public static bool ModernWindowArgs(string versionId)
{
    var m = Regex.Match(versionId, @"^1\.(\d+)");
    return !m.Success || int.TryParse(m.Groups[1].Value, out var minor) && minor >= 13;
}

--width / --height / --fullscreen 只在 1.13 之后注入。旧版本客户端的 Main 不认识这些开关,多余的未知参数会让它启动即崩——所以宁可不注入,也不用"试试看"的写法。

!m.Success 的分支是给快照版和特殊命名用的:解析不出 1.x 格式就当现代版处理。

相关链接:classpath 与继承链 · 进程与日志 · 从点击到游戏起来 · 第三方认证

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