启动参数怎么构建
参数构建集中在 GameLauncher.BuildArgsCore,桌面命令行和安卓进程内 JVM 启动共用这一份逻辑——安卓侧只是通过 BuildOverrides 换掉平台名、natives 目录和 classpath。
变量替换
版本 JSON 里的参数写成 ${auth_player_name} 这样的占位符,构建时统一替换:
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 列表是逐段追加的:
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 打开时追加八个参数:
"-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,一个整字符串,空格分隔:
gameArgs.AddRange(version.MinecraftArguments
.Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(Substitute));1.13 及以后改成 arguments.game / arguments.jvm 两个数组,每一项是带 rules 的对象,值可以是字符串也可以是字符串数组:
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::
// 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 得启动器自己给:
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 注入
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 与继承链 · 进程与日志 · 从点击到游戏起来 · 第三方认证