McLite 是面向 Windows 的 Minecraft Java 版启动器。它把“下载游戏、准备 Java、安装加载器、导入整合包、启动游戏”串成一条完整链路,界面使用 Electron,核心逻辑是纯 Node.js。
项目不依赖预先安装 Java,也不要求用户手动整理 .minecraft。启动器会从 Mojang 获取版本清单与游戏文件,在本机寻找匹配的 Java,必要时下载 Mojang 官方运行时;安装加载器时调用 Fabric / Quilt 的 profile 接口,或运行 Forge / NeoForge 安装器。

它解决什么问题
一个“能启动”的启动器,实际上要处理很多碎片:
- 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,没有前端框架和运行时依赖。浅色主题、中文文案、任务进度与日志面板都围绕“安装 / 启动”这两个高频动作设计。
界面预览






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


已验证的环境
开发过程中实际完成过以下端到端测试(安装文件并启动到游戏主界面):
- 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并重新校验清单 - 启动诊断规则、常见问题弹窗和中文实例名创建
- 原版版本压缩缓存、删除 / 恢复确认界面,以及缓存恢复后的文件校验
这些结果来自开发环境,不能替代所有硬件、网络与整合包组合的验证。遇到问题时,优先查看启动日志与诊断结果。

进程模型
┌────────────────────────────────────────────────────┐
│ 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 / 子进程
选择这种结构的原因:
- 渲染进程不碰文件系统。即使界面被 XSS 注入,也无法直接读写磁盘或启动进程。
- 核心逻辑可测试。
src/core不依赖 Electron,测试可以直接require。 - 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() 处理三件事:
- 别名 JSON 必须自带
mainClass,否则 PCL 会把它归入“错误的版本”。 - 写入加载器的
libraries,让 PCL 能识别加载器类型。 - 创建与版本同名的客户端 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() 按以下顺序决定代理:
- 设置中的“网络代理”(
http://127.0.0.1:7897这样的地址); - 环境变量
HTTPS_PROXY/HTTP_PROXY/ALL_PROXY; - Windows 系统代理(通过 Electron
session.resolveProxy()); - 都没有则直连。
填写 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;
}
这样即使界面只显示一句“安装失败”,活动面板里仍然保留了完整的阶段日志。
扩展点
新增加载器
需要改四个位置:
src/core/meta.jsnormalizeLoader()接受新类型;getLoaderVersions()返回版本列表;getLoaderProfile()或安装器 URL 提供安装信息。
src/core/versions.jsinstallLoader()增加分支;- 如需要,新增
installXxx()。
src/core/scanner.jsdetectLoader()能识别新加载器的库或mainClass。
src/ui/renderer.js- 新建实例弹窗与加载器标签增加选项。
新增整合包来源
modpackCatalog.js的normalizeSource()接受新来源;- 实现
searchXxx()与resolveXxxDownload(); searchModpacks()/resolveModpackDownload()增加分支;- 界面增加来源标签页。
如果来源不托管文件(像 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 的探测顺序:
- 设置中手动指定的 Java 路径;
JAVA_HOME;PATH中的java;- 本启动器游戏目录下的
runtime/; - 常见 JDK / JRE 安装目录(Adoptium、Microsoft、Zulu、BellSoft、Corretto、Semeru、GraalVM、
.jdks等); - 官方启动器的
%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() 做四件事:
- 展开版本 JSON 的
arguments.jvm,按规则过滤并替换占位符; - 如果用户没有写
-Xms/-Xmx,加入默认内存与 G1GC; - 加入 UTF-8、Log4j 安全参数、启动器品牌;
- 如果没有
-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百科是资料站,不托管文件。启动器做两件事:
- 解析整合包列表页的
modlist-block,提取名称、简介、封面与详情页 ID; - 打开详情页,解析作者提供的下载链接。
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 或安全验证会提示稍后再试。
扫描外部实例:识别而不是复制
扫描的入口有四个:
- 启动器所在目录的
.minecraft; - 当前游戏目录(如果设置中改过);
- 官方启动器的
%APPDATA%\.minecraft; - 用户手动添加的目录(最多 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 后,启动器会自动选择稳定版本;
- 版本隔离(推荐) 默认开启。
点击“创建并安装”后,启动器会依次:
- 下载 Mojang 版本清单与版本 JSON;
- 下载客户端 JAR、库、资源索引、全部资源对象与日志配置;
- 安装加载器;
- 创建别名版本与隔离目录;
- 写入原版缓存。
安装过程中可以在右下角“活动”面板查看阶段、文件数、字节数和日志。
安装失败不会留下半成品版本记录。已经下载的共享库、资源和原版缓存会保留,重试时直接复用。
实例与版本
一个实例,多个版本
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 仍把版本放在“错误的版本”分组:
- 关闭 PCL;
- 删除或重命名
.minecraft/PCL.ini; - 重新打开 PCL,让它重新扫描。
PCL.ini 只是 PCL 的版本列表缓存,不包含游戏文件。在 PCL 中启动时请保持“版本隔离”开启。
账户
本地离线账户
输入 3–16 位字母、数字或下划线即可创建。本地账户:
- 不需要密码;
- 使用离线 UUID;
- 适合单人和离线服务器;
- 不能进入开启正版验证的服务器。
Microsoft 正版登录
McLite 使用 Microsoft 设备代码流程:
- 在账户窗口填写 Azure Application (client) ID;
- 启动器显示设备代码,并在浏览器打开 Microsoft 登录页;
- 登录完成后,启动器依次请求 Xbox Live、XSTS 与 Minecraft 服务;
- 读取 Minecraft 档案名称与 UUID。
准备 Client ID
Microsoft 自 2022 年起要求第三方 Minecraft 应用通过 Mojang 白名单审核。本仓库不包含任何已获批准的 Client ID,也不能借用其他启动器的 Client ID。
应用注册本身是免费的,不需要 Azure 订阅或信用卡。
- 打开 Microsoft Entra 管理中心 → 应用注册 → 新注册。
- “支持的账户类型”选择 仅个人 Microsoft 账户;如果使用工作 / 学校租户,可以选择 任何组织目录中的账户和个人 Microsoft 账户。不要选择 仅此组织目录中的账户。
- 在 身份验证 → 高级设置 中,把 允许公共客户端流 设为 是。设备代码流程必须开启。
- 在 API 权限 → 添加权限 中搜索 Xbox Live,添加委托权限
XboxLive.signin,然后点击 授予管理员同意(如果租户要求)。 - 复制 Application (client) ID,填入启动器,或设置环境变量:
$env:MCLITE_MSA_CLIENT_ID = "你的-client-id" - 通过 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。
导入时启动器会自动:
- 识别 Minecraft 版本与加载器;
- 安装对应原版与加载器;
- 下载清单中的模组与资源;
- 解压
overrides/与client-overrides/; - 创建实例与隔离目录。
如果再次导入同名整合包,会加入现有实例,作为新版本。
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/中的文件; - 只有基础文件缺失时才会重新下载。
扫描其他启动器的实例
启动时和手动点击“扫描实例”时,会读取:
- 启动器所在目录的
.minecraft; - 当前游戏目录(如果设置中改过);
- 官方启动器默认目录
%APPDATA%\.minecraft; - 设置 → 实例扫描中手动添加的目录(最多 24 个)。
添加扫描目录的入口:
- 设置 → 实例扫描;
- 实例库页面右上角的“扫描目录”。
批量添加时:
- 每行一个路径,也可以用分号分隔;
- “选择目录”可以连续追加多个路径;
- 可以直接选择
.minecraft、包含.minecraft的游戏文件夹,或包含多个.minecraft子文件夹的父目录; - 父目录的直接子目录会被自动展开;
- 与默认目录重复的路径会自动跳过。
扫描会识别原版、Fabric、Quilt、Forge、NeoForge 以及基础 Minecraft 版本。扫描到的版本会直接出现在实例库中,可以切换、启动或查看来源目录。
重要区别:
- 扫描不会复制或移动原有游戏文件;
- 首次启动扫描版本时,缺失的库 / 资源 / Java 会被补全到对应来源的
.minecraft; - 删除扫描版本会永久删除来源
.minecraft/versions/<版本>中的整个版本目录,包括模组与存档; - 如果只想停止管理,请在设置中“停止扫描”该目录,而不是删除版本;
- 启动器自己安装的版本带有
.mclite-*标记,重新扫描时不会重复显示。
设置项

| 设置 | 默认值 | 说明 |
|---|---|---|
| 本地账户名称 | 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/。
修改游戏目录后,实例列表会切换到新目录中的实例。旧目录不会被自动删除。
卸载
- 关闭启动器和所有游戏进程;
- 删除便携版 exe 及其同级的
.minecraft(如果想保留游戏文件,先备份); - 删除
%APPDATA%\McLite(配置与账户); - 如果使用源码运行,删除项目目录即可。
启动器不安装系统服务,也不写入卸载项。
安全与合规
- 请使用合法拥有的 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