McLite:轻量 Minecraft 启动器完整实现与使用指南

从项目定位、架构设计、核心实现到安装使用与故障排查,一篇完整的 McLite 文档。

McLite 是面向 Windows 的 Minecraft Java 版启动器。它把“下载游戏、准备 Java、安装加载器、导入整合包、启动游戏”串成一条完整链路,界面使用 Electron,核心逻辑是纯 Node.js。

项目不依赖预先安装 Java,也不要求用户手动整理 .minecraft。启动器会从 Mojang 获取版本清单与游戏文件,在本机寻找匹配的 Java,必要时下载 Mojang 官方运行时;安装加载器时调用 Fabric / Quilt 的 profile 接口,或运行 Forge / NeoForge 安装器。

McLite 项目封面
McLite:轻量、完整的 Minecraft Java 启动器

Table of Contents

它解决什么问题

一个“能启动”的启动器,实际上要处理很多碎片:

  • Minecraft 版本 JSON 有继承关系,库文件还有平台规则和原生库;
  • 不同加载器的安装方式完全不同,Fabric 给 profile,Forge 要跑安装器;
  • 资源索引、资源对象、日志配置分散在不同地址;
  • 国内网络访问 Mojang、Forge Maven、Modrinth 时经常超时;
  • 整合包既有 Modrinth 的 .mrpack,也有 CurseForge 的 manifest.json;
  • PCL、HMCL、官方启动器已经在磁盘上留下了版本,用户不想重新下载;
  • 启动失败时,真正的原因往往埋在几十行 Java 堆栈里。

McLite 的选择是:把这条链路做完整,同时保持结构简单、可读、可改造。核心模块不依赖 Electron,可以单独测试;界面只负责展示状态和触发 IPC。

功能一览

模块 能力
版本安装 Vanilla、Fabric、Quilt、Forge、NeoForge
游戏文件 客户端 JAR、库、资源索引、全部资源对象、日志配置
Java 运行时 检测本机 Java;缺失时从 Mojang 运行时清单下载对应主版本
多版本实例 一个实例包含多个版本,可切换当前版本、单独启动或删除
版本隔离 每个版本独立的 mods、config、saves 等目录,兼容 HMCL / PCL 结构
原版缓存 把原版 JSON 与客户端 JAR 压缩为 ZIP,供多个实例复用
实例扫描 自动发现其他 .minecraft 中已安装的版本,最多添加 24 个额外目录
整合包导入 Modrinth .mrpack、CurseForge .zip,自动解析版本与加载器
在线整合包 Modrinth、CurseForge(MCIM 国内镜像)、MC百科
整合包导出 将当前版本导出为 Modrinth .mrpack
账户 本地离线账户、Microsoft 正版设备代码登录
网络 代理、重试、SHA1 / 大小校验、BMCLAPI / MCIM 镜像回退
启动调试 完整 JVM 参数、Classpath、版本 JSON、stdout / stderr 日志
问题诊断 根据日志识别 Java、内存、模组、图形、网络、权限等常见错误

技术栈

层 技术 说明
桌面容器 Electron 45 主进程 + 渲染进程,启用 contextIsolation 与 sandbox
核心运行时 Node.js 20+ 文件、进程、网络、加密全部使用标准库
HTTP 原生 fetch + undici 默认直连;配置代理时使用 ProxyAgent
压缩包 adm-zip 读取 .mrpack / CurseForge .zip,写入原版缓存与导出包
打包 electron-builder Windows portable target
测试 node --test test/core.test.js
界面 原生 HTML / CSS / JavaScript 无前端框架,降低构建复杂度

六条设计原则

核心逻辑与界面解耦

src/core/ 下的模块不引用 Electron,只依赖 Node 标准库与 undici、adm-zip。main.js 负责把它们编排成 IPC 命令,renderer.js 只消费状态。

这样做的好处是:

  • 核心逻辑可以用 node --test 直接测试,不需要启动窗口;
  • 更换 UI 框架时,不需要重写安装与启动逻辑;
  • 排查问题时,可以单独调用 installLoader()、resolveVersion() 等函数。

与 PCL / HMCL 共存

游戏目录采用标准 .minecraft 结构:

.minecraft
├── assets/
├── libraries/
├── runtime/
├── versions/
│   ├── <实例版本>/          # 每个实例版本独立目录
│   └── <基础版本>/          # 共享的 Vanilla / 加载器版本
└── mclite/                  # McLite 私有数据
    ├── instances/
    └── cache/

实例元数据放在 .minecraft/mclite/instances/,不污染 versions/ 的标准结构。其他启动器仍能扫描到 McLite 安装的版本;McLite 也能扫描其他启动器的版本。

文件优先,先校验再落盘

所有下载先写入 .part 临时文件,校验大小与 SHA1 后再原子重命名。已存在且大小匹配的文件直接跳过,失败重试不会留下半截文件。

失败可恢复

  • 网络失败按指数退避重试;
  • Mojang / Forge Maven 连接失败时自动尝试 BMCLAPI;
  • 整合包在线下载失败时改用官方 CDN;
  • 安装或导入失败时回滚本次创建的版本记录与别名目录;
  • 原版文件缺失时优先从压缩缓存恢复。

安全默认值

  • 渲染进程 nodeIntegration: false、contextIsolation: true、sandbox: true;
  • 页面 CSP 禁止外部脚本、表单提交和外部导航;
  • 所有来自压缩包或版本 JSON 的路径都经过 safeJoin(),阻止目录穿越;
  • 启动日志中的访问令牌替换为 ***;
  • Microsoft 令牌使用 Electron safeStorage 加密后保存。

中文优先的轻量界面

界面只使用原生 HTML / CSS / JavaScript,没有前端框架和运行时依赖。浅色主题、中文文案、任务进度与日志面板都围绕“安装 / 启动”这两个高频动作设计。

界面预览

首页
首页:没有实例时引导新建或导入整合包
新建实例
新建实例:选择 Minecraft 版本、加载器,默认开启版本隔离
在线整合包
在线整合包:Modrinth、CurseForge 与 MC百科
实例库
实例库:一个实例可以保存多个版本
原版缓存
原版缓存:原版 JSON 与客户端 JAR 的压缩缓存
设置
设置:账户、Java、内存、代理、镜像与调试日志

一次完整的用户路径

  1. 启动启动器:默认使用 exe 同级的 .minecraft;如果目录不可写,回退到 %APPDATA%\McLite\.minecraft。
  2. 选择账户:首次启动自动创建本地离线账户;也可以配置 Microsoft Client ID 登录正版。
  3. 新建实例:选择 Minecraft 版本与加载器。启动器下载原版基础文件,安装加载器,创建别名版本与隔离目录。
  4. 启动游戏:补全缺失文件,选择 Java,解压原生库,拼装 JVM / 游戏参数,用 javaw.exe 启动。
  5. 导入整合包:拖入 .mrpack 或 CurseForge .zip,启动器解析版本、安装加载器、下载模组、解压覆盖文件。
  6. 出问题时:打开调试日志,或点击“诊断”,根据规则匹配结果逐项排查。
安装流程
从零安装一个实例:原版基础 → 加载器 → 别名版本 → 隔离目录
启动流程
一次启动发生了什么:账户、文件、继承、Java、参数、进程与日志

已验证的环境

开发过程中实际完成过以下端到端测试(安装文件并启动到游戏主界面):

  • Minecraft 1.6.4(旧版虚拟资源)
  • Minecraft 1.7.10
  • Minecraft 1.12.2 + Forge 14.23.5.2859
  • Minecraft 1.20.1 Vanilla / Fabric / Forge 47.4.x
  • Minecraft 1.21.1 + NeoForge 21.1.249(在无法直连 NeoForge Maven 的网络中,通过镜像预下载完成安装)
  • Minecraft 26.3(Java 25)
  • Modrinth Forge 整合包导入、覆盖文件解压、模组下载与启动
  • Modrinth 在线搜索、下载并导入 Fabulously Optimized(48 个模组,导出包约 42 MB)
  • 将导入的整合包导出为 .mrpack 并重新校验清单
  • 启动诊断规则、常见问题弹窗和中文实例名创建
  • 原版版本压缩缓存、删除 / 恢复确认界面,以及缓存恢复后的文件校验

这些结果来自开发环境,不能替代所有硬件、网络与整合包组合的验证。遇到问题时,优先查看启动日志与诊断结果。

整体架构
McLite 整体架构:渲染层、桥接层、主进程、核心服务、数据与网络

进程模型

┌────────────────────────────────────────────────────┐
│ Renderer(src/ui)                                  │
│ index.html + renderer.js + styles.css               │
│ 无 Node 权限;只能调用 window.launcher.*            │
└───────────────────────┬────────────────────────────┘
                        │ contextBridge
┌───────────────────────▼────────────────────────────┐
│ Preload(preload.js)                               │
│ 白名单 API;统一 invoke / onEvent / 错误转换         │
└───────────────────────┬────────────────────────────┘
                        │ ipcRenderer.invoke
┌───────────────────────▼────────────────────────────┐
│ Main(main.js)                                     │
│ 窗口、单实例锁、IPC 路由、任务编排、事件广播         │
└───────────────────────┬────────────────────────────┘
                        │ 直接函数调用
┌───────────────────────▼────────────────────────────┐
│ Core(src/core)                                    │
│ 元数据、版本、下载、启动、实例、整合包、账户、诊断    │
└───────────────────────┬────────────────────────────┘
                        │
             文件系统 / HTTPS / 子进程

选择这种结构的原因:

  1. 渲染进程不碰文件系统。即使界面被 XSS 注入,也无法直接读写磁盘或启动进程。
  2. 核心逻辑可测试。src/core 不依赖 Electron,测试可以直接 require。
  3. IPC 是唯一的边界。所有能力都通过 preload.js 显式暴露,新增能力必须同时修改主进程和桥接层。

目录与模块职责

main.js                 Electron 主进程:窗口、IPC、任务编排、退出清理
preload.js              contextBridge 白名单:invoke / onEvent
src/core/
  accounts.js           本地 / Microsoft 账户存储与加密
  config.js             配置读写、字段校验、默认值
  diagnose.js           日志规则与常见问题
  http.js               HTTP、代理、重试、镜像、下载、校验
  instances.js          实例组、多版本、隔离目录、别名记录
  java.js               Java 探测、Mojang 运行时安装
  launch.js             参数构建、原生库解压、进程启动与清理
  meta.js               Mojang / Fabric / Quilt / Forge / NeoForge 元数据
  modpack.js            mrpack / CurseForge 导入
  modpackCatalog.js     在线整合包搜索、MCIM、MC百科解析
  modpackExport.js      导出 .mrpack
  scanner.js            扫描其他 .minecraft
  tasks.js              任务进度、日志事件、节流
  util.js               文件、JSON、路径、版本比较等工具
  versionCache.js       原版 ZIP 缓存
  versions.js           版本合并、库规则、安装、别名兼容
src/ui/
  index.html            外壳结构
  renderer.js           页面渲染、状态、交互
  styles.css            主题与组件样式
test/core.test.js       核心单元测试
scripts/                开发启动与截图脚本

依赖方向

main.js
  ├─ meta.js        ── http.js
  ├─ versions.js    ── meta.js / java.js / versionCache.js / http.js
  ├─ launch.js      ── versions.js / java.js / instances.js
  ├─ modpack.js     ── versions.js / instances.js / modpackCatalog.js
  ├─ scanner.js     ── instances.js
  ├─ accounts.js
  ├─ microsoft.js   ── http.js
  └─ tasks.js

依赖是单向的:http.js 不知道版本,versions.js 不知道界面,main.js 不实现具体下载逻辑。这样可以避免“启动器越写越像一个巨大的 main 函数”。

IPC 约定

主进程用 handle() 包装所有通道:

function handle(channel, fn) {
  ipcMain.handle(channel, async (_event, ...args) => {
    try {
      const data = await fn(...args);
      return { ok: true, data };
    } catch (error) {
      console.error(`[IPC ${channel}]`, error);
      return { ok: false, error: errorPayload(error) };
    }
  });
}

errorPayload() 只返回 code、message,调试模式下才附带 stack:

function errorPayload(error) {
  return {
    code: error?.code || 'ERROR',
    message: error?.message || String(error),
    detail: process.env.MCLITE_DEBUG ? error?.stack || '' : '',
  };
}

Preload 把 { ok, data, error } 还原成 Promise 语义:

async function invoke(channel, ...args) {
  const response = await ipcRenderer.invoke(channel, ...args);
  if (!response?.ok) {
    const error = new Error(response?.error?.message || '操作失败');
    error.code = response?.error?.code || 'ERROR';
    error.detail = response?.error?.detail || '';
    throw error;
  }
  return response.data;
}

contextBridge.exposeInMainWorld('launcher', {
  bootstrap: () => invoke('app:bootstrap'),
  createInstance: (input) => invoke('instance:create', input),
  play: (instanceId, versionKey = '', options = {}) =>
    invoke('instance:play', instanceId, versionKey, options),
  // …
});

设计要点:

  • 每个通道只做一件事。例如 instance:create 负责安装,instance:play 负责启动,instance:delete-version 负责删除。
  • 参数使用对象。新增字段不会破坏旧调用方。
  • 错误带业务码。BUSY、VERSION_NOT_FOUND、CURSEFORGE_KEY_REQUIRED 等码让界面可以给出具体提示。

事件与任务

渲染进程不轮询进度。主进程通过 launcher:event 推送事件,preload.js 暴露订阅:

onEvent: (callback) => {
  const listener = (_event, payload) => callback(payload);
  ipcRenderer.on('launcher:event', listener);
  return () => ipcRenderer.removeListener('launcher:event', listener);
}

主要事件:

事件 触发时机 典型消费者
task 安装 / 下载 / 启动任务进度变化 活动面板、进度弹窗
log 任意阶段写入日志 活动面板日志页
instances-changed 实例创建、更新、删除、启动、退出 首页、实例库
config-changed 设置保存 设置页、全局内存显示
accounts-changed 账户新增、切换、删除、令牌刷新 侧边栏、账户弹窗
auth-result Microsoft 设备代码登录完成 / 失败 账户弹窗
game-exit 游戏进程退出 实例详情、诊断
fatal 未捕获异常 全局错误提示

TaskManager 负责把高频进度压到可接受的刷新频率:

_emit(force = false) {
  const now = Date.now();
  if (!force && now - this._lastEmit < 120) {
    if (!this._timer) {
      this._timer = setTimeout(() => {
        this._timer = null;
        this._lastEmit = Date.now();
        this.manager.broadcast({ type: 'task', task: this.snapshot() });
      }, 120);
    }
    return;
  }
  this._lastEmit = now;
  this.manager.broadcast({ type: 'task', task: this.snapshot() });
}

下载数千个资源对象时,如果每个 chunk 都发一次 IPC,界面会被消息淹没。120 ms 的节流让进度看起来连续,同时保持主进程轻量。

数据模型:实例组 + 多版本

McLite 的“实例”是一个组,组内可以有多个版本。这是本项目与很多启动器不同的地方:

实例:Fabulously Optimized
├── 版本:Fabulously Optimized(1.21.1 / Fabric)
├── 版本:Fabulously Optimized 1.20.1(1.20.1 / Fabric)
└── 版本:原版测试(1.21.1 / Vanilla)

实例记录保存在 .minecraft/mclite/instances/<id>/instance.json,核心字段如下:

{
  "id": "fabulously-optimized-a1b2c3",
  "schemaVersion": 2,
  "name": "Fabulously Optimized",
  "versions": [
    {
      "key": "v-5f2c1d9a8b7e6c",
      "name": "Fabulously Optimized",
      "mcVersion": "1.21.1",
      "loaderType": "fabric",
      "loaderVersion": "0.16.x",
      "versionId": "Fabulously Optimized",
      "baseVersionId": "fabric-loader-0.16.x-1.21.1",
      "gameRoot": "C:\\Users\\<用户名>\\Desktop\\McLite\\.minecraft",
      "versionIsolation": true,
      "alias": true,
      "installing": false,
      "broken": false,
      "modpack": {
        "kind": "modrinth",
        "name": "Fabulously Optimized",
        "version": "6.x.x",
        "partial": false,
        "warnings": []
      }
    }
  ],
  "activeVersionKey": "v-5f2c1d9a8b7e6c"
}

为什么用 versions[] 而不是“一个实例一个版本”?

  • 同一个整合包更新后,可以保留旧版本;
  • 同一实例可以同时放原版和加载器版本,方便对比;
  • 删除一个版本不会误删整个实例,除非删的是最后一个版本。

顶层的 mcVersion、loaderType、versionId 等字段是当前活动版本的镜像,用于兼容旧调用方和卡片展示;versions[] 才是唯一事实来源。

版本隔离与别名版本

隔离目录

gameDirForInstance() 决定游戏目录:

function gameDirForInstance(gameDir, instance) {
  const root = gameRootForInstance(gameDir, instance);
  if (instance && instance.versionIsolation === false) return root;
  return versionDirForId(root, instance?.versionId || instance?.mcVersion);
}
  • 开启隔离:游戏目录是 .minecraft/versions/<版本名>/,模组、配置、存档都放在这里。
  • 关闭隔离:使用共享的 .minecraft 根目录。
  • 扫描到的外部版本:如果版本目录里已经有 mods、saves、config 等,自动按隔离模式启动。

这与 HMCL / PCL 的版本隔离结构一致,因此其他启动器可以直接识别。

别名版本

安装完成后,McLite 不直接把“基础加载器版本”当作实例版本,而是创建一个别名版本:

async function createVersionAlias(gameDir, baseVersionId, desiredName) {
  const aliasId = await uniqueVersionFolder(gameDir, desiredName || baseVersionId);
  const dir = versionDirForId(gameDir, aliasId);
  await ensureDir(dir);
  const now = new Date().toISOString();
  await writeJson(path.join(dir, `${aliasId}.json`), {
    id: aliasId,
    inheritsFrom: baseVersionId,
    type: 'release',
    releaseTime: now,
    time: now,
  });
  await writeJson(path.join(dir, '.mclite-alias.json'), {
    baseVersionId,
    createdAt: now,
  });
  return { versionId: aliasId, baseVersionId, alias: true };
}

别名版本的好处:

  • 每个实例版本有独立的目录名,可以放中文名、整合包名;
  • 多个别名可以共享同一个基础版本的库与资源;
  • 删除别名只删除自己的目录,不影响其他实例;
  • 版本 JSON 只写 inheritsFrom,避免复制整份元数据。

PCL 兼容的三个细节

PCL 2.99 对版本 JSON 的要求比较严格。ensureVersionAliasCompatibility() 处理三件事:

  1. 别名 JSON 必须自带 mainClass,否则 PCL 会把它归入“错误的版本”。
  2. 写入加载器的 libraries,让 PCL 能识别加载器类型。
  3. 创建与版本同名的客户端 JAR,优先硬链接到基础 JAR,跨卷时复制。
const aliasJar = path.join(versionDir, `${id}.jar`);
if (!(await statSize(aliasJar) > 0) && resolved.jarPath) {
  try {
    await fsp.link(resolved.jarPath, aliasJar);
  } catch {
    await fsp.copyFile(resolved.jarPath, aliasJar);
  }
}

const enriched = { ...alias };
enriched.mainClass = resolved.json.mainClass;
enriched.libraries = resolved.json.libraries || [];
delete enriched.jar; // 关键:不要使用 JSON 的 jar 字段
await writeJson(aliasFile, enriched);

为什么不能用 jar 字段?

NeoForge 的启动器会把继承的原版 JAR 也放进模块路径。如果别名 JSON 用 jar 指向原版 JAR,Java 会看到两份 Minecraft,部分模组会触发:

Modules minecraft and _1._21._1 export package ... to module ...

同名 JAR 硬链接则不会进入 NeoForge 的模块路径,因为它在 -DignoreList 的覆盖范围内。启动器在每次启动时会自动修复旧版本创建的别名:删除 jar 字段,补上 mainClass、libraries 与同名 JAR。

如果 PCL 仍把版本放在“错误的版本”分组,关闭 PCL 后删除 .minecraft/PCL.ini,让它重新扫描。这个文件只是 PCL 的版本列表缓存,不包含游戏文件。

网络层:代理、镜像与校验

代理选择顺序

main.js 的 applyNetworkSettings() 按以下顺序决定代理:

  1. 设置中的“网络代理”(http://127.0.0.1:7897 这样的地址);
  2. 环境变量 HTTPS_PROXY / HTTP_PROXY / ALL_PROXY;
  3. Windows 系统代理(通过 Electron session.resolveProxy());
  4. 都没有则直连。

填写 direct 可以强制直连。

Node 原生 fetch 使用的 undici 默认不读取 HTTP_PROXY,所以只有在配置了代理时才加载 ProxyAgent:

let proxyDispatcher = null;
let proxyFetch = null;

function configureHttp(options = {}) {
  const next = normalizeProxyUrl(options.proxy);
  if (!next) return;
  const undici = require('undici');
  proxyDispatcher = new undici.ProxyAgent(next);
  proxyFetch = undici.fetch;
}

function httpFetch(url, options) {
  if (proxyDispatcher && proxyFetch) {
    return proxyFetch(url, { ...options, dispatcher: proxyDispatcher });
  }
  return fetch(url, options);
}

镜像回退

mirrorCandidates() 只对三类 Maven 主机生成 BMCLAPI 地址:

maven.neoforged.net    → https://bmclapi2.bangbang93.com/maven/...
maven.minecraftforge.net
libraries.minecraft.net

fetchResponse() 先试官方地址,遇到连接失败再试镜像。某个主机首次失败后,本次运行期间会优先尝试镜像,避免每个文件都先等一次超时:

if (!activeProxy && mirrorPreferredHosts.has(host)) {
  return [...mirrors, url];
}
return [url, ...mirrors];

下载与校验

downloadToFile() 的流程:

已有文件大小匹配? → 跳过(可选校验 SHA1)
        ↓
写入 <目标>.<pid>.part
        ↓
流式计算 SHA1 + 统计字节数
        ↓
大小 / SHA1 校验失败? → 删除临时文件,重试
        ↓
校验通过 → 原子重命名到目标路径

核心代码:

const temp = `${destination}.${process.pid}.part`;
const hash = crypto.createHash('sha1');
let sent = 0;

await pipeline(
  Readable.fromWeb(response.body),
  meter, // 统计进度、检测空闲超时、更新 SHA1
  fs.createWriteStream(temp, { flags: 'w' })
);

const actualSize = await statSize(temp);
const actualSha1 = hash.digest('hex');
if (expectedSize >= 0 && actualSize !== expectedSize) {
  throw new Error('文件大小校验失败');
}
if (expectedSha1 && actualSha1 !== expectedSha1) {
  throw new Error('文件 SHA1 校验失败');
}
await fsp.rename(temp, destination);

downloadMany() 在此之上提供:

  • 目标路径去重;
  • 已存在文件跳过;
  • 1–16 的并发上限;
  • 单个文件失败时记录日志并中止整批;
  • 镜像失败后自动改用 fallbackUrl。

安全边界

渲染进程

BrowserWindow 的关键配置:

webPreferences: {
  preload: path.join(__dirname, 'preload.js'),
  contextIsolation: true,
  nodeIntegration: false,
  sandbox: true,
  webviewTag: false,
  spellcheck: false,
}

页面 CSP:

<meta http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';
           img-src 'self' data: https:; font-src 'self' data:; connect-src 'none';
           object-src 'none'; base-uri 'none'; form-action 'none'" />

这意味着:

  • 渲染进程不能直接访问 Node、文件系统或子进程;
  • 页面不能发起网络请求,所有网络操作都在主进程;
  • 外部链接必须通过 shell.openExternal() 打开,并且只允许 http(s)。

路径穿越

整合包与版本 JSON 中的路径都不可信。safeJoin() 拒绝绝对路径、盘符路径和 .. 越界:

function safeJoin(base, relative) {
  const raw = String(relative || '').replace(/\\/g, '/');
  if (!raw || raw.startsWith('/') || /^[a-zA-Z]:/.test(raw)) {
    throw new Error(`非法文件路径: ${relative}`);
  }
  const target = path.resolve(base, raw);
  const root = path.resolve(base);
  if (target !== root && !target.startsWith(root + path.sep)) {
    throw new Error(`文件路径越界: ${relative}`);
  }
  return target;
}

令牌脱敏

启动日志会记录完整命令行,但访问令牌必须替换:

function redactArgs(args) {
  return args.map((arg, index) => {
    if (index > 0 && /^--(?:accessToken|session|auth_session|clientToken)$/i.test(args[index - 1])) {
      return '***';
    }
    if (/^--(?:accessToken|session|auth_session|clientToken)=/i.test(arg)) {
      return arg.replace(/=.*/s, '=***');
    }
    return arg;
  });
}

诊断模块 diagnose.js 也会对日志做二次脱敏,包括 Bearer 头、accessToken / refreshToken JSON 字段。

并发与状态

启动器只允许一个进程实例运行:

const gotLock = app.requestSingleInstanceLock();
if (!gotLock) app.quit();

安装、导入、启动等写操作通过 withOperation() 串行化:

function withOperation(fn) {
  return async (...args) => {
    if (busyOperation) {
      throw codedError('BUSY', '另一个安装或启动任务正在进行,请稍候');
    }
    busyOperation = true;
    try {
      return await fn(...args);
    } finally {
      busyOperation = false;
    }
  };
}

这不是为了限制并发下载(下载本身有并发控制),而是为了避免两个安装任务同时改写 versions/、libraries/ 与实例记录。

运行中的游戏用 runningGames: Set<instanceId> 跟踪。启动前检查、退出后移除;关闭启动器时统一请求游戏正常退出:

app.on('before-quit', (event) => {
  if (stoppingGamesOnQuit) return;
  if (!runningGames.size) return;
  event.preventDefault();
  stoppingGamesOnQuit = true;
  terminateLaunchedGames({ graceMs: 4000, forceWaitMs: 2000 })
    .finally(() => app.quit());
});

Windows 上先执行 taskkill /PID <pid> /T,给游戏 4 秒保存世界;仍未退出则加 /F 强制结束进程树。

错误处理

错误分三层:

层 表现 例子
核心函数 抛出带 code 的 Error codedError('LOADER_NOT_FOUND', …)
IPC 包装成 { ok: false, error } BUSY、VERSION_NOT_FOUND
界面 Toast / 弹窗 / 进度面板错误态 “另一个安装任务正在进行”

安装类任务还会把失败写入 TaskManager:

try {
  await installLoader(…);
  task.finish('安装完成');
} catch (error) {
  task.fail(error);
  // 回滚本次创建的别名与版本记录
  throw error;
}

这样即使界面只显示一句“安装失败”,活动面板里仍然保留了完整的阶段日志。

扩展点

新增加载器

需要改四个位置:

  1. src/core/meta.js
    • normalizeLoader() 接受新类型;
    • getLoaderVersions() 返回版本列表;
    • getLoaderProfile() 或安装器 URL 提供安装信息。
  2. src/core/versions.js
    • installLoader() 增加分支;
    • 如需要,新增 installXxx()。
  3. src/core/scanner.js
    • detectLoader() 能识别新加载器的库或 mainClass。
  4. src/ui/renderer.js
    • 新建实例弹窗与加载器标签增加选项。

新增整合包来源

  1. modpackCatalog.js 的 normalizeSource() 接受新来源;
  2. 实现 searchXxx() 与 resolveXxxDownload();
  3. searchModpacks() / resolveModpackDownload() 增加分支;
  4. 界面增加来源标签页。

如果来源不托管文件(像 MC百科),resolveModpackDownload() 应抛出明确的 MCMOD_MANUAL_DOWNLOAD 类错误,让界面引导用户打开下载页,而不是假装可以自动下载。

新增诊断规则

在 diagnose.js 的 RULES 中追加一条:

{
  id: 'example-error',
  title: '示例错误',
  severity: 'error',
  patterns: [/ExampleException/i, /example failed/i],
  explanation: '一句话解释原因。',
  suggestions: ['第一步排查', '第二步排查'],
  actions: ['open-logs', 'open-mods-folder'],
}

规则按数组顺序匹配,命中多条时会全部返回。evidence 会自动截取第一条命中的日志并脱敏。

版本元数据:先缓存,再回退

Mojang 的版本清单不大,但加载器元数据和版本 JSON 会被反复读取。MetaService 做了三级缓存:

数据 内存 磁盘 失败回退
版本清单 10 分钟 30 分钟 使用任意旧缓存
版本 JSON 无 按清单 SHA1 校验 使用旧缓存
加载器版本 10 分钟 无 直接报错
Java 运行时清单 60 分钟 无 直接报错
async getVersionManifest(force = false) {
  if (!force && this.manifest && Date.now() - this.manifest.fetchedAt < 10 * 60 * 1000) {
    return this.manifest.data;
  }
  const cached = force ? undefined : await this._cachedDisk('version_manifest.json', 30 * 60 * 1000);
  if (cached) return cached;

  try {
    const data = await fetchJson(VERSION_MANIFEST_URL, { retries: 3 });
    await this._saveDisk('version_manifest.json', data);
    return data;
  } catch (error) {
    const stale = await this._cachedDisk('version_manifest.json', 0);
    if (stale) return stale; // 网络失败时仍然可以列出已知版本
    throw error;
  }
}

版本 JSON 的缓存键是清单里的 sha1。如果 Mojang 更新了某个版本,缓存会自动失效,不会拿到旧 JSON。

加载器版本

  • Fabric / Quilt:请求 meta.fabricmc.net / meta.quiltmc.org 的 /versions/loader/<mc>,直接拿到版本列表。
  • Forge:读取 Maven 的 maven-metadata.xml,筛选 <mc>-<loader>,再用 promotions_slim.json 标记“推荐 / 最新”。
  • NeoForge:1.20.1 使用 net/neoforged/forge,其他版本使用 net/neoforged/neoforge;版本号前缀按 Minecraft 次版本匹配,例如 1.21.1 匹配 21.1.。
async _neoforgeVersions(mcVersion) {
  const legacy = mcVersion === '1.20.1';
  const groupPath = legacy ? 'net/neoforged/forge' : 'net/neoforged/neoforge';
  const metadata = await fetchText(`${NEOFORGE_MAVEN}/${groupPath}/maven-metadata.xml`);

  let versions = parseMavenVersions(metadata);
  if (legacy) {
    versions = versions
      .filter((v) => v.startsWith(`${mcVersion}-`))
      .map((v) => v.slice(mcVersion.length + 1));
  } else {
    const [, minor = '0', patch = '0'] = mcVersion.split('.');
    versions = versions.filter((v) => v.startsWith(`${minor}.${patch}.`));
  }
  return [...new Set(versions)].map((version) => ({
    version,
    stable: !/beta|alpha/i.test(version),
    label: '稳定版',
  }));
}

版本继承:合并而不是覆盖

Minecraft 的版本 JSON 支持 inheritsFrom。一个 Fabric 版本的 JSON 可能只有:

{
  "id": "fabric-loader-0.16.x-1.21.1",
  "inheritsFrom": "1.21.1",
  "mainClass": "net.fabricmc.loader.impl.launch.knot.KnotClient",
  "libraries": [ … ]
}

真正的客户端 JAR、资源索引、日志配置都在父版本 1.21.1 里。resolveVersion() 递归读取继承链,mergeVersions() 负责合并:

function mergeVersions(parent, child) {
  const merged = { ...parent, ...child };
  merged.arguments = {
    game: [...(parent.arguments?.game || []), ...(child.arguments?.game || [])],
    jvm: [...(parent.arguments?.jvm || []), ...(child.arguments?.jvm || [])],
  };
  if (!parent.arguments && !child.arguments) delete merged.arguments;
  if (child.minecraftArguments) merged.minecraftArguments = child.minecraftArguments;

  const libs = [];
  const index = new Map();
  for (const library of [...(parent.libraries || []), ...(child.libraries || [])]) {
    const key = libraryKey(library);
    if (index.has(key)) {
      libs[index.get(key)] = library; // 子版本覆盖父版本的同名库
    } else {
      index.set(key, libs.length);
      libs.push(library);
    }
  }
  merged.libraries = libs;
  merged.assetIndex = child.assetIndex || parent.assetIndex;
  merged.assets = child.assets || parent.assets;
  merged.javaVersion = child.javaVersion || parent.javaVersion;
  merged.logging = child.logging || parent.logging;
  merged.downloads = child.downloads?.client ? child.downloads : parent.downloads || child.downloads;
  merged.mainClass = child.mainClass || parent.mainClass;
  return merged;
}

为什么库的去重键要带规则

libraryKey() 不只用 Maven 坐标,还包含 rules 与 natives:

function libraryKey(library) {
  const rules = JSON.stringify(library.rules || null);
  const natives = JSON.stringify(library.natives || null);
  try {
    const coordinate = parseCoordinate(library.name);
    return `${coordinate.group}:${coordinate.artifact}:${coordinate.classifier}:${coordinate.extension}|${rules}|${natives}`;
  } catch {
    return `${library.name || JSON.stringify(library)}|${rules}|${natives}`;
  }
}

原因是 1.12.2 同时带了两个 LWJGL 2 版本,它们的 allow / disallow 规则不同。如果只按坐标去重,会误删其中一个平台的库。

平台规则

evaluateRules() 从前往后应用规则,最后命中的规则决定 allow / disallow:

function evaluateRules(rules, context = osContext()) {
  if (!Array.isArray(rules) || rules.length === 0) return true;
  let allowed = false;
  for (const rule of rules) {
    if (matchesRule(rule, context)) allowed = rule.action === 'allow';
  }
  return allowed;
}

context 包含 windows / osx / linux、x86 / x64 / arm64、内核版本,以及 has_custom_resolution 等特性标记。原生库还要根据 natives 模板替换 ${arch},并检查 classifier 是否匹配当前架构。

原版安装:一次下载,多处复用

installVanilla() 的顺序:

检查 .mclite-installed.json 标记
        ↓ 已安装
复用现有文件,写入原版缓存
        ↓ 未安装
尝试从原版 ZIP 缓存恢复
        ↓
获取版本 JSON → 写 versions/<id>/<id>.json
        ↓
下载客户端 JAR
        ↓
按规则下载 libraries
        ↓
下载日志配置
        ↓
下载资源索引 + 全部资源对象
        ↓
整理旧版虚拟资源(virtual / map_to_resources)
        ↓
写入安装标记 + 原版缓存

简化后的代码:

async function installVanilla(meta, gameDir, versionId, options = {}) {
  const task = options.task;
  const marker = path.join(gameDir, 'versions', versionId, '.mclite-installed.json');
  const versionFile = path.join(gameDir, 'versions', versionId, `${versionId}.json`);
  const jarFile = path.join(gameDir, 'versions', versionId, `${versionId}.jar`);

  if ((await pathExists(marker)) && (await pathExists(versionFile)) && (await statSize(jarFile) > 0)) {
    await writeVanillaCache(gameDir, versionId, { task }).catch(() => {});
    return readVersionJson(gameDir, versionId);
  }

  const cached = await restoreVanillaFromCache(gameDir, versionId);
  const versionJson = cached.json || await meta.getVersionJson(versionId);

  await writeJson(versionFile, versionJson);
  await ensureClientJar(gameDir, versionId, versionJson, task);
  await downloadVersionLibraries(gameDir, versionJson.libraries, task, options.concurrency);
  await ensureLoggingConfig(gameDir, versionJson, task);
  await ensureAssets(gameDir, versionJson, task, options.concurrency);

  await writeJson(marker, { versionId, installedAt: new Date().toISOString() });
  await writeVanillaCache(gameDir, versionId, { task, force: true });
  return versionJson;
}

资源下载

资源对象按哈希分目录存放:

assets/objects/<hash 前两位>/<hash>

资源索引里的 objects 只记录哈希和大小,下载地址由启动器拼接:

url: `https://resources.download.minecraft.net/${prefix}/${object.hash}`

资源索引可能包含数千个对象,downloadMany() 会用 8 个并发(可配置)下载,并在每个文件完成后更新进度。

旧版资源

1.6.4 等旧版本的资源索引带有 virtual: true,游戏期望资源位于 assets/virtual/<index>/;某些版本则使用 map_to_resources,期望位于 resources/。启动器在下载完成后会:

  • 硬链接(失败则复制)assets/objects/<hash> 到目标目录;
  • 保留原始对象,避免其他版本重复下载。

原版缓存

缓存文件位于:

.minecraft/mclite/cache/vanilla/<版本>.zip

ZIP 内包含三个文件:

cache.json   版本号、大小、SHA1、创建时间
version.json 原版版本 JSON
client.jar   客户端 JAR

恢复时会校验 cache.json 中的 SHA1 与版本号;损坏或不匹配时返回 cache-corrupt / cache-mismatch,不会把坏文件写进 versions/。

Java:先找,找不到再下载

java.js 的探测顺序:

  1. 设置中手动指定的 Java 路径;
  2. JAVA_HOME;
  3. PATH 中的 java;
  4. 本启动器游戏目录下的 runtime/;
  5. 常见 JDK / JRE 安装目录(Adoptium、Microsoft、Zulu、BellSoft、Corretto、Semeru、GraalVM、.jdks 等);
  6. 官方启动器的 %APPDATA%\.minecraft\runtime。

每个候选都会执行 java -version,从 stdout / stderr 解析主版本:

const JAVA_VERSION_RE = /(?:java|openjdk) version\s+"([^"]+)"/i;

function parseJavaVersionText(text) {
  const match = String(text).match(JAVA_VERSION_RE);
  if (!match) return null;
  const version = match[1];
  const major = version.startsWith('1.')
    ? Number(version.split('.')[1])
    : Number(version.split(/[._-]/)[0]);
  return { version, major };
}

版本映射

ensureJava() 读取版本 JSON 的 javaVersion.majorVersion,找不到匹配的 Java 时调用 installJavaRuntime():

Minecraft Java
1.16.5 及更早 8
1.17 16
1.18 – 1.20.4 17
1.20.5 – 1.21.x 21
新版快照 / 正式版 以版本 JSON 的 javaVersion 为准

Mojang 运行时清单里有 jre-legacy、java-runtime-alpha、java-runtime-gamma、java-runtime-delta、java-runtime-epsilon 等组件。MetaService.findJavaRuntime() 优先使用版本 JSON 指定的 component,否则按主版本回退。

下载后会把文件解压到:

.minecraft/runtime/<组件>/<平台>/

并再次执行 java -version 校验主版本,避免下载到错误架构或损坏的运行时。

启动:参数是拼出来的

launchInstance() 的主线:

校验账户与实例状态
        ↓
ensureInstanceFiles() 补全库 / 资源 / 日志配置
        ↓
resolveVersion() 解析继承链
        ↓
ensureJava() 选择或下载 Java
        ↓
extractNatives() 解压原生库
        ↓
classpathFor() 构建 Classpath
        ↓
buildJvmArguments() + buildGameArguments()
        ↓
spawn(javaw.exe, args)
        ↓
日志流 + 退出回调

Classpath 的特殊情况

现代 Forge / NeoForge 使用 BootstrapLauncher,它自己管理打补丁后的 Minecraft JAR。如果把继承的原版 JAR 放进普通 Classpath,Java 会看到两份 Minecraft:

const bootstrapManagesClientJar =
  resolved.json.mainClass === 'cpw.mods.bootstraplauncher.BootstrapLauncher' ||
  resolved.json.mainClass === 'net.minecraftforge.bootstrap.ForgeBootstrap';

if (!bootstrapManagesClientJar && resolved.jarPath) {
  entries.push(resolved.jarPath);
}

其余库按 libraryFiles() 的结果加入 Classpath,路径统一经过 safeJoin()。

JVM 参数

buildJvmArguments() 做四件事:

  1. 展开版本 JSON 的 arguments.jvm,按规则过滤并替换占位符;
  2. 如果用户没有写 -Xms/-Xmx,加入默认内存与 G1GC;
  3. 加入 UTF-8、Log4j 安全参数、启动器品牌;
  4. 如果没有 -cp,追加 -cp <classpath>;如果没有 java.library.path,追加原生库目录。
if (!userMemory) {
  const memory = Number(config.memory) || 4096;
  prefix.push(`-Xms${Math.min(1024, memory)}M`, `-Xmx${memory}M`, '-XX:+UseG1GC');
}

if (!hasLibraryPath) body.push(`-Djava.library.path=${nativesDir}`);
if (loggingPath && !hasLog4j) body.push(`-Dlog4j.configurationFile=${loggingPath}`);
if (!hasCp) body.push('-cp', classpath.join(path.delimiter));

游戏参数

新版本使用 arguments.game,旧版本使用 minecraftArguments。两者都支持 ${...} 占位符:

const replacements = {
  auth_player_name: username,
  auth_uuid: uuid,
  auth_access_token: accessToken,
  user_type: isMicrosoft ? 'msa' : 'legacy',
  version_name: instance.versionId,
  version_type: resolved.json.type || 'release',
  game_directory: minecraftDir,
  assets_root: assetsRoot,
  assets_index_name: indexId,
  library_directory: path.join(gameRoot, 'libraries'),
  classpath: classpath.join(path.delimiter),
  // …
};

resolveList() 会丢弃仍然包含未替换占位符的参数,避免把 ${auth_access_token} 原样传给游戏。

启动进程

Windows 下优先使用 javaw.exe,它是 GUI 子系统的 JVM,不会弹出控制台窗口,但 stdout / stderr 仍然可以管道读取:

async function launchBinaryFor(javaPath) {
  if (process.platform !== 'win32') return javaPath;
  if (path.basename(javaPath).toLowerCase() === 'javaw.exe') return javaPath;
  if (path.basename(javaPath).toLowerCase() === 'java.exe') {
    const javaw = path.join(path.dirname(javaPath), 'javaw.exe');
    if (await pathExists(javaw)) return javaw;
  }
  return javaPath;
}

spawn 时不使用 detached,因为 Windows 上 detached 子进程会获得自己的控制台窗口。启动器通过 gameProcesses 跟踪 PID,退出时统一清理。

日志同时写入文件与内存:

mclite/instances/<实例>/logs/mclite-<时间戳>.log

每一行都带时间、级别与来源:

[2026-01-01T12:00:00.000Z][info][stdout] [12:00:00] [main/INFO]: ...

调试模式额外记录:

  • JVM 参数、游戏参数(令牌已脱敏);
  • 完整 Classpath(逐行);
  • 合并后的版本 JSON;
  • Node / Electron / Java 版本与内存配置。

加载器安装:三种完全不同的路径

installLoader() 先安装原版基础,再按加载器类型分支:

if (loader === 'vanilla') {
  const baseJson = await installVanilla(meta, gameDir, mcVersion, { … });
  await ensureJava(meta, baseJson, { … });
  return { versionId: mcVersion };
}

const baseJson = await installVanilla(meta, gameDir, mcVersion, { … });

if (loader === 'fabric' || loader === 'quilt') {
  versionId = await installFabricLike(meta, gameDir, mcVersion, loader, loaderVersion, task);
  await ensureJava(meta, baseJson, { … });
} else if (loader === 'forge' || loader === 'neoforge') {
  versionId = await installForgeLike(meta, gameDir, mcVersion, loader, loaderVersion, task, options);
}

Fabric / Quilt

直接请求官方 profile JSON,写入 versions/<id>/<id>.json,再下载 profile 里的库:

const profile = await meta.getLoaderProfile(loaderType, mcVersion, loaderVersion);
const versionFile = path.join(gameDir, 'versions', profile.id, `${profile.id}.json`);
await writeJson(versionFile, profile);
await downloadVersionLibraries(gameDir, profile.libraries, task, concurrency);

profile 自带 inheritsFrom、mainClass 与 libraries,不需要运行安装器。

Forge / NeoForge

这两个加载器需要运行官方安装器:

下载 installer.jar(校验 .sha1)
        ↓
NeoForge:预下载安装器所需的库与 universal JAR
        ↓
写入 launcher_profiles.json(安装器要求)
        ↓
java -jar installer.jar --installClient <gameDir>
        ↓
在 versions/ 中找到新生成的版本
        ↓
补全库与日志配置

runProcess() 会实时读取安装器输出,转发到任务日志,并设置 30 分钟超时。代理会以 -Dhttp.proxyHost / -Dhttps.proxyHost 参数传给安装器。

NeoForge 在无法直连 Maven 的网络里容易失败,所以安装前会从 installer 的 install_profile.json 与版本 JSON 中提取库,预先下载到 libraries/:

const zip = new AdmZip(installerPath);
const profile = JSON.parse(zip.readAsText('install_profile.json'));
const versionJson = JSON.parse(zip.readAsText(profile?.json || 'version.json'));
addLibraries(profile.libraries);
addLibraries(versionJson?.libraries);

安装器随后只需要访问 Mojang 官方源,镜像回退负责把 NeoForge Maven 的请求转到 BMCLAPI。

安装完成后,findGeneratedVersion() 按命名规则和时间戳定位新版本:

const expected = loaderType === 'forge'
  ? [`${mcVersion}-forge-${loaderVersion}`, `forge-${loaderVersion}`]
  : [`${mcVersion}-neoforge-${loaderVersion}`, `neoforge-${loaderVersion}`];

如果安装器退出码为 0 但找不到版本,会抛出明确错误,提示查看安装日志。

整合包:解析、安装、补齐

识别格式

inspectModpack() 打开 ZIP,先找 modrinth.index.json,再找 CurseForge 的 manifest.json:

const summary = inspectMrpack(zip, resolved) || inspectCurseforge(zip, resolved);
if (!summary) {
  throw codedError('MODPACK_UNSUPPORTED', '未找到 modrinth.index.json 或 CurseForge manifest.json');
}

Modrinth 清单的关键字段:

{
  "game": "minecraft",
  "dependencies": {
    "minecraft": "1.21.1",
    "fabric-loader": "0.16.x"
  },
  "files": [
    {
      "path": "mods/example.jar",
      "hashes": { "sha1": "…" },
      "env": { "client": "required" },
      "downloads": ["https://cdn.modrinth.com/…"],
      "fileSize": 123456
    }
  ]
}

CurseForge 清单的关键字段:

{
  "minecraft": {
    "version": "1.20.1",
    "modLoaders": [{ "id": "forge-47.4.0", "primary": true }]
  },
  "files": [{ "projectID": 123, "fileID": 456, "required": true }]
}

导入流程

解析清单 → 确定 Minecraft / 加载器版本
        ↓
查找同名实例;存在则追加版本,否则新建实例
        ↓
installLoader() 安装原版 + 加载器
        ↓
createVersionAlias() + ensureVersionAliasCompatibility()
        ↓
创建隔离目录
        ↓
解压 overrides/ 与 client-overrides/
        ↓
下载清单文件(mrpack 直接下载;CurseForge 先查文件信息)
        ↓
记录 warnings / partial,完成任务

覆盖文件

Modrinth 使用 overrides/ 与 client-overrides/;CurseForge 使用 manifest.overrides 指定目录(默认 overrides):

function getOverridePrefixes(kind, manifest) {
  if (kind === 'modrinth') return ['overrides/', 'client-overrides/'];
  const prefix = String(manifest.overrides || 'overrides')
    .replace(/\\/g, '/')
    .replace(/^\/+|\/+$/g, '');
  return [`${prefix}/`, 'client-overrides/'];
}

解压时每个路径都经过 safeJoin(),并跳过 __MACOSX。覆盖文件通常包含配置、资源包、光影和 options.txt,因此要先解压,再下载清单文件;这样作者提供的同名文件会直接复用。

CurseForge 文件解析

CurseForge 的清单只有 projectID / fileID,需要查询文件信息才能拿到下载地址:

const data = await curseforgeRequest(`/mods/${file.projectID}/files/${file.fileID}`, {
  curseforgeApiKey,
  domesticMirror: domestic,
});
const info = data?.data;
if (!info?.downloadUrl) {
  missing.push({ path: info?.fileName, reason: '作者未允许第三方下载' });
}

默认通过 MCIM 国内镜像访问,不需要 API Key;关闭镜像后使用官方 API,需要用户自己的 Key。镜像只解决网络访问,不会绕过作者的分发限制。

缺失文件

以下情况会记录为 warning,而不是让整个导入失败:

  • 作者没有提供下载地址;
  • 作者禁止第三方下载;
  • 单个模组查询失败。

导入完成后,实例记录里的 modpack.partial 为 true,界面会显示缺失数量。游戏可能缺少部分内容,但已经下载的文件仍然可用。

导出

exportModpack() 生成 Modrinth .mrpack:

modrinth.index.json
overrides/
  mods/
  config/
  resourcepacks/
  shaderpacks/
  options.txt

默认排除:

  • 存档 saves/(可用 includeSaves 打开);
  • 日志、崩溃报告、截图;
  • natives/、cache/、downloads/、backups/;
  • assets/、libraries/、runtime/、versions/;
  • servers.dat、usercache.json、launcher_profiles.json 与 McLite 标记文件。

导出包不依赖原作者的文件下载地址,所有内容都在 overrides/ 里,因此可以直接重新导入 McLite,也可以交给其他支持 .mrpack 的启动器。

在线目录:Modrinth、CurseForge 与 MC百科

Modrinth

搜索使用 facets:

project_type:modpack
versions:<mcVersion>
categories:<loader>

空关键词按下载量排序,有关键词按相关度排序。开启国内镜像时,API 与图标、文件 CDN 都走 MCIM;失败时回退官方 API。

下载时按 Minecraft 版本与加载器过滤版本,选择最新发布的一个,优先取 .mrpack 文件:

const file =
  version.files.find((item) => item.primary && /\.mrpack$/i.test(item.filename)) ||
  version.files.find((item) => /\.mrpack$/i.test(item.filename)) ||
  version.files.find((item) => item.primary) ||
  version.files[0];

CurseForge

搜索使用 gameId=432、classId=4471(整合包分类)。默认走 MCIM 的 /curseforge/v1,没有 API Key 也能搜索;关闭镜像后必须提供 Key。

下载时先列文件,再按 gameVersions 过滤 Minecraft 版本与加载器,优先正式版(releaseType === 1),最后按发布时间倒序。

MC百科

MC百科是资料站,不托管文件。启动器做两件事:

  1. 解析整合包列表页的 modlist-block,提取名称、简介、封面与详情页 ID;
  2. 打开详情页,解析作者提供的下载链接。

MC百科的下载链接不是直接 URL,而是经过 link.mcmod.cn/target/<base64> 跳转:

const targetMatch = block.match(/link\.mcmod\.cn\/target\/([A-Za-z0-9+/=]+)/);
let url = '';
try {
  url = Buffer.from(targetMatch[1], 'base64').toString('utf8');
} catch {
  continue;
}
if (!/^https?:\/\//i.test(url)) continue;

解析结果会过滤广告、赞助与推广链接,只保留网盘、CurseForge、Modrinth、GitHub 等下载相关地址。网盘需要用户手动下载后再拖入启动器。

列表与详情分别缓存 60 秒和 5 分钟;遇到 429 或安全验证会提示稍后再试。

扫描外部实例:识别而不是复制

扫描的入口有四个:

  1. 启动器所在目录的 .minecraft;
  2. 当前游戏目录(如果设置中改过);
  3. 官方启动器的 %APPDATA%\.minecraft;
  4. 用户手动添加的目录(最多 24 个)。

expandMinecraftRoots() 支持三种输入:

  • 直接选择 .minecraft;
  • 选择包含 .minecraft 的游戏文件夹;
  • 选择包含多个 .minecraft 子文件夹的父目录。

识别加载器

detectLoader() 不依赖目录名,而是看 mainClass 与库坐标:

function detectLoader(versionId, json) {
  const id = String(versionId || '').toLowerCase();
  const mainClass = String(json?.mainClass || '').toLowerCase();
  const libraries = (json?.libraries || []).map((library) => versionIdFromLibrary(library.name));

  const has = (group, artifact) =>
    libraries.some((library) => library.group === group && library.artifact === artifact);

  if (id.includes('neoforge') || mainClass.includes('neoforge') || has('net.neoforged', 'neoforge')) {
    return { type: 'neoforge', version: … };
  }
  if (id.includes('quilt') || mainClass.includes('quilt') || has('org.quiltmc', 'quilt-loader')) {
    return { type: 'quilt', version: … };
  }
  if (id.includes('fabric') || mainClass.includes('fabricmc') || has('net.fabricmc', 'fabric-loader')) {
    return { type: 'fabric', version: … };
  }
  if (id.includes('forge') || mainClass.includes('forge') || has('net.minecraftforge', 'forge')) {
    return { type: 'forge', version: … };
  }
  return { type: 'vanilla', version: '' };
}

版本 JSON 里的库版本可能是 net.minecraftforge:forge:<mc>-<loader>,需要去掉 Minecraft 前缀;目录名里也可能有 forge-<loader>,作为兜底。

识别隔离与基础版本

  • 如果 versions/<版本>/ 下存在 mods、saves、config、options.txt 等,按版本隔离启动;
  • 沿 inheritsFrom 向上找基础版本,推断 Minecraft 版本;
  • 启动器自己安装的版本带有 .mclite-* 标记,扫描时跳过,避免重复显示。

去重与删除

扫描到的版本使用 sha1(root + 版本目录) 生成稳定 ID。首次扫描会新建实例;后续扫描更新元数据;来源目录被移除或版本消失时,删除对应的外部实例记录。

删除扫描版本会永久删除来源目录中的整个版本文件夹(包括模组与存档)。如果只想停止管理,应在设置中移除扫描目录,而不是删除版本。界面会二次确认这一点。

账户:离线 UUID 与设备代码

本地账户

本地账户只做两件事:校验名称、生成离线 UUID。

function offlineUuid(name) {
  const digest = crypto.createHash('md5').update(`OfflinePlayer:${name}`, 'utf8').digest();
  digest[6] = (digest[6] & 0x0f) | 0x30; // version 3
  digest[8] = (digest[8] & 0x3f) | 0x80; // variant
  const hex = digest.toString('hex');
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}

这个算法与 Minecraft 的离线模式一致,因此同一个名称在不同启动器中会得到相同的 UUID。它不能用于正版验证服务器。

Microsoft 登录

流程:

请求设备代码
   ↓
用户在浏览器输入 user_code 并登录
   ↓
轮询 token 端点
   ↓
Xbox Live 用户认证
   ↓
XSTS 授权
   ↓
Minecraft 登录(login_with_xbox)
   ↓
读取 Minecraft 档案(名称 / UUID)

关键端点:

const DEVICE_CODE_URL = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode';
const TOKEN_URL = 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token';
const XBOX_USER_URL = 'https://user.auth.xboxlive.com/user/authenticate';
const XBOX_XSTS_URL = 'https://xsts.auth.xboxlive.com/xsts/authorize';
const MINECRAFT_LOGIN_URL = 'https://api.minecraftservices.com/authentication/login_with_xbox';
const MINECRAFT_PROFILE_URL = 'https://api.minecraftservices.com/minecraft/profile';

使用 consumers 端点意味着只接受个人 Microsoft 账户;工作 / 学校账户无法完成 Minecraft 登录。

async function obtainMinecraftAccount(tokens, options = {}) {
  const xbl = await authenticateXboxLive(tokens.access_token);
  const xsts = await authorizeXsts(xbl);
  const uhs = xsts?.DisplayClaims?.xui?.[0]?.uhs;
  const xuid = xsts?.DisplayClaims?.xui?.[0]?.xid || '';

  const minecraft = await loginWithXbox(xsts.Token, uhs);
  const profile = await fetchMinecraftProfile(minecraft.access_token);

  return {
    type: 'microsoft',
    name: profile.name,
    uuid: formatUuid(profile.id),
    xuid,
    accessToken: minecraft.access_token,
    refreshToken: tokens.refresh_token || options.previousRefreshToken || '',
    expiresAt: Date.now() + (Number(minecraft.expires_in) || 86400) * 1000,
    clientId: options.clientId,
  };
}

令牌在保存前使用 Electron safeStorage 加密:

dpapi:<base64>   Windows DPAPI 加密
plain:<base64>   系统不支持加密时的降级格式

启动前如果令牌将在 5 分钟内过期,会自动刷新;刷新失败会要求重新登录。

403 的真实原因

Mojang 自 2022 年起要求第三方 Minecraft 应用把 Azure Client ID 提交到白名单。未获批时,登录会在最后一步返回 403:

Invalid app registration

这不是启动器能绕过的问题。文档与界面都会提示用户前往 https://aka.ms/mce-reviewappid 提交申请。审核是人工的,可能不批准个人 / 非公开项目。

诊断:把堆栈翻译成可执行的建议

诊断规则是一组“正则 → 解释 → 建议 → 快捷操作”:

{
  id: 'java-version',
  title: 'Java 版本不匹配',
  severity: 'error',
  patterns: [
    /UnsupportedClassVersionError/i,
    /Unsupported class file major version/i,
    /requires Java (?:SE )?\d+/i,
  ],
  explanation: '游戏或模组加载器需要的 Java 版本与当前使用的 Java 不一致。',
  suggestions: [
    '在“设置 → Java 运行时”中选择“自动选择”,让启动器匹配对应版本。',
    '如果手动指定了 Java,请改用与 Minecraft 版本对应的 Java。',
  ],
  actions: ['open-java-settings'],
}

diagnoseText() 遍历所有规则,命中后截取第一条证据:

function diagnoseText(text, context = {}) {
  const content = String(text || '');
  const issues = [];
  for (const rule of RULES) {
    if (!rule.patterns.some((pattern) => pattern.test(content))) continue;
    issues.push({
      id: rule.id,
      title: rule.title,
      severity: rule.severity,
      explanation: rule.explanation,
      suggestions: rule.suggestions,
      actions: rule.actions,
      evidence: evidenceFor(content, rule.patterns),
    });
  }
  return { issues, matched: issues.length > 0, logLength: content.length };
}

覆盖范围包括:

  • Java 版本、内存不足;
  • 模组缺失依赖、重复模组、加载器 / Mixin 不兼容;
  • 显卡驱动、OpenGL、访问冲突;
  • 正版验证失败;
  • 网络 / 下载失败;
  • 文件损坏、SHA1 校验失败;
  • 权限不足、存档损坏;
  • 找不到 Java。

如果游戏非零退出但没有命中任何规则,会返回一条“游戏异常退出”,提示查看第一条 ERROR / Exception / Caused by。

证据行会经过 redactSensitive():--accessToken、Bearer、accessToken / refreshToken JSON 字段都会被替换为 ***。

那些踩过的坑

这一节记录实现中最容易复发的细节,也是二次开发时最值得保留的部分。

PCL 的 mainClass 与 jar 字段

  • 别名 JSON 必须自带 mainClass,否则 PCL 2.99 归入“错误的版本”;
  • 不要使用 JSON 的 jar 字段,否则 NeoForge 会把原版 JAR 放进模块路径;
  • 用同名的 versions/<id>/<id>.jar 硬链接代替;
  • 启动时自动修复旧别名;
  • 必要时删除 .minecraft/PCL.ini 让 PCL 重新扫描。

NeoForge 的模块路径

错误信息:

Modules minecraft and _1._21._1 export package ... to module ...

根因是 Classpath 或模块路径里出现了两份 Minecraft。除了删除 jar 字段,classpathFor() 还会在 BootstrapLauncher / ForgeBootstrap 主类下跳过继承的客户端 JAR。

javaw.exe 与进程清理

  • 用 javaw.exe 避免控制台窗口;
  • 不要 detached,否则 Windows 会给子进程分配新控制台;
  • 关闭启动器时先 taskkill /T 等待 4 秒,再 /F;
  • 游戏正常退出后要从 runningGames 移除,否则实例会一直显示“运行中”。

旧版资源与 Java 25

  • 1.6.4 需要把 assets/objects 硬链接到 assets/virtual/<index>;
  • 1.7.10 的 Forge 安装器行为与新版不同,仍然走 --installClient;
  • 26.3 使用 Java 25,必须读取版本 JSON 的 javaVersion,不能只按 Minecraft 版本硬编码。

路径与编码

  • 所有写入使用 UTF-8;
  • 压缩包条目统一把 \ 替换为 /;
  • 启动参数加 -Dfile.encoding=UTF-8、-Dstdout.encoding=UTF-8;
  • 文件名经过 sanitizeFileName(),实例 ID 只保留 ASCII,显示名可以是中文;
  • 所有拼接路径经过 safeJoin(),防止压缩包写出游戏目录。

镜像不是万能药

  • BMCLAPI 只覆盖 Maven 主机,不覆盖所有 Mojang 资源;
  • MCIM 是第三方服务,可能限流或暂时不可用;
  • CurseForge 作者可以禁止第三方下载,镜像不会改变这一点;
  • 镜像失败时保留官方回退,但不要静默忽略错误,日志里要写清楚是哪个地址失败。

这些实现并不追求“支持所有情况”,而是把最常见的路径做稳:原版、四大加载器、两类整合包、正版 / 离线账户、国内网络、与 PCL / HMCL 共存。剩下的扩展点已经在架构文档里标出,可以按需接入。

获取与启动

便携版

运行:

dist/McLite-1.0.0-portable.exe

便携版的行为:

  • 启动器配置保存在 %APPDATA%\McLite;
  • 游戏文件默认保存在 exe 同级 的 .minecraft 文件夹;
  • 整个文件夹可以一起移动,换电脑后仍然可用;
  • 如果 exe 所在目录不可写(例如放在 Program Files),会自动回退到 %APPDATA%\McLite\.minecraft。

从源码运行

前置要求:

  • Windows 10 / 11;
  • Node.js 20 或更高版本;
  • 能访问 Mojang、Fabric、Forge、Modrinth 等下载源。
npm install
npm start

也可以双击 run.bat。如果 Electron 下载失败,先设置镜像:

$env:ELECTRON_MIRROR = "https://npmmirror.com/mirrors/electron/"
npm install

启动器本身不需要 Java。安装实例或启动游戏时,McLite 会自动寻找所需版本;找不到时会下载 Mojang 官方运行时。

构建与测试

npm test                 # 核心单元测试
npm run capture -- .tmp/ui.png   # 渲染界面并截图后退出
npm run dist             # 构建 Windows 便携版

便携版产物位于 dist/McLite-1.0.0-portable.exe。构建使用 electron-builder 的 Windows portable target。

第一次启动

首次打开时,实例库是空的:

首页
首次启动:新建实例或导入整合包

启动器会自动创建一个本地离线账户(默认名称来自配置,通常是 Steve)。你可以直接开始,也可以先配置账户。

新建实例

点击“新建实例”,填写实例名称,选择 Minecraft 版本与加载器:

新建实例
新建实例:版本列表、加载器、版本隔离

几个要点:

  • 实例名称会作为第一个版本的名称和版本目录名;
  • 版本列表支持搜索,例如输入 1.20.1;
  • “显示快照”可以查看快照版本;
  • 加载器默认是原版;选择 Fabric / Quilt / Forge / NeoForge 后,启动器会自动选择稳定版本;
  • 版本隔离(推荐) 默认开启。

点击“创建并安装”后,启动器会依次:

  1. 下载 Mojang 版本清单与版本 JSON;
  2. 下载客户端 JAR、库、资源索引、全部资源对象与日志配置;
  3. 安装加载器;
  4. 创建别名版本与隔离目录;
  5. 写入原版缓存。

安装过程中可以在右下角“活动”面板查看阶段、文件数、字节数和日志。

实例与版本

一个实例,多个版本

McLite 的实例是“组”。一个实例可以包含多个版本:

实例库
实例库:每个实例可以保存多个版本

在实例详情中:

  • 添加版本:向同一实例继续添加原版、加载器或整合包版本;
  • 切换当前版本:卡片和首页会显示当前版本;
  • 单独启动:不切换当前版本也可以直接启动某个版本;
  • 删除版本:从磁盘永久删除该版本目录。

版本隔离

新建实例时“版本隔离(推荐)”默认勾选。

状态 游戏目录 适用场景
开启 .minecraft/versions/<版本名>/ 多版本共存,模组 / 配置 / 存档互不影响
关闭 .minecraft/ 单版本,或希望与旧启动器共享根目录

开启隔离后,每个版本拥有独立的:

versions/<版本名>/
├── mods/
├── config/
├── saves/
├── resourcepacks/
├── shaderpacks/
└── logs/

这与 HMCL / PCL 的版本隔离结构一致。其他启动器开启版本隔离后可以直接识别。

切换隔离状态不会自动迁移已有文件。如果版本目录里已经有模组或存档,切换前先手动备份。

删除版本

删除版本会:

  • 从实例记录中移除该版本;
  • 永久删除对应的版本目录(包括模组、配置、存档);
  • 如果删除的是最后一个版本,实例本身也会被移除;
  • 保留共享的 libraries/、assets/ 与原版缓存。

如果实例正在运行,删除会被拒绝。删除前请确认版本目录里没有需要保留的存档。

与 PCL / HMCL 共存

McLite 安装的版本使用标准 .minecraft/versions/ 结构。别名版本会写入 mainClass 与加载器 libraries,并创建同名客户端 JAR,因此 PCL 2.99 可以识别。

如果 PCL 仍把版本放在“错误的版本”分组:

  1. 关闭 PCL;
  2. 删除或重命名 .minecraft/PCL.ini;
  3. 重新打开 PCL,让它重新扫描。

PCL.ini 只是 PCL 的版本列表缓存,不包含游戏文件。在 PCL 中启动时请保持“版本隔离”开启。

账户

本地离线账户

输入 3–16 位字母、数字或下划线即可创建。本地账户:

  • 不需要密码;
  • 使用离线 UUID;
  • 适合单人和离线服务器;
  • 不能进入开启正版验证的服务器。

Microsoft 正版登录

McLite 使用 Microsoft 设备代码流程:

  1. 在账户窗口填写 Azure Application (client) ID;
  2. 启动器显示设备代码,并在浏览器打开 Microsoft 登录页;
  3. 登录完成后,启动器依次请求 Xbox Live、XSTS 与 Minecraft 服务;
  4. 读取 Minecraft 档案名称与 UUID。

准备 Client ID

Microsoft 自 2022 年起要求第三方 Minecraft 应用通过 Mojang 白名单审核。本仓库不包含任何已获批准的 Client ID,也不能借用其他启动器的 Client ID。

应用注册本身是免费的,不需要 Azure 订阅或信用卡。

  1. 打开 Microsoft Entra 管理中心 → 应用注册 → 新注册。
  2. “支持的账户类型”选择 仅个人 Microsoft 账户;如果使用工作 / 学校租户,可以选择 任何组织目录中的账户和个人 Microsoft 账户。不要选择 仅此组织目录中的账户。
  3. 在 身份验证 → 高级设置 中,把 允许公共客户端流 设为 是。设备代码流程必须开启。
  4. 在 API 权限 → 添加权限 中搜索 Xbox Live,添加委托权限 XboxLive.signin,然后点击 授予管理员同意(如果租户要求)。
  5. 复制 Application (client) ID,填入启动器,或设置环境变量:
    $env:MCLITE_MSA_CLIENT_ID = "你的-client-id"
  6. 通过 Mojang 应用审核表 为该 Client ID 提交白名单申请。

登录时请使用拥有 Minecraft 的个人 Microsoft 账户。启动器使用 consumers 端点,工作 / 学校账户(包括开发者租户的管理员账户)无法完成 Minecraft 登录。

403 Invalid app registration

如果登录在最后一步返回 403:

Invalid app registration

说明 Mojang 白名单尚未通过。审核是人工进行的,可能需要较长时间,也可能不批准个人 / 非公开项目。这是 Minecraft 服务端的限制,无法在启动器内绕过。使用本地账户不受影响。

令牌存储

登录令牌使用 Electron safeStorage 加密后保存在本机:

%APPDATA%\McLite\accounts.json

Windows 上使用 DPAPI。如果系统不支持加密,会退化为可读的编码存储。请不要分享这个文件。

整合包

整合包页面
整合包:在线目录、拖放导入与格式说明

导入本地文件

把整合包拖到“整合包”页面,或点击“选择整合包文件”。

支持:

  • Modrinth .mrpack;
  • CurseForge 客户端整合包 .zip。

导入时启动器会自动:

  1. 识别 Minecraft 版本与加载器;
  2. 安装对应原版与加载器;
  3. 下载清单中的模组与资源;
  4. 解压 overrides/ 与 client-overrides/;
  5. 创建实例与隔离目录。

如果再次导入同名整合包,会加入现有实例,作为新版本。

Modrinth .mrpack

  • 读取 modrinth.index.json;
  • 按 env.client 过滤仅服务端文件;
  • 下载清单中的所有客户端文件,校验 SHA1;
  • 解压 overrides/ 与 client-overrides/;
  • 作者没有提供下载地址的文件会跳过并提示。

CurseForge .zip

有三种方式:

方式 是否需要 API Key 说明
国内镜像(默认) 否 搜索、版本信息与文件下载都走 mod.mcimirror.top
官方 API 是 在 CurseForge 开发者控制台创建 Key,关闭国内镜像后使用
本地导入 否 直接选择已下载的 .zip,但模组仍需下载地址

镜像只解决网络访问,不改变作者的分发限制。作者禁止第三方下载的文件仍会提示无法获取。

在线整合包

“整合包 → 在线整合包”支持三个来源:

来源 能力 注意
Modrinth 搜索、筛选、下载并导入 无需 API Key;开启镜像时元数据与文件走 MCIM
CurseForge 搜索、筛选、下载并导入 默认走 MCIM,无需 Key;关闭镜像后需要 Key
MC百科 浏览列表、查看作者下载页 不托管文件,网盘需手动下载

可以留空关键词查看热门整合包,也可以按 Minecraft 版本和加载器筛选。点击“下载并导入”后,启动器会下载整合包文件,再按普通整合包流程安装。镜像下载失败时会自动改用官方地址重试。

MC百科的“查看下载”会列出作者提供的夸克 / 百度网盘、CurseForge、Modrinth 等链接。网盘文件需要手动下载后再拖入启动器;CurseForge / Modrinth 链接会打开对应项目页。

导出整合包

在实例详情的当前版本中点击“导出”,选择保存位置,会生成 Modrinth .mrpack。

包含:

  • mods/;
  • config/;
  • 资源包、光影;
  • options.txt。

不包含:

  • 存档(除非显式勾选);
  • 日志、崩溃报告、截图;
  • 缓存、共享库、运行时;
  • 服务器列表与启动器配置。

导出的文件可以直接重新导入 McLite,也可以交给其他支持 .mrpack 的启动器。

原版缓存

原版缓存
原版缓存:查看、恢复、扫描与删除

下载过的原版版本会额外保存一份压缩缓存:

.minecraft/mclite/cache/vanilla/<版本>.zip

ZIP 内容:

cache.json   版本号、大小、SHA1、创建时间
version.json 原版版本 JSON
client.jar   客户端 JAR

用途:

  • 不同整合包或实例依赖同一个原版版本时复用;
  • 安装前如果基础版本文件缺失,优先从缓存恢复;
  • 缓存页可以查看大小、恢复文件、扫描已安装版本、删除缓存。

注意:

  • 缓存只包含原版 JSON 与客户端 JAR;
  • libraries/、assets/ 等共享文件本来就只保存一份;
  • 删除缓存不会删除已安装实例或 versions/ 中的文件;
  • 只有基础文件缺失时才会重新下载。

扫描其他启动器的实例

启动时和手动点击“扫描实例”时,会读取:

  1. 启动器所在目录的 .minecraft;
  2. 当前游戏目录(如果设置中改过);
  3. 官方启动器默认目录 %APPDATA%\.minecraft;
  4. 设置 → 实例扫描中手动添加的目录(最多 24 个)。

添加扫描目录的入口:

  • 设置 → 实例扫描;
  • 实例库页面右上角的“扫描目录”。

批量添加时:

  • 每行一个路径,也可以用分号分隔;
  • “选择目录”可以连续追加多个路径;
  • 可以直接选择 .minecraft、包含 .minecraft 的游戏文件夹,或包含多个 .minecraft 子文件夹的父目录;
  • 父目录的直接子目录会被自动展开;
  • 与默认目录重复的路径会自动跳过。

扫描会识别原版、Fabric、Quilt、Forge、NeoForge 以及基础 Minecraft 版本。扫描到的版本会直接出现在实例库中,可以切换、启动或查看来源目录。

重要区别:

  • 扫描不会复制或移动原有游戏文件;
  • 首次启动扫描版本时,缺失的库 / 资源 / Java 会被补全到对应来源的 .minecraft;
  • 删除扫描版本会永久删除来源 .minecraft/versions/<版本> 中的整个版本目录,包括模组与存档;
  • 如果只想停止管理,请在设置中“停止扫描”该目录,而不是删除版本;
  • 启动器自己安装的版本带有 .mclite-* 标记,重新扫描时不会重复显示。

设置项

设置
设置:账户、游戏与 Java、实例扫描、性能、CurseForge
设置 默认值 说明
本地账户名称 Steve 在账户窗口中管理;3–16 位字母、数字或下划线
游戏目录 exe 同级 .minecraft 必须是绝对路径;修改后实例列表切换
Java 运行时 自动选择 也可以手动指定 java.exe 或 JDK 目录
最大内存 4096 MB UI 滑块 1–16 GB;配置上限 32 GB;大型整合包建议 4–8 GB
并发下载数 8 1–16;网络不稳定时调低
网络代理 自动 留空自动读取环境变量 / 系统代理;direct 强制直连
JVM 参数 空 空格或换行分隔;用户写了 -Xmx 时启动器不再覆盖
启动后最小化 关 游戏启动后自动收起窗口
显示快照 关 新建实例时显示快照版本
记录完整启动日志 关 每次启动都写调试信息
CurseForge API Key 空 仅在关闭国内镜像时需要
使用国内镜像(MCIM) 开 CurseForge 搜索无需 Key,文件优先走国内镜像

大多数设置会在下一次安装或启动时生效。

启动调试与日志

调试启动

在实例详情中点击“调试启动”。本次启动会记录:

  • JVM 参数与游戏参数(令牌替换为 ***);
  • Classpath(逐行);
  • 合并后的版本 JSON;
  • Java / Node / Electron 版本;
  • 完整 stdout / stderr;
  • 退出码。

也可以在“设置 → 性能与启动”中打开“记录完整启动日志(调试)”,让每次启动都记录。

日志位置:

.minecraft/mclite/instances/<实例>/logs/mclite-<时间戳>.log

实例详情中的“打开日志”会直接打开该目录。

普通启动

普通启动也会写入日志,只是不包含调试头。游戏输出仍然会被捕获,因此崩溃后可以直接诊断。

诊断与常见问题

在实例详情中点击“诊断”,启动器会读取最近日志并匹配规则。游戏异常退出或启动失败后,也会自动分析并显示可能原因。

识别范围:

  • Java 版本不匹配;
  • 内存不足;
  • 模组缺失依赖 / 重复 / 冲突;
  • Mixin 或加载器不兼容;
  • 显卡驱动 / OpenGL 异常;
  • 正版验证失败;
  • 网络或下载失败;
  • 文件损坏 / SHA1 校验失败;
  • 权限不足;
  • 存档损坏;
  • 找不到 Java。

常见问题速查

现象 常见原因 处理
启动后立刻退出 Java 版本不匹配、模组冲突 看“诊断”与日志第一条 ERROR
卡在 0% / 下载失败 网络或代理未生效 设置代理;确认镜像;重试时已下载文件会跳过
提示缺少模组依赖 整合包文件缺失或被手动删除 查看导入日志的“文件缺失”;重新导入
正版登录 403 Mojang 白名单未通过 提交 Client ID 审核;先用本地账户
CurseForge 搜索失败 镜像不可用且未配置 Key 开启 MCIM 镜像,或填写 API Key
导入后没有模组 作者禁止第三方下载 查看 warnings;手动下载缺失文件放入 mods/
PCL 显示“错误的版本” 别名 JSON 缺少 mainClass 重新启动 McLite 自动修复;删除 PCL.ini
游戏关闭后仍显示运行中 进程未正常退出 等待几秒;启动器会在退出时清理进程树
存档打不开 存档损坏或维度模组被移除 备份 saves/;恢复对应模组

数据位置与迁移

配置与账户

%APPDATA%\McLite\config.json     启动器设置
%APPDATA%\McLite\accounts.json   账户与加密令牌

游戏目录

默认:

<启动器目录>\.minecraft\

结构:

.minecraft
├── assets/
├── libraries/
├── runtime/                 自动下载的 Java
├── versions/
│   ├── <实例版本>/          每个实例版本独立目录
│   │   ├── <版本名>.json
│   │   ├── <版本名>.jar
│   │   ├── mods/
│   │   ├── config/
│   │   └── saves/
│   └── <基础版本>/          共享的 Vanilla / 加载器版本
└── mclite/
    ├── instances/<实例>/    McLite 私有元数据、日志、原生库
    └── cache/
        └── vanilla/
            └── <版本>.zip   原版压缩缓存

迁移

  • 迁移整个启动器:把 exe 与 .minecraft 一起复制到新位置;
  • 迁移单个实例:复制 .minecraft/versions/<版本名>/,并在新启动器中重新创建实例指向该版本;
  • 完整备份:复制整个 .minecraft 与 %APPDATA%\McLite;
  • 只备份存档:复制对应版本的 saves/。

修改游戏目录后,实例列表会切换到新目录中的实例。旧目录不会被自动删除。

卸载

  1. 关闭启动器和所有游戏进程;
  2. 删除便携版 exe 及其同级的 .minecraft(如果想保留游戏文件,先备份);
  3. 删除 %APPDATA%\McLite(配置与账户);
  4. 如果使用源码运行,删除项目目录即可。

启动器不安装系统服务,也不写入卸载项。

安全与合规

  • 请使用合法拥有的 Minecraft 账户与内容;
  • 本地账户不能绕过正版验证;
  • Microsoft 登录需要你自己的 Azure Client ID,并通过 Mojang 白名单;
  • 不要把 accounts.json、config.json(含 API Key)分享给他人;
  • 不要从不可信来源下载整合包或模组;
  • 镜像只解决网络访问,不改变作者的分发许可。

McLite 是非官方项目,与 Mojang Studios、Microsoft、Modrinth、CurseForge、Fabric、Quilt、Forge、NeoForge 无隶属关系。

边界与限制

  • 当前只对 Windows 做了完整测试;核心代码包含少量跨平台处理,但未验证 macOS / Linux。
  • 不支持 authlib-injector 等第三方认证服务。
  • 界面语言为中文,暂未做国际化。
  • 没有自动更新器,更新需要替换便携版 exe 或重新构建。
  • Microsoft 正版登录需要自备 Azure 应用 Client ID,并提交 Mojang 白名单审核;未获批时会在 Minecraft 登录阶段返回 403。
  • 启动器不处理 Microsoft 密码,所有登录都在 Microsoft 官方页面完成。
  • 不会绕过正版验证;本地离线账户只能进入离线服务器。
  • NeoForge 仅对 21.1.249 做了完整验证,其他版本建议按实际安装结果确认。
  • MC百科是资料站,不托管整合包文件;启动器只解析作者提供的下载链接,网盘文件需要手动下载后再导入。
  • CurseForge 镜像只解决网络访问,不改变作者的分发限制;作者禁止第三方下载的文件会被跳过。

适合谁

  • 想了解 Minecraft 启动器内部结构的开发者;
  • 需要一个便携、可改造启动器的玩家;
  • 想学习 Electron 主进程 / 渲染进程分层与 IPC 设计的读者;
  • 想接入自己的下载源、整合包源或认证方式的二次开发者。

如果你需要自动更新、跨平台支持、第三方登录或商业分发,这个项目需要额外改造。

版本与许可

  • 版本:1.0.0(见 package.json)
  • 许可:MIT(见 LICENSE)
  • McLite 是非官方项目,与 Mojang Studios、Microsoft、Modrinth、CurseForge、Fabric、Quilt、Forge、NeoForge 无隶属关系。Minecraft 是 Mojang Studios 的商标。

主题测试文章,只做测试使用。发布者:zy,转转请注明出处:https://zynetc.top/?p=1001

赞 (0)
zy的头像zy
R730xd 服务器管理器:用 Redfish + IPMI 打造一台戴尔服务器的桌面控制台
上一篇 6天前
从 B 站链接到任意播放器:B2VRC 的完整实现与使用指南
下一篇 6天前

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注