从点击到游戏起来
一次启动要串起四个阶段:环境体检 → 文件预检 → 拉起进程 → 日志重定向。它们在 HomePage.AcquireAndLaunchAsync 里是顺序执行的,每一步都可能中止。
环境体检:不联网,只看本地
EnvironmentCheckService.CheckAsync 在启动前跑一遍轻量检查,全程不联网、不算 SHA1,因为体检的目的是快,不是验完整性(那是预检的活)。它产出 EnvLevel 三档:
public enum EnvLevel { Ok, Warn, Block }Block 会弹窗让用户决定是否继续(磁盘剩余 < 512 MB、GameRoot 不可写、内存 Min>Max 或 Max<512、指定的 Java 不存在),Warn 只提示不阻断(路径含非 ASCII 字符、路径 > 200 字符、Max 内存超物理内存 80%、模组 > 150 个)。
两处刻意的口径:
- 非 ASCII 只警告不阻断。CJK 汉字、CJK 标点、全角字符都被显式放行,只有其它非 ASCII 才提示——因为游戏路径里出现西里尔字母这类字符时 Java 的默认编码容易出问题,但真出现概率极低,不值得为此拒绝启动。
- 可写性用写临时文件判定,不是查目录属性。
Directory.CreateDirectory+ 写.starcloud-write-test-<guid>.tmp+ 删除,失败就是Block。属性位在网络盘和容器挂载上经常骗人。
体检项多数带 AutoFixable 和 FixHint(「恢复推荐值」「套用智能内存推荐值」「下调到推荐值」),UI 上的"一键修复"就是调它。修完会重新走一遍启动,所以内存档位用的是闭包里重新解析出来的 options,不是体检前的那份。
防双开:我们不再互斥
这是产品上改过的地方。早期版本同版本只允许跑一局,现在同一 .minecraft 下同版本也允许多开,GameSessionGuard 从"锁"退化成"注册表":
// 注册表只做「谁在跑」的事实记录,不再承担互斥(多开是产品能力)。唯一还保留互斥语义的地方是危险操作避让(自更新、删实例目录):IsAnyGameAlive(dotMc) 为真时这些操作会拒绝执行,因为它们会破坏正在运行的进程正在用的文件。
文件预检:只查不动
LaunchPreflightService.CheckAsync 检查四类东西,全部按 SHA1 校验(资源只查存在性,因为资源对象能到数 GB,做内容校验会拖垮启动):
| Kind | 检查什么 | 校验方式 |
|---|---|---|
jar | 客户端 jar | SHA1 |
lib | 库文件 | SHA1 |
natives | 当前平台 classifier 的 native 压缩包 | SHA1 |
index | 资源索引 | 存在 + 能解析 |
asset | 索引里枚举出的每个资源对象 | 仅存在 |
客户端 jar 的定位口径和启动器完全一致,都走 GameLauncher.ResolveClientJar,不然会出现"预检说 jar 在,启动却说 jar 不在"的分裂。
版本 JSON 坏了就是全瞎
LoadVersionAsync 抛 FileNotFoundException / JsonException / InvalidOperationException 时,预检直接返回,不再往下查。因为库清单、资源索引、下载 URL 全都从 JSON 里来,JSON 没有就无从检查。
此时 VersionJsonBroken = true,并且会去官方清单里找这个版本的 JSON 下载地址填 ManifestJsonUrl。找不到(比如 Fabric 这类不在官方清单里的)CanAutoRestoreJson 就是 false,UI 直接报"版本 JSON 缺失或损坏,无法自动恢复,请到下载页重新安装"。
补全:能自动修的自动修
EnsureReadyAsync 拿到 PreflightReport 后按顺序做事:
- JSON 恢复(仅 vanilla)。从清单 URL 重新下一份,然后重新跑一遍完整预检——之前那一轮因为 JSON 缺失什么都没查出来。
- 先删损坏文件。这一步是必须的:批量下载会按"文件已存在"跳过,一个体积正常但内容损坏的 jar 会永远补不上。
- 构造下载任务:jar 走
downloads.client,库和 natives 走MavenArtifacts.Resolve反查路径匹配,资源按 hash 从resources.download.minecraft.net/<前两位>/<hash>拉。 - 批量下载,复用下载引擎的并发 + 重试 + 下载后 SHA1 校验。
- 解压 natives 到
versions/<id>/natives/。
某个文件重试穷尽仍不可用时抛 PreflightException,带失败文件清单。UI 的处理是弹窗列出前 5 个,用户可以选"仍然启动"(带着缺失文件继续)或取消。
崩溃了能不能自动修
这是 GameDiagnosticsService 的活,和预检不同:诊断是扫全盘(versions/ 下每个目录),预检是查一个版本。
RepairAsync 的分派逻辑:
const string fabricPrefix = "fabric-loader-";
if (versionId.StartsWith(fabricPrefix, ...))
{
// 按 loader 版本重装:把 id 拆回「loader 版本 - MC 版本」
await _installer.InstallFabricAsync(string.Join('-', tail[1..]), tail[0], ...);
}
if (versionId.Contains("forge", ...))
throw new InvalidOperationException(
"Forge/NeoForge 版本暂不支持自动修复,请到「下载」页重新安装");
// 原版:从官方清单重装
await _installer.InstallVanillaAsync(versionId, entry.Url, ...);Forge/NeoForge 不支持自动修复
这不是偷懒。Forge 的版本 JSON 是一套安装器生成的产物,重装它等于重跑一遍 Forge 的安装流程,那涉及下载安装器 jar、跑它的 Java 主类、处理它的 GUI 交互——在后台静默跑这套东西风险远大于收益。引导到下载页让用户手动重装,是更诚实的选择。
诊断还会检查依赖的父版本:Fabric 实例的 inheritsFrom 指向原版,原版没装就先报"依赖版本缺失",不再往下查这个实例自己的文件——父版本缺了,子版本的文件检查没有意义。
拉起进程
GameLauncher.Launch 收到拼好的命令行后引号感知切分、逐项进 ArgumentList,重定向 stdout/stderr,生成带 pid 的日志文件名,然后返回一个 GameSession。之后的一切(参数怎么拼、classpath 怎么算、日志怎么读)分别在这几页展开。
相关链接:启动参数怎么构建 · classpath 与继承链 · 进程与日志 · 防双开与运行态