1
0
Fork 0
GoodPlanCraftLaunCher/main.js

3412 lines
112 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* CaelLab BY-SA Code License
* Copyright (c) 2026 Yunyun(云云) By 虚舟实验室(CaelLab) / CaelLabGameTS
* Source: https://github.com/yunyun-3782/GoodPlanCraftLauncher
*/
const { app, BrowserWindow, ipcMain, net, dialog, Menu, shell } = require('electron');
const path = require('path');
const { spawn, spawnSync } = require('child_process');
const fs = require('fs');
const os = require('os');
const { tmpdir } = require('os');
const { pipeline } = require('stream/promises');
process.env.TZ = 'Asia/Shanghai';
app.commandLine.appendSwitch('js-flags', '--compile-hints-always');
app.commandLine.appendSwitch('disk-cache-size', '134217728');
function formatTimestamp() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const ms = String(now.getMilliseconds()).padStart(3, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}`;
}
function checkDebuggerAttached() {
try {
if (process.execArgv.some(arg => arg.includes('--inspect') || arg.includes('--debug-brk'))) {
return true;
}
if (process.env.ELECTRON_ENABLE_STACK_DUMPING) {
return true;
}
} catch (e) {}
return false;
}
let isDeveloperMode = false;
let BASE_DIR;
let GAME_DIR;
let CACHE_DIR;
let LOG_DIR;
let LOG_FILE;
let APP_VERSION;
let CONFIG_DIR;
const DEFAULT_SETTINGS = {
game: {
memory: '2',
playerName: 'GPCL_Player',
javaPath: '',
jvmArgs: '',
windowMode: 'windowed'
},
appearance: {
theme: 'light',
scale: '110'
},
download: {
maxConcurrent: 64,
javaMirror: 'tsinghua',
customJavaMirror: ''
},
advanced: {
autoCheckUpdate: true,
preventMultipleLaunch: true,
autoClearLogs: true,
logRetentionValue: 7,
logRetentionUnit: 'day',
playStartupAnimation: true,
developerMode: false
}
};
function parseIni(text) {
const result = {};
let currentSection = null;
const lines = text.split(/\r?\n/);
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith(';') || trimmed.startsWith('#')) {
continue;
}
const sectionMatch = trimmed.match(/^\[(.+)\]$/);
if (sectionMatch) {
currentSection = sectionMatch[1].toLowerCase();
if (!result[currentSection]) {
result[currentSection] = {};
}
continue;
}
const keyValueMatch = trimmed.match(/^([^=]+)=(.*)$/);
if (keyValueMatch && currentSection) {
const key = keyValueMatch[1].trim();
let value = keyValueMatch[2].trim();
if (value.toLowerCase() === 'true') {
value = true;
} else if (value.toLowerCase() === 'false') {
value = false;
} else {
const num = Number(value);
if (!isNaN(num)) {
value = num;
}
}
result[currentSection][key] = value;
}
}
return result;
}
function stringifyIni(obj) {
let result = '';
for (const [section, sectionData] of Object.entries(obj)) {
result += `[${section}]\n`;
for (const [key, value] of Object.entries(sectionData)) {
result += `${key}=${value}\n`;
}
result += '\n';
}
return result.trim();
}
function mergeWithDefaults(settings) {
const merged = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
for (const [section, sectionData] of Object.entries(settings)) {
if (merged[section]) {
for (const [key, value] of Object.entries(sectionData)) {
if (key in merged[section]) {
merged[section][key] = value;
}
}
}
}
return merged;
}
function loadSettings() {
try {
const configPath = path.join(BASE_DIR, 'gpcl', 'config', 'setting.ini');
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, 'utf8');
const parsed = parseIni(content);
const merged = mergeWithDefaults(parsed);
writeLog('配置已加载');
return merged;
}
} catch (e) {
writeLog(`加载配置失败: ${e.message}`);
}
writeLog('使用默认配置');
return JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
}
function saveSettings(settings) {
try {
const configDir = path.join(BASE_DIR, 'gpcl', 'config');
const configPath = path.join(configDir, 'setting.ini');
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
const content = stringifyIni(settings);
fs.writeFileSync(configPath, content, 'utf8');
writeLog('配置已保存');
return true;
} catch (e) {
writeLog(`保存配置失败: ${e.message}`);
return false;
}
}
function initPaths() {
BASE_DIR = app.getPath('userData');
GAME_DIR = path.join(BASE_DIR, '.minecraft');
CACHE_DIR = path.join(BASE_DIR, 'gpcl', 'cache');
LOG_DIR = path.join(BASE_DIR, 'gpcl', 'log');
CONFIG_DIR = path.join(BASE_DIR, 'gpcl', 'config');
const startupTime = new Date().toISOString().replace(/:/g, '-').replace('.', '_');
LOG_FILE = path.join(LOG_DIR, `${startupTime}.log`);
const requiredDirs = [
path.join(BASE_DIR, 'gpcl', 'config'),
path.join(BASE_DIR, 'gpcl', 'users'),
LOG_DIR,
CACHE_DIR
];
for (const dir of requiredDirs) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
}
function cleanupExpiredLogs() {
try {
const settings = loadSettings();
const autoClearLogs = settings.advanced?.autoClearLogs;
if (!autoClearLogs) {
writeLog('[日志清理] 自动清理日志已禁用');
return;
}
const retentionValue = settings.advanced?.logRetentionValue || 7;
const retentionUnit = settings.advanced?.logRetentionUnit || 'day';
// 计算过期时间
const now = Date.now();
let expireTime;
switch (retentionUnit) {
case 'hour':
expireTime = now - retentionValue * 60 * 60 * 1000;
break;
case 'day':
expireTime = now - retentionValue * 24 * 60 * 60 * 1000;
break;
case 'month':
expireTime = now - retentionValue * 30 * 24 * 60 * 60 * 1000;
break;
case 'year':
expireTime = now - retentionValue * 365 * 24 * 60 * 60 * 1000;
break;
default:
expireTime = now - retentionValue * 24 * 60 * 60 * 1000;
}
if (!fs.existsSync(LOG_DIR)) {
writeLog('[日志清理] 日志目录不存在');
return;
}
const files = fs.readdirSync(LOG_DIR);
let cleanedCount = 0;
for (const file of files) {
if (!file.endsWith('.log')) continue;
const filePath = path.join(LOG_DIR, file);
try {
const stat = fs.statSync(filePath);
if (stat.mtime.getTime() < expireTime) {
fs.unlinkSync(filePath);
cleanedCount++;
}
} catch (e) {
writeLog(`[日志清理] 清理文件失败: ${file} - ${e.message}`);
}
}
if (cleanedCount > 0) {
writeLog(`[日志清理] 已清理 ${cleanedCount} 个过期日志文件`);
} else {
writeLog('[日志清理] 没有需要清理的过期日志');
}
} catch (e) {
writeLog(`[日志清理] 清理过程出错: ${e.message}`);
}
}
let mainWindow;
APP_VERSION = require('./package.json').version;
function writeLog(message) {
const timestamp = formatTimestamp();
const logLine = `[${timestamp}] ${message}\n`;
try {
if (LOG_FILE) {
fs.appendFileSync(LOG_FILE, logLine, 'utf8');
}
console.log(`[${timestamp}] LOG:`, message);
} catch (e) {
console.error(`[${formatTimestamp()}] 写入日志失败:`, e);
}
}
let isDownloading = false;
let currentDownloadVersion = null;
let shouldCancelDownload = false;
let currentDownloadType = null; // 'game' or 'java'
let currentDownloadPath = null; // 下载的文件或文件夹路径
let javaDownloadResolve = null; // Java下载对话框的Promise resolve
let javaDownloadReject = null; // Java下载对话框的Promise reject
let isJavaDownloading = false; // Java下载中标志
function createWindow() {
mainWindow = new BrowserWindow({
width: 1000,
height: 700,
minWidth: 800,
minHeight: 600,
maxWidth: 1920,
maxHeight: 1080,
frame: false,
title: 'GoodPlanCraftLauncher',
icon: path.join(__dirname, 'gpcl', 'logo.ico'),
backgroundColor: '#0d0d15',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
devTools: true
}
});
try { Menu.setApplicationMenu(null); } catch (e) {}
mainWindow.setMenuBarVisibility(false);
mainWindow.setAutoHideMenuBar(true);
mainWindow.loadFile(path.join(__dirname, 'renderer', 'splash.html'));
// 关闭窗口前拦截
mainWindow.on('close', (event) => {
if (isDownloading || isJavaDownloading) {
event.preventDefault();
// 发送询问到渲染进程
mainWindow.webContents.send('confirm-close-while-downloading');
}
});
mainWindow.webContents.on('before-input-event', (event, input) => {
if (isDeveloperMode) return;
const key = input.key && input.key.toLowerCase();
const ctrlOrCmd = input.control || input.meta;
if ((ctrlOrCmd && key === 'r') || input.key === 'F5') event.preventDefault();
if ((input.control && input.shift && key === 'i') || input.key === 'F12' || (input.meta && input.alt && key === 'i')) event.preventDefault();
});
mainWindow.webContents.on('context-menu', (e) => {
if (!isDeveloperMode) e.preventDefault();
});
}
app.on('web-contents-created', (event, contents) => {
try {
contents.on('context-menu', (e) => {
if (!isDeveloperMode) e.preventDefault();
});
contents.on('before-input-event', (event, input) => {
if (isDeveloperMode) return;
const key = input.key && input.key.toLowerCase();
const ctrlOrCmd = input.control || input.meta;
if ((ctrlOrCmd && key === 'r') || input.key === 'F5') event.preventDefault();
if ((input.control && input.shift && key === 'i') || input.key === 'F12' || (input.meta && input.alt && key === 'i')) event.preventDefault();
});
} catch (e) {}
});
// 单实例锁:当检测到第二个实例时,凸显已有窗口
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) mainWindow.restore();
mainWindow.focus();
}
});
function createGPCLShortcut() {
try {
const startMenuDir = path.join(app.getPath('appData'), 'Microsoft', 'Windows', 'Start Menu', 'Programs');
const shortcutPath = path.join(startMenuDir, 'GPCL.lnk');
if (fs.existsSync(shortcutPath)) {
return;
}
if (app.isPackaged) {
const exePath = process.execPath;
shell.writeShortcutLink(shortcutPath, 'create', {
target: exePath,
description: 'GoodPlanCraftLauncher - Minecraft 启动器',
icon: exePath,
iconIndex: 0
});
writeLog('[快捷方式] 已创建 GPCL 快捷方式');
}
} catch (e) {
writeLog(`[快捷方式] 创建 GPCL 快捷方式失败: ${e.message}`);
}
}
app.whenReady().then(() => {
// 检测是否是 electron-builder 的测试运行
const isTesting = process.argv.includes('--squirrel-install') ||
process.argv.includes('--squirrel-obsolete') ||
process.argv.includes('--squirrel-updated') ||
process.argv.includes('--testing');
if (isTesting) {
console.log('[启动] 检测到构建测试模式,跳过窗口创建');
app.quit();
return;
}
// 提前初始化 BASE_DIR 以读取配置文件
BASE_DIR = app.getPath('userData');
// 检查是否启用防止多次启动
let preventMultipleLaunch = true;
try {
const configPath = path.join(BASE_DIR, 'gpcl', 'config', 'setting.ini');
if (fs.existsSync(configPath)) {
const content = fs.readFileSync(configPath, 'utf8');
const parsed = parseIni(content);
if (parsed.advanced) {
if (parsed.advanced.preventMultipleLaunch === false || parsed.advanced.preventmultiplelaunch === false) {
preventMultipleLaunch = false;
}
}
}
} catch (e) {}
if (preventMultipleLaunch) {
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
writeLog('检测到已有实例运行,退出');
app.quit();
return;
}
}
initPaths();
cleanupExpiredLogs();
try {
const devConfigPath = path.join(BASE_DIR, 'gpcl', 'config', 'setting.ini');
if (fs.existsSync(devConfigPath)) {
const devContent = fs.readFileSync(devConfigPath, 'utf8');
const devParsed = parseIni(devContent);
if (devParsed.advanced && devParsed.advanced.developerMode === true) {
isDeveloperMode = true;
writeLog('[开发者模式] 启动时已启用');
}
}
} catch (e) {}
createGPCLShortcut();
writeLog(`GPCL v${APP_VERSION} 启动`);
console.log('日志文件路径:', LOG_FILE);
createWindow();
});
app.on('window-all-closed', () => app.quit());
ipcMain.handle('show-message-dialog', async (_, options) => {
if (!mainWindow) return { success: false };
const result = await dialog.showMessageBox(mainWindow, {
type: options.type || 'info',
title: options.title || '提示',
message: options.message || '',
detail: options.detail || '',
buttons: options.buttons || ['确定'],
defaultId: options.defaultId || 0,
cancelId: options.cancelId || 0,
noLink: true
});
return { success: true, response: result.response };
});
// Java版本 → 官方runtime目录名映射
const JAVA_RUNTIME_MAP = {
"8": "jre-legacy",
"16": "java-runtime-beta",
"17": "java-runtime-gamma",
"21": "java-runtime-delta",
"25": "java-runtime-epsilon"
};
// Java版本 → 下载URL的key映射
const JAVA_DOWNLOAD_KEYS = {
"8": "jre-legacy",
"17": "java-runtime-gamma",
"21": "java-runtime-delta",
"25": "java-runtime-epsilon"
};
// 可选择的Java版本列表
const JAVA_VERSIONS_AVAILABLE = ["8", "17", "21", "25"];
// 获取Java运行时目录官方目录结构使用绝对路径
function getJavaRuntimeDir(javaVersion) {
const runtimeName = JAVA_RUNTIME_MAP[javaVersion] || `jre${javaVersion}`;
// 使用官方Minecraft运行时目录C:\Users\用户名\AppData\Roaming\.minecraft\runtime
return path.join(os.homedir(), 'AppData', 'Roaming', '.minecraft', 'runtime', runtimeName);
}
// 在目录中查找java.exe自动拼接\bin\使用java.exe以便捕获错误输出
function findJavaExecutable(runtimeDir) {
// 优先使用 bin\java.exe可以捕获控制台输出用于调试
const javaPath = path.join(runtimeDir, 'bin', 'java.exe');
if (fs.existsSync(javaPath)) {
try {
fs.accessSync(javaPath, fs.constants.R_OK);
return javaPath;
} catch (e) {
return null;
}
}
// 兜底:递归查找
return findJavaExecutableInDir(runtimeDir);
}
// 检查指定版本的Java是否已安装
async function checkJavaInstalled(javaVersion) {
const runtimeDir = getJavaRuntimeDir(javaVersion);
if (fs.existsSync(runtimeDir)) {
const exe = findJavaExecutable(runtimeDir);
if (exe) {
return { installed: true, javaPath: exe };
}
}
return { installed: false, javaPath: null };
}
// 获取指定Minecraft版本所需的Java版本
function getJavaVersionForMCVersion(versionId) {
// Minecraft 1.16及以下: Java 8
// Minecraft 1.17: Java 16
// Minecraft 1.18-1.20: Java 17
// Minecraft 1.21+: Java 21
const parts = versionId.split('.');
let major = parseInt(parts[0], 10);
let minor = parseInt(parts[1], 10);
// 如果版本号是 "26" 这种没有点的格式
if (isNaN(major)) major = 1;
// 处理像 "26" 这种格式(没有 minor
if (isNaN(minor)) {
// 26 及以上使用 Java 25
if (major >= 26) return '25';
// 21-25 使用 Java 21
if (major >= 21) return '21';
// 否则默认 Java 17
return '17';
}
// 处理 1.x.x 的旧版本
if (major === 1) {
if (minor < 17) return '8';
if (minor === 17) return '16';
if (minor <= 20) return '17';
return '21';
}
// 处理 2.x.x 和更大的版本21, 24, 26, ...
// 26 及以上使用 Java 25
if (major >= 26) return '25';
// 21-25 使用 Java 21
if (major >= 21) return '21';
return '17';
}
// 获取版本数据中指定的Java版本
function getJavaVersionFromVersionData(versionData) {
if (versionData?.javaVersion?.majorVersion) {
return String(versionData.javaVersion.majorVersion);
}
if (versionData?.javaVersion?.component) {
const m = versionData.javaVersion.component.match(/\d+/);
if (m) return m[0];
}
return getJavaVersionForMCVersion(versionData.id);
}
// 查找本地Java运行时只查找不下载
async function ensureJavaRuntime(version) {
const javaVersion = typeof version === 'string' ? version : String(version);
const runtimeDir = getJavaRuntimeDir(javaVersion);
writeLog(`[Java] 查找 Java ${javaVersion} 运行时: ${runtimeDir}`);
if (fs.existsSync(runtimeDir)) {
const exe = findJavaExecutable(runtimeDir);
if (exe) {
writeLog(`[Java] 找到 Java ${javaVersion}: ${exe}`);
return exe;
}
}
writeLog(`[Java] 未找到 Java ${javaVersion} 运行时`);
return null;
}
// 完整的Java运行时保证包括下载和安装
async function ensureJavaRuntimeWithInstall(version, mainWindow) {
const javaVersion = typeof version === 'string' ? version : String(version);
// 先检查是否已安装
const installed = await checkJavaInstalled(javaVersion);
if (installed.installed && installed.javaPath) {
writeLog(`[Java] Java ${javaVersion} 已安装: ${installed.javaPath}`);
return { success: true, javaPath: installed.javaPath };
}
// 如果未安装,触发下载流程
writeLog(`[Java] Java ${javaVersion} 未安装,开始下载流程`);
return null;
}
// 用户确认关闭并清理
ipcMain.handle('confirm-close-download', async () => {
if (isDownloading && currentDownloadVersion) {
shouldCancelDownload = true;
try {
// 清理游戏版本下载
const versionDir = currentDownloadPath || path.join(GAME_DIR, 'versions', currentDownloadVersion);
const cancelFilePath = path.join(versionDir, 'CancelDownload.txt');
const cancelContent = `GoodPlanCraftLauncher ${APP_VERSION}\n取消时间:${formatTimestamp()}\n\n正确的关闭\n您在下载完成前正确地关闭了GPCL。`;
if (fs.existsSync(versionDir)) {
fs.writeFileSync(cancelFilePath, cancelContent, 'utf8');
const files = fs.readdirSync(versionDir);
for (const file of files) {
const filePath = path.join(versionDir, file);
if (file !== 'CancelDownload.txt') {
if (fs.statSync(filePath).isDirectory()) {
fs.rmSync(filePath, { recursive: true, force: true });
} else {
fs.unlinkSync(filePath);
}
}
}
}
writeLog(`已清理游戏版本下载目录: ${versionDir}`);
} catch (e) {
console.error('清理下载失败:', e);
writeLog(`清理下载失败: ${e.message}`);
}
isDownloading = false;
currentDownloadVersion = null;
currentDownloadType = null;
currentDownloadPath = null;
}
if (mainWindow) {
mainWindow.destroy();
}
return { success: true };
});
// 取消关闭
ipcMain.handle('cancel-close', () => {
return { success: true };
});
// 取消启动游戏
let launchCancelFlag = false;
let currentGameProcess = null;
let currentGamePid = null;
function killGameProcessTree(pid) {
try {
const result = spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { windowsHide: true, timeout: 5000 });
if (result.status === 0) {
writeLog(`[启动] 已终止进程树 PID: ${pid}`);
return true;
}
writeLog(`[启动] taskkill PID ${pid} 返回: ${result.status}, ${result.stderr?.toString().trim()}`);
} catch (e) {
writeLog(`[启动] taskkill 失败: ${e.message}`);
}
return false;
}
function findAndKillMinecraftJavaProcesses() {
try {
const script = `
$javaProcs = Get-Process -Name "java","javaw" -ErrorAction SilentlyContinue
if ($javaProcs) {
foreach ($p in $javaProcs) {
try {
$cmdLine = (Get-CimInstance Win32_Process -Filter "ProcessId=$($p.Id)" -ErrorAction SilentlyContinue).CommandLine
if ($cmdLine -and ($cmdLine -like '*net.minecraft*' -or $cmdLine -like '*minecraft*' -or $cmdLine -like '*forge*' -or $cmdLine -like '*fabric*' -or $cmdLine -like '*optifine*')) {
Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue
Write-Output "KILLED:$($p.Id)"
}
} catch {}
}
}
`;
const result = spawnSync('powershell', ['-NoProfile', '-Command', script], { windowsHide: true, timeout: 8000, encoding: 'utf8' });
const output = (result.stdout || '').trim();
if (output) {
const pids = output.split('\n').filter(l => l.startsWith('KILLED:')).map(l => l.replace('KILLED:', ''));
writeLog(`[启动] 通过命令行匹配杀死了 ${pids.length} 个Minecraft Java进程: ${pids.join(', ')}`);
return pids.length > 0;
}
} catch (e) {
writeLog(`[启动] 查找Minecraft进程失败: ${e.message}`);
}
return false;
}
ipcMain.handle('cancel-launch', async () => {
launchCancelFlag = true;
writeLog('[启动] 用户取消启动');
let killed = false;
if (currentGameProcess) {
try {
const pid = currentGameProcess.pid;
if (pid) {
writeLog(`[启动] 尝试终止游戏进程 PID: ${pid}`);
killed = killGameProcessTree(pid);
try { currentGameProcess.kill(); } catch {}
}
} catch (e) {
writeLog(`[启动] 终止游戏进程异常: ${e.message}`);
}
currentGameProcess = null;
currentGamePid = null;
}
if (!killed) {
writeLog('[启动] 未找到已保存的游戏进程尝试通过命令行匹配查找Minecraft Java进程');
findAndKillMinecraftJavaProcesses();
}
if (mainWindow) {
mainWindow.webContents.send('game-closed', -1);
}
return { success: true };
});
// 检查更新使用Node.js网络请求避免CORS问题
const https = require('https');
ipcMain.handle('check-for-updates', async () => {
return new Promise((resolve) => {
const url = 'https://gamets.caellab.com/gpcl/data.json';
writeLog('[更新] 开始检查更新...');
https.get(url, (res) => {
let data = '';
res.setEncoding('utf8');
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const json = JSON.parse(data);
writeLog('[更新] 获取到数据: ' + data);
resolve({ success: true, data: json });
} catch (e) {
writeLog('[更新] JSON解析失败: ' + e.message);
resolve({ success: false, error: 'JSON解析失败: ' + e.message });
}
});
}).on('error', (e) => {
writeLog('[更新] 请求失败: ' + e.message);
resolve({ success: false, error: e.message });
});
});
});
// 获取当前版本号从package.json读取
ipcMain.handle('get-app-version', () => {
return APP_VERSION || app.getVersion() || '1.0.0';
});
// 打开外部链接
ipcMain.handle('open-external', (_, url) => {
try {
shell.openExternal(url);
return { success: true };
} catch (e) {
writeLog('[外部链接] 打开失败: ' + e.message);
return { success: false, error: e.message };
}
});
// IPC: 卸载Java运行时
ipcMain.handle('uninstall-java', async (_, javaVersion) => {
try {
const runtimeDir = getJavaRuntimeDir(javaVersion);
writeLog(`[Java] 开始卸载 Java ${javaVersion},目录: ${runtimeDir}`);
if (!fs.existsSync(runtimeDir)) {
return { success: false, error: `Java ${javaVersion} 未安装` };
}
fs.rmSync(runtimeDir, { recursive: true, force: true });
writeLog(`[Java] Java ${javaVersion} 已卸载,目录已删除: ${runtimeDir}`);
if (mainWindow) {
mainWindow.webContents.send('java-uninstalled', { javaVersion });
}
return { success: true };
} catch (e) {
writeLog(`[Java] 卸载 Java ${javaVersion} 失败: ${e.message}`);
return { success: false, error: e.message };
}
});
// 获取Java镜像源设置
ipcMain.handle('get-java-mirror-settings', () => {
try {
const settingsPath = path.join(GAME_DIR, 'settings', 'java_mirror.json');
if (fs.existsSync(settingsPath)) {
return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
}
return {
mirror: 'tsinghua',
customUrl: ''
};
} catch (e) {
return {
mirror: 'tsinghua',
customUrl: ''
};
}
});
// 获取Java版本对应的下载URL
ipcMain.handle('get-java-download-url', async (_, javaVersion) => {
try {
const settingsPath = path.join(GAME_DIR, 'settings', 'java_mirror.json');
let settings = {
mirror: 'tsinghua',
customUrl: ''
};
if (fs.existsSync(settingsPath)) {
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
}
// 根据镜像源生成URL
const url = getJavaDownloadUrl(javaVersion, settings.mirror, settings.customUrl);
writeLog(`[Java] 获取下载URL: Java ${javaVersion} - ${url}`);
return { success: true, url };
} catch (e) {
writeLog(`[Java] 获取下载URL失败: ${e.message}`);
return { success: false, error: e.message };
}
});
// 根据镜像源生成下载URL
function getJavaDownloadUrl(javaVersion, mirror, customUrl) {
// 清华大学镜像
const tsinghuaMirrors = {
"8": "https://mirrors.tuna.tsinghua.edu.cn/Adoptium/8/jre/x64/windows/OpenJDK8U-jre_x64_windows_hotspot_8u492b09.zip",
"17": "https://mirrors.tuna.tsinghua.edu.cn/Adoptium/17/jre/x64/windows/OpenJDK17U-jre_x64_windows_hotspot_17.0.19_10.zip",
"21": "https://mirrors.tuna.tsinghua.edu.cn/Adoptium/21/jre/x64/windows/OpenJDK21U-jre_x64_windows_hotspot_21.0.11_10.zip",
"25": "https://mirrors.tuna.tsinghua.edu.cn/Adoptium/25/jre/x64/windows/OpenJDK25U-jre_x64_windows_hotspot_25.0.3_9.zip"
};
// 如果使用清华大学镜像
if (mirror === 'tsinghua') {
return tsinghuaMirrors[javaVersion] || tsinghuaMirrors["17"];
}
// 如果使用自定义镜像
if (mirror === 'custom' && customUrl) {
// 替换{v}变量
let url = customUrl;
// Java版本号8, 17, 21等
url = url.replace(/\{v\}/gi, javaVersion);
// 完整版本号的映射
const versionMap = {
"8": "8u492b09",
"17": "17.0.19_10",
"21": "21.0.11_10",
"25": "25.0.3_9"
};
url = url.replace(/\{version\}/gi, versionMap[javaVersion] || javaVersion);
return url;
}
// 默认使用清华大学镜像
return tsinghuaMirrors[javaVersion] || tsinghuaMirrors["17"];
}
// 保存Java镜像源设置
function saveJavaMirrorSettings(mirror, customUrl) {
try {
const settingsDir = path.join(GAME_DIR, 'settings');
if (!fs.existsSync(settingsDir)) {
fs.mkdirSync(settingsDir, { recursive: true });
}
const settingsPath = path.join(settingsDir, 'java_mirror.json');
fs.writeFileSync(settingsPath, JSON.stringify({
mirror,
customUrl
}, null, 2));
writeLog(`[Java] 镜像源设置已保存: ${mirror}`);
return true;
} catch (e) {
writeLog(`[Java] 保存镜像源设置失败: ${e.message}`);
return false;
}
}
// 取消下载(只清理文件,不关闭窗口)
ipcMain.handle('cancel-download-only', async () => {
writeLog('[取消下载] 开始取消下载流程');
// 设置取消标志
shouldCancelDownload = true;
// 清空下载队列
const pendingCount = downloadQueue.length;
downloadQueue = [];
writeLog(`[取消下载] 已清空 ${pendingCount} 个待处理下载任务`);
// 等待当前活跃的下载完成最多等待2秒
const waitStart = Date.now();
while (activeDownloads > 0 && (Date.now() - waitStart) < 2000) {
await new Promise(r => setTimeout(r, 100));
}
writeLog(`[取消下载] 活跃下载数: ${activeDownloads}`);
if ((isDownloading && currentDownloadVersion) || isJavaDownloading) {
try {
if (currentDownloadType === 'game' && currentDownloadPath) {
// 清理游戏版本下载
const versionDir = currentDownloadPath;
if (fs.existsSync(versionDir)) {
const files = fs.readdirSync(versionDir);
for (const file of files) {
const filePath = path.join(versionDir, file);
try {
if (fs.statSync(filePath).isDirectory()) {
fs.rmSync(filePath, { recursive: true, force: true });
} else {
fs.unlinkSync(filePath);
}
} catch (e) {
writeLog(`清理文件失败: ${filePath} - ${e.message}`);
}
}
}
writeLog(`[取消下载] 已清理游戏版本下载目录: ${versionDir}`);
} else if (currentDownloadType === 'java' && currentDownloadPath) {
// 清理Java下载
const javaFile = currentDownloadPath;
if (fs.existsSync(javaFile)) {
try {
fs.unlinkSync(javaFile);
writeLog(`[取消下载] 已清理Java下载文件: ${javaFile}`);
} catch (e) {
writeLog(`清理Java文件失败: ${e.message}`);
}
}
}
} catch (e) {
console.error('清理下载失败:', e);
writeLog(`清理下载失败: ${e.message}`);
}
// 重置所有下载状态
isDownloading = false;
isJavaDownloading = false;
currentDownloadVersion = null;
currentDownloadType = null;
currentDownloadPath = null;
activeDownloads = 0;
totalDownloadFiles = 0;
completedDownloadFiles = 0;
}
// 通知渲染进程下载已取消
for (const w of BrowserWindow.getAllWindows()) {
w.webContents.send('download-cancelled');
}
writeLog('[取消下载] 取消下载流程完成');
return { success: true };
});
// 显示Java版本选择对话框
ipcMain.handle('show-java-install-dialog', async (_, options) => {
try {
const { mcVersion, recommendedJava } = options;
// 构建版本选择列表,按推荐程度排序
const versionPriority = {
[recommendedJava]: 0,
"17": 1,
"21": 2,
"25": 3,
"8": 4
};
const sortedVersions = [...JAVA_VERSIONS_AVAILABLE].sort((a, b) => {
return (versionPriority[a] || 99) - (versionPriority[b] || 99);
});
const result = await new Promise((resolve, reject) => {
javaDownloadResolve = resolve;
javaDownloadReject = reject;
// 发送消息给渲染进程显示对话框
if (mainWindow) {
mainWindow.webContents.send('show-java-install-dialog', {
mcVersion,
recommendedJava,
availableVersions: sortedVersions
});
} else {
reject(new Error('窗口未初始化'));
}
});
return result;
} catch (e) {
writeLog(`Java安装对话框错误: ${e.message}`);
return { success: false, cancelled: true };
}
});
// 用户在对话框中选择Java版本
ipcMain.handle('select-java-version', async (_, data) => {
const { selectedVersion, cancelled } = data;
if (cancelled) {
if (javaDownloadResolve) {
javaDownloadResolve({ success: false, cancelled: true });
javaDownloadResolve = null;
javaDownloadReject = null;
}
return { success: false, cancelled: true };
}
if (selectedVersion && javaDownloadResolve) {
javaDownloadResolve({ success: true, selectedVersion });
javaDownloadResolve = null;
javaDownloadReject = null;
return { success: true, selectedVersion };
}
return { success: false, cancelled: true };
});
// 下载并安装Java运行时
async function downloadAndInstallJava(javaVersion) {
const runtimeDir = getJavaRuntimeDir(javaVersion);
writeLog(`[Java] 开始安装 Java ${javaVersion} 运行时`);
try {
// 检查是否已存在
if (fs.existsSync(runtimeDir)) {
const exe = findJavaExecutable(runtimeDir);
if (exe) {
writeLog(`[Java] Java ${javaVersion} 已存在,无需下载`);
return { success: true, javaPath: exe };
}
}
// 获取Java运行时的下载URL支持自定义镜像源
const settingsPath = path.join(GAME_DIR, 'settings', 'java_mirror.json');
let settings = { mirror: 'tsinghua', customUrl: '' };
try {
if (fs.existsSync(settingsPath)) {
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
}
} catch (e) {}
const downloadUrl = getJavaDownloadUrl(javaVersion, settings.mirror, settings.customUrl);
writeLog(`[Java] 使用镜像源: ${settings.mirror}, URL: ${downloadUrl}`);
// 使用Electron webContents下载更可靠
const result = await downloadJavaWithWebContents(javaVersion, downloadUrl);
return result;
} catch (e) {
writeLog(`[Java] Java ${javaVersion} 安装失败: ${e.message}`);
// 清理失败的安装
if (fs.existsSync(runtimeDir)) {
try {
fs.rmSync(runtimeDir, { recursive: true, force: true });
} catch (e2) {}
}
return { success: false, error: e.message };
}
}
// 使用Electron webContents下载Java推荐方法
async function downloadJavaWithWebContents(javaVersion, downloadUrl) {
const runtimeName = JAVA_RUNTIME_MAP[javaVersion];
const runtimeDir = getJavaRuntimeDir(javaVersion);
const tempDir = path.join(CACHE_DIR, 'java_temp');
const tempFile = path.join(tempDir, `java-${javaVersion}.zip`);
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
writeLog(`[Java] 使用Electron下载 Java ${javaVersion}: ${downloadUrl}`);
// 设置下载路径,以便取消时能正确删除
currentDownloadVersion = `java-${javaVersion}`;
currentDownloadType = 'java';
currentDownloadPath = tempFile;
shouldCancelDownload = false;
isJavaDownloading = true;
return new Promise((resolve, reject) => {
const win = BrowserWindow.getFocusedWindow() || BrowserWindow.getAllWindows()[0];
if (!win) {
isJavaDownloading = false;
currentDownloadVersion = null;
currentDownloadType = null;
currentDownloadPath = null;
reject(new Error('没有可用的窗口'));
return;
}
// 使用webContents.downloadURL
win.webContents.downloadURL(downloadUrl, true);
// 监听下载开始
win.webContents.session.once('will-download', (event, item) => {
// 设置下载路径
item.setSavePath(tempFile);
const totalBytes = item.getTotalBytes();
writeLog(`[Java] 开始下载,总大小: ${totalBytes} bytes`);
// 监听进度
item.on('updated', (event, state) => {
if (state === 'progressing') {
const received = item.getReceivedBytes();
const percent = totalBytes > 0 ? Math.floor((received / totalBytes) * 100) : 0;
for (const w of BrowserWindow.getAllWindows()) {
w.webContents.send('download-progress', {
label: `Java ${javaVersion}`,
percent: percent,
bytesDownloaded: received,
current: completedDownloadFiles,
total: totalDownloadFiles
});
}
if (shouldCancelDownload) {
item.cancel();
writeLog(`[Java] 下载已取消`);
}
}
});
// 监听完成
item.on('done', (event, state) => {
if (state === 'cancelled') {
isJavaDownloading = false;
currentDownloadVersion = null;
currentDownloadType = null;
currentDownloadPath = null;
reject(new Error('下载已取消'));
return;
}
if (state === 'interrupted') {
isJavaDownloading = false;
currentDownloadVersion = null;
currentDownloadType = null;
currentDownloadPath = null;
reject(new Error('下载中断'));
return;
}
if (state === 'completed') {
writeLog(`[Java] 下载完成,开始解压...`);
// 验证文件
const fileStats = fs.statSync(tempFile);
const fileSize = fileStats.size;
if (fileSize < 1024) {
reject(new Error(`下载失败:文件太小 (${fileSize} bytes)`));
return;
}
// 检查ZIP头
const header = Buffer.alloc(4);
const fd = fs.openSync(tempFile, 'r');
fs.readSync(fd, header, 0, 4, 0);
fs.closeSync(fd);
if (header.toString('hex') !== '504b0304') {
reject(new Error('下载失败文件不是有效的ZIP格式'));
return;
}
writeLog(`[Java] 文件验证通过,大小: ${fileSize} bytes`);
// 解压
const parentDir = path.dirname(runtimeDir);
if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true });
}
if (fs.existsSync(runtimeDir)) {
fs.rmSync(runtimeDir, { recursive: true, force: true });
}
try {
const AdmZip = require('adm-zip');
const zip = new AdmZip(tempFile);
zip.extractAllTo(parentDir, false);
writeLog(`[Java] 解压完成`);
} catch (zipError) {
reject(new Error(`解压失败: ${zipError.message}`));
return;
}
// 重命名目录
const entries = fs.readdirSync(parentDir);
let extractedDir = null;
for (const entry of entries) {
const entryPath = path.join(parentDir, entry);
if (fs.statSync(entryPath).isDirectory()) {
if (entry.includes('jdk') || entry.includes('jre')) {
const binPath = path.join(entryPath, 'bin', 'java.exe');
if (fs.existsSync(binPath)) {
extractedDir = entryPath;
writeLog(`[Java] 找到目录: ${entry}`);
break;
}
}
}
}
if (extractedDir && extractedDir !== runtimeDir) {
if (fs.existsSync(runtimeDir)) {
fs.rmSync(runtimeDir, { recursive: true, force: true });
}
fs.renameSync(extractedDir, runtimeDir);
writeLog(`[Java] 已重命名为: ${runtimeName}`);
}
// 清理临时文件
try {
fs.unlinkSync(tempFile);
} catch (e) {}
// 验证安装
const javaPath = findJavaExecutable(runtimeDir);
if (!javaPath) {
reject(new Error('Java运行时安装失败未找到java.exe'));
return;
}
writeLog(`[Java] Java ${javaVersion} 安装成功: ${javaPath}`);
isJavaDownloading = false;
currentDownloadVersion = null;
currentDownloadType = null;
currentDownloadPath = null;
resolve({ success: true, javaPath });
} else {
isJavaDownloading = false;
currentDownloadVersion = null;
currentDownloadType = null;
currentDownloadPath = null;
reject(new Error(`下载失败: ${state}`));
}
});
});
// 超时处理10分钟
setTimeout(() => {
if (isJavaDownloading) {
writeLog(`[Java] 下载超时`);
isJavaDownloading = false;
currentDownloadVersion = null;
currentDownloadType = null;
currentDownloadPath = null;
reject(new Error('下载超时'));
}
}, 600000);
}).catch((e) => {
writeLog(`[Java] Java ${javaVersion} 安装失败: ${e.message}`);
if (fs.existsSync(runtimeDir)) {
try {
fs.rmSync(runtimeDir, { recursive: true, force: true });
} catch (e2) {}
}
return { success: false, error: e.message };
});
}
// 从Minecraft元数据获取Java运行时的下载信息
async function getJavaRuntimeDownloadInfo(javaVersion) {
try {
const runtimeKey = JAVA_DOWNLOAD_KEYS[javaVersion];
if (!runtimeKey) {
writeLog(`[Java] 不支持的Java版本: ${javaVersion}`);
return null;
}
writeLog(`[Java] 正在查找 Java ${javaVersion} (${runtimeKey}) 的下载信息...`);
// 使用清华大学开源软件镜像站TUNA的Adoptium镜像
const runtimeInfo = {
"8": {
id: "jre-legacy",
version: "8",
url: "https://mirrors.tuna.tsinghua.edu.cn/Adoptium/8/jre/x64/windows/OpenJDK8U-jre_x64_windows_hotspot_8u492b09.zip"
},
"17": {
id: "java-runtime-gamma",
version: "17",
url: "https://mirrors.tuna.tsinghua.edu.cn/Adoptium/17/jre/x64/windows/OpenJDK17U-jre_x64_windows_hotspot_17.0.19_10.zip"
},
"21": {
id: "java-runtime-delta",
version: "21",
url: "https://mirrors.tuna.tsinghua.edu.cn/Adoptium/21/jre/x64/windows/OpenJDK21U-jre_x64_windows_hotspot_21.0.11_10.zip"
},
"25": {
id: "java-runtime-epsilon",
version: "25",
url: "https://mirrors.tuna.tsinghua.edu.cn/Adoptium/25/jre/x64/windows/OpenJDK25U-jre_x64_windows_hotspot_25.0.3_9.zip"
}
};
const javaInfo = runtimeInfo[javaVersion];
if (javaInfo) {
writeLog(`[Java] ✓ 找到 Java ${javaVersion} 下载信息`);
writeLog(`[Java] URL: ${javaInfo.url}`);
return {
url: javaInfo.url,
name: javaInfo.id,
sha1: null,
size: null
};
}
writeLog(`[Java] ✗ 无法找到 Java ${javaVersion} 的下载信息`);
return null;
} catch (e) {
writeLog(`[Java] 获取Java运行时下载信息失败: ${e.message}`);
return null;
}
}
// 获取MC版本推荐的Java版本
function getRecommendedJavaVersion(mcVersion) {
// Minecraft 1.16.5及以下: Java 8
// Minecraft 1.17: Java 16/17
// Minecraft 1.18-1.20.4: Java 17
// Minecraft 1.20.5+: Java 21
// Minecraft 1.21+: Java 21/25
// Minecraft 26+: Java 21
const parts = mcVersion.split('.');
let major = parseInt(parts[0], 10);
let minor = parseInt(parts[1], 10);
let patch = parts.length > 2 ? parseInt(parts[2], 10) : 0;
// 如果版本号是 "26" 这种没有点的格式
if (isNaN(major)) major = 1;
// 处理像 "26" 这种格式(只有 major 没有 minor
if (isNaN(minor)) {
// 26 及以上使用 Java 25
if (major >= 26) return '25';
// 21-25 使用 Java 21
if (major >= 21) return '21';
return '17';
}
// 处理 1.x.x 的旧版本
if (major === 1) {
if (minor < 17) return '8';
if (minor === 17) return '17';
if (minor <= 20) return '17';
if (minor === 21 && patch <= 4) return '21';
if (minor === 21 && patch > 4) return '21';
if (minor >= 22) return '21';
return '17';
}
// 处理 2.x.x 和更大的版本21, 24, 26, ...
// 26 及以上使用 Java 25
if (major >= 26) return '25';
// 21-25 使用 Java 21
if (major >= 21) return '21';
return '17';
}
function findJavaExecutableInDir(rootDir) {
const stack = [rootDir];
while (stack.length > 0) {
const current = stack.pop();
if (!fs.existsSync(current)) continue;
const javaw = path.join(current, 'bin', 'javaw.exe');
const java = path.join(current, 'bin', 'java.exe');
if (fs.existsSync(javaw)) return javaw;
if (fs.existsSync(java)) return java;
try {
const entries = fs.readdirSync(current, { withFileTypes: true });
for (const e of entries) {
if (e.isDirectory()) stack.push(path.join(current, e.name));
}
} catch (e) {}
}
return null;
}
async function fetchText(url) {
return new Promise((resolve, reject) => {
let finished = false;
const fallbackToHttps = (reason) => {
if (finished) return;
try {
const https = require('https');
const req2 = https.get(url, { timeout: 20000 }, res2 => {
let body = '';
res2.on('data', d => body += d);
res2.on('end', () => { finished = true; resolve(body); });
res2.on('error', err2 => { if (!finished) { finished = true; reject(err2); } });
});
req2.on('timeout', () => { req2.destroy(); if (!finished) { finished = true; reject('超时'); } });
req2.on('error', err2 => { if (!finished) { finished = true; reject(err2); } });
} catch (e2) {
if (!finished) { finished = true; reject(e2); }
}
};
try {
const req = net.request(url);
const to = setTimeout(() => { try { req.abort(); } catch {} ; fallbackToHttps('timeout'); }, 20000);
req.on('response', res => {
clearTimeout(to);
let body = '';
res.on('data', d => body += d);
res.on('end', () => { if (!finished) { finished = true; resolve(body); } });
res.on('error', err => { if (!finished) fallbackToHttps(err); });
});
req.on('error', err => { if (!finished) fallbackToHttps(err); });
req.end();
} catch (e) {
fallbackToHttps(e);
}
});
}
let MAX_CONCURRENT = 128;
let downloadQueue = [];
let activeDownloads = 0;
let totalDownloadFiles = 0;
let completedDownloadFiles = 0;
let currentDownloadSpeed = 0;
async function downloadWithPool(url, dest, label) {
return new Promise((resolve, reject) => {
downloadQueue.push({ url, dest, label, resolve, reject });
processDownloadQueue();
});
}
async function processDownloadQueue() {
while (downloadQueue.length > 0 && activeDownloads < MAX_CONCURRENT && !shouldCancelDownload) {
const task = downloadQueue.shift();
activeDownloads++;
downloadFileInternal(task.url, task.dest, task.label)
.then(() => {
task.resolve();
})
.catch(err => {
task.reject(err);
})
.finally(() => {
activeDownloads--;
completedDownloadFiles++;
processDownloadQueue();
});
}
// 如果取消下载,拒绝队列中所有剩余任务
if (shouldCancelDownload) {
while (downloadQueue.length > 0) {
const task = downloadQueue.shift();
task.reject(new Error('下载已取消'));
}
// 通知渲染进程下载已取消
for (const w of BrowserWindow.getAllWindows()) {
w.webContents.send('download-cancelled');
}
}
}
async function downloadFile(url, dest, label) {
if (fs.existsSync(dest)) {
const stat = fs.statSync(dest);
if (stat.size > 1024) {
for (const w of BrowserWindow.getAllWindows()) {
w.webContents.send('download-progress', {
label,
percent: 100,
current: completedDownloadFiles,
total: totalDownloadFiles
});
}
return;
}
}
await downloadWithPool(url, dest, label);
}
async function downloadFileInternal(url, dest, label) {
return new Promise((resolve, reject) => {
const dir = path.dirname(dest);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
// 确保使用二进制模式写入
const file = fs.createWriteStream(dest, { encoding: null });
let loaded = 0;
let total = 0;
const fallbackToHttps = (reason) => {
writeLog(`[下载] ${label} HTTP请求失败: ${reason}尝试HTTPS`);
try {
const https = require('https');
const options = {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
};
const req2 = https.get(url, options, res2 => {
// 处理重定向
if (res2.statusCode >= 300 && res2.statusCode < 400 && res2.headers.location) {
writeLog(`[下载] ${label} 重定向到: ${res2.headers.location}`);
file.close();
// 递归处理重定向
downloadFileInternal(res2.headers.location, dest, label).then(resolve).catch(reject);
return;
}
total = parseInt(res2.headers['content-length'] || '0', 10);
// 检查内容类型
const contentType = res2.headers['content-type'] || '';
if (!contentType.includes('zip') && !contentType.includes('octet-stream')) {
writeLog(`[下载] ${label} 警告: 内容类型是 ${contentType}可能不是ZIP文件`);
}
writeLog(`[下载] ${label} 开始下载,大小: ${total} bytes`);
res2.on('data', d => {
if (shouldCancelDownload) {
req2.destroy();
file.destroy();
reject(new Error('下载已取消'));
return;
}
loaded += d.length;
file.write(d);
const p = total > 0 ? Math.floor((loaded / total) * 100) : 0;
for (const w of BrowserWindow.getAllWindows()) {
w.webContents.send('download-progress', {
label,
percent: p,
bytesDownloaded: loaded,
current: completedDownloadFiles,
total: totalDownloadFiles
});
}
});
res2.on('end', () => {
file.end();
writeLog(`[下载] ${label} 下载完成,实际大小: ${loaded} bytes`);
resolve();
});
res2.on('error', err2 => {
file.destroy();
reject(err2);
});
});
req2.on('timeout', () => { req2.destroy(); reject('超时'); });
req2.on('error', err2 => {
file.destroy();
reject(err2);
});
} catch (e2) {
file.destroy();
reject(e2);
}
};
try {
const req = net.request(url);
const to = setTimeout(() => { try { req.abort(); } catch {} ; fallbackToHttps('timeout'); }, 60000);
// 设置User-Agent头
req.setHeader('User-Agent', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36');
req.on('response', res => {
clearTimeout(to);
// 处理重定向
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
writeLog(`[下载] ${label} 重定向到: ${res.headers.location}`);
file.close();
// 递归处理重定向
downloadFileInternal(res.headers.location, dest, label).then(resolve).catch(reject);
return;
}
total = parseInt(res.headers['content-length'] || '0', 10);
// 检查内容类型
const contentType = res.headers['content-type'] || '';
if (!contentType.includes('zip') && !contentType.includes('octet-stream')) {
writeLog(`[下载] ${label} 警告: 内容类型是 ${contentType}可能不是ZIP文件`);
}
writeLog(`[下载] ${label} 开始下载,大小: ${total} bytes`);
res.on('data', d => {
if (shouldCancelDownload) {
try { req.abort(); } catch {}
file.destroy();
reject(new Error('下载已取消'));
return;
}
loaded += d.length;
file.write(d);
const p = total > 0 ? Math.floor((loaded / total) * 100) : 0;
for (const w of BrowserWindow.getAllWindows()) {
w.webContents.send('download-progress', {
label,
percent: p,
bytesDownloaded: loaded,
current: completedDownloadFiles,
total: totalDownloadFiles
});
}
});
res.on('end', () => {
file.end();
writeLog(`[下载] ${label} 下载完成,实际大小: ${loaded} bytes`);
resolve();
});
res.on('error', err => {
file.destroy();
fallbackToHttps(err);
});
});
req.on('error', err => {
file.destroy();
fallbackToHttps(err);
});
req.end();
} catch (e) {
file.destroy();
fallbackToHttps(e);
}
});
}
// 带超时的下载包装器,超时后会抛出错误让调用方尝试下一个源
async function downloadFileInternalWithTimeout(url, dest, label, timeoutMs) {
return new Promise((resolve, reject) => {
let settled = false;
const timer = setTimeout(() => {
if (!settled) {
settled = true;
reject(new Error(`连接超时 (${timeoutMs / 1000}秒)`));
}
}, timeoutMs);
downloadFileInternal(url, dest, label)
.then(result => {
if (!settled) {
settled = true;
clearTimeout(timer);
resolve(result);
}
})
.catch(err => {
if (!settled) {
settled = true;
clearTimeout(timer);
reject(err);
}
});
});
}
async function installVersion(versionId, maxConcurrent = 20, downloadPath = null) {
const targetDir = downloadPath || path.join(GAME_DIR, 'versions', versionId);
const versionDir = targetDir;
const cancelFilePath = path.join(versionDir, 'CancelDownload.txt');
if (fs.existsSync(cancelFilePath)) {
try { fs.unlinkSync(cancelFilePath); } catch (e) {}
}
// 重置取消标志
shouldCancelDownload = false;
isDownloading = true;
currentDownloadVersion = versionId;
currentDownloadType = 'game';
currentDownloadPath = targetDir;
writeLog(`开始下载:${versionId}${targetDir}`);
try {
MAX_CONCURRENT = maxConcurrent;
downloadQueue = [];
activeDownloads = 0;
totalDownloadFiles = 0;
completedDownloadFiles = 0;
const versionDir = path.join(GAME_DIR, 'versions', versionId);
if (!fs.existsSync(versionDir)) fs.mkdirSync(versionDir, { recursive: true });
const versionJsonPath = path.join(versionDir, versionId + '.json');
const versionJarPath = path.join(versionDir, versionId + '.jar');
// 获取版本清单
const manifest = JSON.parse(await fetchText('https://piston-meta.mojang.com/mc/game/version_manifest_v2.json'));
const vInfo = manifest.versions.find(v => v.id === versionId);
if (!vInfo) throw new Error('版本不存在: ' + versionId);
// 下载版本JSON
if (!fs.existsSync(versionJsonPath)) {
const vData = await fetchText(vInfo.url);
fs.writeFileSync(versionJsonPath, vData);
}
const versionData = JSON.parse(fs.readFileSync(versionJsonPath, 'utf8'));
// 下载客户端JAR
if (!fs.existsSync(versionJarPath)) {
await downloadFile(versionData.downloads.client.url, versionJarPath, '客户端');
}
// 检查是否取消
if (shouldCancelDownload) {
writeLog(`下载取消:${versionId}`);
throw new Error('下载已取消');
}
// 下载资源索引
const assetIndexPath = path.join(GAME_DIR, 'assets', 'indexes', versionData.assetIndex.id + '.json');
if (!fs.existsSync(assetIndexPath)) {
await downloadFile(versionData.assetIndex.url, assetIndexPath, '资源索引');
}
// 检查是否取消
if (shouldCancelDownload) {
writeLog(`下载取消:${versionId}`);
throw new Error('下载已取消');
}
// 等待资源索引下载完成并读取
let assetIndexData;
for (let i = 0; i < 10; i++) {
try {
assetIndexData = JSON.parse(fs.readFileSync(assetIndexPath, 'utf8'));
break;
} catch (e) {
await new Promise(r => setTimeout(r, 500));
}
}
if (!assetIndexData) throw new Error('无法读取资源索引');
// 收集所有需要下载的文件
const toDownload = [];
// 资源文件
for (const [name, obj] of Object.entries(assetIndexData.objects)) {
const hash = obj.hash;
const prefix = hash.substring(0, 2);
const assetPath = path.join(GAME_DIR, 'assets', 'objects', prefix, hash);
if (!fs.existsSync(assetPath)) {
toDownload.push({
url: `https://resources.download.minecraft.net/${prefix}/${hash}`,
dest: assetPath,
label: `资源: ${name}`
});
}
}
// 库文件
for (const lib of versionData.libraries) {
if (lib.downloads?.artifact) {
const libPath = path.join(GAME_DIR, 'libraries', lib.downloads.artifact.path);
if (!fs.existsSync(libPath)) {
toDownload.push({
url: lib.downloads.artifact.url,
dest: libPath,
label: `库: ${lib.name}`
});
}
}
if (lib.downloads?.classifiers) {
for (const [cls, info] of Object.entries(lib.downloads.classifiers)) {
if (cls.includes('natives-windows')) {
const libPath = path.join(GAME_DIR, 'libraries', info.path);
if (!fs.existsSync(libPath)) {
toDownload.push({
url: info.url,
dest: libPath,
label: `原生库: ${lib.name}`
});
}
}
}
}
}
totalDownloadFiles = toDownload.length;
completedDownloadFiles = 0;
// 并发下载所有文件
const downloadPromises = toDownload.map(item => downloadFile(item.url, item.dest, item.label));
await Promise.all(downloadPromises);
// 解压原生库
const nativesDir = path.join(GAME_DIR, 'versions', versionId, 'natives');
if (!fs.existsSync(nativesDir)) fs.mkdirSync(nativesDir, { recursive: true });
for (const lib of versionData.libraries) {
if (lib.downloads?.classifiers) {
for (const [cls, info] of Object.entries(lib.downloads.classifiers)) {
if (cls.includes('natives-windows')) {
const libPath = path.join(GAME_DIR, 'libraries', info.path);
if (fs.existsSync(libPath)) {
const AdmZip = require('adm-zip');
const zip = new AdmZip(libPath);
zip.extractAllTo(nativesDir, true);
}
}
}
}
}
if (shouldCancelDownload) {
writeLog(`下载取消:${versionId}`);
throw new Error('下载已取消');
}
writeLog(`下载完成:${versionId}`);
// 更新版本最后启动时间
updateVersionLastPlayed(versionId, targetDir);
return { success: true };
} catch (e) {
writeLog(`下载失败:${versionId} - ${e.message}`);
return { success: false, error: e.message };
} finally {
isDownloading = false;
currentDownloadVersion = null;
currentDownloadType = null;
currentDownloadPath = null;
shouldCancelDownload = false;
downloadQueue = [];
activeDownloads = 0;
totalDownloadFiles = 0;
completedDownloadFiles = 0;
}
}
// 更新版本的最后启动时间
function updateVersionLastPlayed(versionId, targetDir) {
try {
const versionDir = targetDir || path.join(GAME_DIR, 'versions', versionId);
const gpclDir = path.join(versionDir, 'gpcl');
const configPath = path.join(gpclDir, 'config.json');
let settings = {};
// 读取现有配置
if (fs.existsSync(configPath)) {
try {
settings = JSON.parse(fs.readFileSync(configPath, 'utf8'));
} catch (e) {
settings = {};
}
}
// 更新最后启动时间
settings.lastPlayed = new Date().toISOString();
// 确保目录存在
if (!fs.existsSync(gpclDir)) {
fs.mkdirSync(gpclDir, { recursive: true });
}
// 保存配置
fs.writeFileSync(configPath, JSON.stringify(settings, null, 2), 'utf8');
writeLog(`[版本] ${versionId} 最后启动时间已更新: ${settings.lastPlayed}`);
} catch (e) {
writeLog(`[版本] 更新 ${versionId} 最后启动时间失败: ${e.message}`);
}
}
ipcMain.handle('scan-versions', async (_, gameDir, silent = false) => {
const targetDir = gameDir ? path.resolve(gameDir) : GAME_DIR;
const versionsDir = path.join(targetDir, 'versions');
if (!fs.existsSync(versionsDir)) {
if (!silent) writeLog('扫描本地版本未找到versions目录');
return [];
}
const entries = fs.readdirSync(versionsDir, { withFileTypes: true });
const versions = [];
const skippedVersions = [];
for (const entry of entries) {
if (entry.isDirectory()) {
const versionDir = path.join(versionsDir, entry.name);
try {
const files = fs.readdirSync(versionDir);
// 检查是否只有一个 CancelDownload.txt 文件
if (files.length === 1 && files[0] === 'CancelDownload.txt') {
skippedVersions.push(entry.name);
if (!silent) writeLog(`扫描本地版本:跳过空目录 "${entry.name}"(仅包含 CancelDownload.txt`);
continue;
}
// 查找所有 .json 文件
const jsonFiles = files.filter(f => f.endsWith('.json'));
// 如果没有 .json 文件,跳过
if (jsonFiles.length === 0) {
skippedVersions.push(entry.name);
if (!silent) writeLog(`扫描本地版本:跳过 "${entry.name}"(无 .json 文件)`);
continue;
}
// 收集所有版本(包括原版和带模组加载器的版本)
// 原版: 1.21.1 -> versionId = "1.21.1"
// Forge: 1.21.1-forge -> versionId = "1.21.1-forge"
// Fabric: 1.21.1-fabric -> versionId = "1.21.1-fabric"
const baseVersionId = entry.name; // 文件夹名
const mcVersion = baseVersionId; // MC 版本号
// 总是添加原版
if (jsonFiles.includes(`${baseVersionId}.json`)) {
versions.push(baseVersionId);
}
// 检查是否有模组加载器版本
for (const jsonFile of jsonFiles) {
const jsonVersionId = jsonFile.replace('.json', '');
// 如果不是原版,且不在列表中,添加它
if (jsonVersionId !== baseVersionId && !versions.includes(jsonVersionId)) {
versions.push(jsonVersionId);
}
}
} catch (e) {
writeLog(`扫描本地版本:读取目录 "${entry.name}" 失败 - ${e.message}`);
}
}
}
if (skippedVersions.length > 0 && !silent) {
writeLog(`扫描本地版本:跳过 ${skippedVersions.length} 个无效目录 - [${skippedVersions.join(', ')}]`);
}
if (!silent) writeLog(`扫描本地版本:共 ${versions.length} 个版本 - [${versions.join(', ')}]`);
return versions;
});
ipcMain.handle('get-version-manifest', async () => {
try {
if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true });
const cacheFile = path.join(CACHE_DIR, 'version_manifest.json');
let data;
let fromCache = false;
if (fs.existsSync(cacheFile)) {
const stat = fs.statSync(cacheFile);
const age = Date.now() - stat.mtimeMs;
if (age < 10 * 60 * 1000) {
data = fs.readFileSync(cacheFile, 'utf8');
fromCache = true;
}
}
if (!data) {
try {
data = await fetchText('https://piston-meta.mojang.com/mc/game/version_manifest_v2.json');
fs.writeFileSync(cacheFile, data);
writeLog('远程拉取版本列表:成功');
} catch (e) {
writeLog(`远程拉取版本列表:失败 - ${e.message}`);
return [];
}
} else {
writeLog(`读取本地缓存版本列表:成功`);
}
const parsed = JSON.parse(data);
// 返回所有类型的版本,包括正式版、快照版、旧测试版
writeLog(`版本列表:共 ${parsed.versions.length} 个版本`);
return parsed.versions;
} catch (e) {
writeLog(`版本列表解析失败:${e.message}`);
return [];
}
});
ipcMain.handle('download-version', (event, versionId, maxConcurrent, downloadPath) => installVersion(versionId, maxConcurrent, downloadPath));
ipcMain.handle('download-with-modloader', (event, versionId, loaderType, loaderVersion, maxConcurrent) =>
installVersionWithModLoader(versionId, loaderType, loaderVersion, maxConcurrent));
ipcMain.handle('get-version-config', (event, versionId) => readVersionConfig(versionId));
ipcMain.handle('get-version-display-name', (event, versionId) => generateVersionDisplayName(versionId));
ipcMain.handle('get-launch-display-name', (event, versionId) => generateLaunchDisplayName(versionId));
ipcMain.handle('delete-version', async (_, versionId) => {
try {
const versionDir = path.join(GAME_DIR, 'versions', versionId);
if (!fs.existsSync(versionDir)) {
return { success: false, error: '版本不存在' };
}
writeLog(`[删除版本] 开始删除版本: ${versionId}`);
fs.rmSync(versionDir, { recursive: true, force: true });
writeLog(`[删除版本] 版本 ${versionId} 删除成功`);
return { success: true };
} catch (e) {
writeLog(`[删除版本] 删除版本失败: ${e.message}`);
return { success: false, error: e.message };
}
});
ipcMain.handle('get-version-settings', async (_, versionId) => {
try {
const versionDir = path.join(GAME_DIR, 'versions', versionId);
const gpclDir = path.join(versionDir, 'gpcl');
const configPath = path.join(gpclDir, 'config.json');
if (!fs.existsSync(configPath)) {
return { success: true, settings: null };
}
const content = fs.readFileSync(configPath, 'utf8');
const settings = JSON.parse(content);
return { success: true, settings };
} catch (e) {
writeLog(`[版本设置] 读取版本 ${versionId} 设置失败: ${e.message}`);
return { success: false, error: e.message, settings: null };
}
});
ipcMain.handle('save-version-settings', async (_, versionId, settings) => {
try {
const versionDir = path.join(GAME_DIR, 'versions', versionId);
const gpclDir = path.join(versionDir, 'gpcl');
if (!fs.existsSync(gpclDir)) {
fs.mkdirSync(gpclDir, { recursive: true });
}
const configPath = path.join(gpclDir, 'config.json');
// 保存设置
fs.writeFileSync(configPath, JSON.stringify(settings, null, 2), 'utf8');
writeLog(`[版本设置] 版本 ${versionId} 设置已保存`);
return { success: true };
} catch (e) {
writeLog(`[版本设置] 保存版本 ${versionId} 设置失败: ${e.message}`);
return { success: false, error: e.message };
}
});
ipcMain.handle('get-game-dir', () => {
return GAME_DIR;
});
ipcMain.handle('get-player-name', () => {
try {
const playerFile = path.join(app.getPath('userData'), 'gpcl', 'player.json');
if (fs.existsSync(playerFile)) {
const data = JSON.parse(fs.readFileSync(playerFile, 'utf8'));
return data.name || 'GPCL_Player';
}
} catch (e) {
console.error('读取玩家名失败:', e);
}
return 'GPCL_Player';
});
ipcMain.handle('save-player-name', (_, name) => {
try {
const gpclDir = path.join(app.getPath('userData'), 'gpcl');
if (!fs.existsSync(gpclDir)) fs.mkdirSync(gpclDir, { recursive: true });
const playerFile = path.join(gpclDir, 'player.json');
fs.writeFileSync(playerFile, JSON.stringify({ name: name || 'GPCL_Player' }));
return { success: true };
} catch (e) {
console.error('保存玩家名失败:', e);
return { success: false, error: e.message };
}
});
ipcMain.handle('window-minimize', () => {
if (mainWindow) mainWindow.minimize();
return { success: true };
});
ipcMain.handle('window-focus', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
mainWindow.show();
mainWindow.focus();
}
return { success: true };
});
ipcMain.handle('window-close', () => {
if (mainWindow) mainWindow.close();
return { success: true };
});
ipcMain.handle('select-directory', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory']
});
if (result.canceled || result.filePaths.length === 0) {
return null;
}
return result.filePaths[0];
});
// 检查指定Minecraft版本所需的Java是否已安装
ipcMain.handle('check-java', async (_, versionOrJavaVersion) => {
try {
// 判断是Minecraft版本号还是Java版本号
const isMcVersion = versionOrJavaVersion && versionOrJavaVersion.match(/^\d+\.\d+/);
if (isMcVersion) {
// Minecraft版本检查该版本需要的Java
const versionId = versionOrJavaVersion;
const jsonPath = path.join(GAME_DIR, 'versions', versionId, versionId + '.json');
if (!fs.existsSync(jsonPath)) {
return { success: false, error: '版本未下载' };
}
const versionData = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
const javaVersion = getJavaVersionFromVersionData(versionData);
const result = await checkJavaInstalled(javaVersion);
return {
success: true,
installed: result.installed,
javaVersion: javaVersion,
javaPath: result.javaPath
};
} else {
// Java版本直接检查该版本是否安装
const javaVersion = versionOrJavaVersion;
const result = await checkJavaInstalled(javaVersion);
return {
success: true,
installed: result.installed,
javaVersion: javaVersion,
javaPath: result.javaPath
};
}
} catch (e) {
writeLog(`检查Java失败${e.message}`);
return { success: false, error: e.message };
}
});
// 解析 Minecraft 启动参数模板
function parseMCArguments(template, vars) {
let result = template;
for (const [key, value] of Object.entries(vars)) {
result = result.replace(new RegExp(`\\$\\{${key}\\}`, 'g'), value);
}
return result;
}
// 智能分割参数(处理引号内的空格)
function splitArgs(str) {
const args = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < str.length; i++) {
const c = str[i];
if (c === '"') {
inQuotes = !inQuotes;
} else if (c === ' ' && !inQuotes) {
if (current.length > 0) {
args.push(current);
current = '';
}
} else {
current += c;
}
}
if (current.length > 0) {
args.push(current);
}
return args;
}
// 检查库规则是否允许当前平台
function checkLibraryRules(rules) {
if (!rules || rules.length === 0) return true;
let allowed = false;
for (const rule of rules) {
let match = true;
if (rule.os) {
const platform = os.platform();
if (rule.os.name === 'windows' && platform !== 'win32') match = false;
if (rule.os.name === 'linux' && platform !== 'linux') match = false;
if (rule.os.name === 'osx' && platform !== 'darwin') match = false;
}
if (match) {
allowed = (rule.action === 'allow');
}
}
return allowed;
}
// 构建 Minecraft 启动参数
function buildLaunchArgs(vd, gameDir, versionId, username, windowMode, memoryMB, serverIp) {
const cp = [];
// 优先使用 downloads.client 的 jar 路径
const clientJar = path.join(gameDir, 'versions', versionId, `${versionId}.jar`);
if (fs.existsSync(clientJar)) {
cp.push(clientJar);
}
// 添加库(带平台规则过滤)
let missingLibs = 0;
for (const lib of vd.libraries || []) {
if (!checkLibraryRules(lib.rules)) continue;
// 普通 artifact
if (lib.downloads?.artifact?.path) {
const libPath = path.join(gameDir, 'libraries', lib.downloads.artifact.path);
if (fs.existsSync(libPath)) {
cp.push(libPath);
} else {
missingLibs++;
writeLog(`[启动] 库文件缺失: ${lib.downloads.artifact.path}`);
}
}
// NativesWindows 平台)
if (lib.downloads?.classifiers?.['natives-windows']?.path) {
const nativePath = path.join(gameDir, 'libraries', lib.downloads.classifiers['natives-windows'].path);
if (fs.existsSync(nativePath)) {
cp.push(nativePath);
} else {
missingLibs++;
writeLog(`[启动] Native库缺失: ${lib.downloads.classifiers['natives-windows'].path}`);
}
}
}
if (missingLibs > 0) {
writeLog(`[启动] 警告: ${missingLibs} 个库文件缺失`);
}
const nativesDir = path.join(gameDir, 'versions', versionId, 'natives');
const assetsDir = path.join(gameDir, 'assets');
const jvmArgs = [
`-Djava.library.path=${nativesDir}`,
'-cp', cp.join(';'),
];
// 添加内存参数
if (memoryMB) {
jvmArgs.unshift(`-Xmx${memoryMB}M`);
writeLog(`[启动] 已设置最大内存: ${memoryMB}M`);
} else {
writeLog(`[启动] 未设置内存限制`);
}
const vars = {
auth_player_name: username,
version_name: versionId,
game_directory: gameDir,
assets_root: assetsDir,
assets_index_name: vd.assetIndex?.id || vd.assets || versionId,
auth_uuid: '00000000000000000000000000000000',
auth_access_token: 'offline',
auth_session: 'offline',
user_type: 'legacy',
user_properties: '{}',
version_type: 'GPCL'
};
let gameArgs = [];
// 新版格式 (1.13+)
if (vd.arguments?.game) {
for (const arg of vd.arguments.game) {
if (typeof arg === 'string') {
gameArgs.push(parseMCArguments(arg, vars));
}
// 忽略条件参数(简化处理)
}
}
// 旧版格式 (1.12.2 及以下)
else if (vd.minecraftArguments) {
const parsed = parseMCArguments(vd.minecraftArguments, vars);
gameArgs = splitArgs(parsed);
}
// 兜底
else {
gameArgs = [
'--username', username,
'--version', versionId,
'--gameDir', gameDir,
'--assetsDir', assetsDir,
'--assetIndex', vd.assetIndex?.id || vd.assets || versionId,
'--accessToken', 'offline',
'--userType', 'legacy'
];
}
// 根据窗口模式添加参数
if (windowMode === 'fullscreen') {
gameArgs.push('--fullscreen');
writeLog(`[启动] 已添加全屏参数: --fullscreen`);
} else if (windowMode === 'borderless') {
// 无边框窗口模式:使用全屏但无边框的分辨率
gameArgs.push('--fullscreen');
gameArgs.push('--width');
gameArgs.push('1920');
gameArgs.push('--height');
gameArgs.push('1080');
writeLog(`[启动] 已添加无边框窗口参数: --fullscreen --width 1920 --height 1080`);
} else if (windowMode === 'minimized') {
// 最小化模式,不添加特殊参数,但会在游戏启动后自动最小化
writeLog(`[启动] 已设置最小化模式`);
}
// 如果有服务器IP添加服务器连接参数
if (serverIp && serverIp.trim()) {
const ipParts = serverIp.trim().split(':');
const serverAddress = ipParts[0];
const serverPort = ipParts[1] || '25565';
gameArgs.push('--server');
gameArgs.push(serverAddress);
gameArgs.push('--port');
gameArgs.push(serverPort);
writeLog(`[启动] 已添加服务器连接参数: --server ${serverAddress} --port ${serverPort}`);
}
return { jvmArgs, gameArgs, mainClass: vd.mainClass };
}
ipcMain.handle('launch-minecraft', async (_, opt) => {
try {
// 重置取消标志
launchCancelFlag = false;
const { versionId, username, gameDir: rawGameDir, windowMode } = opt;
// 使用传入的路径或默认的 GAME_DIR
const gameDir = rawGameDir ? path.resolve(rawGameDir) : GAME_DIR;
// 窗口模式fullscreen, windowed, borderless
const mode = windowMode || 'windowed';
writeLog(`启动游戏:玩家 ${username},版本 ${versionId},目录: ${gameDir},窗口模式: ${mode}`);
// 检查是否已取消
if (launchCancelFlag) {
writeLog('[启动] 用户已取消启动');
return { success: false, error: '启动已取消', cancelled: true };
}
const jsonPath = path.join(gameDir, 'versions', versionId, `${versionId}.json`);
if (!fs.existsSync(jsonPath)) {
writeLog(`启动游戏失败:版本 ${versionId} 未下载`);
return { success: false, error: '请先下载游戏' };
}
const vd = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
const javaVersion = getJavaVersionFromVersionData(vd);
// 检查是否已取消
if (launchCancelFlag) {
writeLog('[启动] 用户已取消启动');
return { success: false, error: '启动已取消', cancelled: true };
}
// 加载设置
const settings = loadSettings();
let memoryMB = null;
let actualWindowMode = mode;
let serverIp = null;
// 首先检查是否有版本特定设置
try {
const versionConfigPath = path.join(gameDir, 'versions', versionId, 'gpcl', 'config.json');
if (fs.existsSync(versionConfigPath)) {
const versionSettings = JSON.parse(fs.readFileSync(versionConfigPath, 'utf8'));
// 处理内存设置
if (versionSettings.memory && versionSettings.memory !== 'global') {
const memoryVal = parseFloat(versionSettings.memory);
if (!isNaN(memoryVal)) {
if (versionSettings.memory === '0.5') {
memoryMB = 512; // 0.5GB = 512MB
} else {
memoryMB = Math.floor(memoryVal * 1024);
}
writeLog(`[启动] 使用版本内存设置: ${memoryMB}M`);
}
}
// 处理窗口模式设置
if (versionSettings.windowMode && versionSettings.windowMode !== 'global') {
actualWindowMode = versionSettings.windowMode;
writeLog(`[启动] 使用版本窗口模式: ${actualWindowMode}`);
}
// 处理启动模式和服务器IP
if (versionSettings.startupMode === 'join' && versionSettings.serverIp) {
serverIp = versionSettings.serverIp;
writeLog(`[启动] 使用版本启动模式: 加入服务器 ${serverIp}`);
}
}
} catch (e) {
writeLog(`[启动] 读取版本设置失败: ${e.message}`);
}
// 如果没有版本特定设置,使用全局设置
if (memoryMB === null && settings.game?.memory) {
const globalMemory = parseFloat(settings.game.memory);
if (!isNaN(globalMemory)) {
memoryMB = Math.floor(globalMemory * 1024);
writeLog(`[启动] 使用全局内存设置: ${memoryMB}M`);
}
}
// 检查是否已取消
if (launchCancelFlag) {
writeLog('[启动] 用户已取消启动');
return { success: false, error: '启动已取消', cancelled: true };
}
// 检查Java是否已安装
let java = await ensureJavaRuntime(javaVersion);
// 检查是否已取消在Java检查后
if (launchCancelFlag) {
writeLog('[启动] 用户已取消启动');
return { success: false, error: '启动已取消', cancelled: true };
}
// 如果Java未安装自动下载
if (!java) {
writeLog(`[Java] Java ${javaVersion} 未安装,自动开始下载`);
// 发送通知给渲染进程
if (mainWindow) {
mainWindow.webContents.send('java-auto-download-start', {
javaVersion: javaVersion,
mcVersion: versionId
});
}
// 自动下载并安装Java期间检查取消标志
const installResult = await downloadAndInstallJava(javaVersion);
// 检查是否已取消在Java下载后
if (launchCancelFlag) {
writeLog('[启动] 用户已取消启动Java下载完成后');
return { success: false, error: '启动已取消', cancelled: true };
}
if (installResult.success) {
java = installResult.javaPath;
writeLog(`[Java] Java ${javaVersion} 安装成功: ${java}`);
// 发送安装完成通知
if (mainWindow) {
mainWindow.webContents.send('java-auto-download-complete', {
javaVersion: javaVersion,
javaPath: java
});
}
} else {
writeLog(`[Java] Java ${javaVersion} 安装失败: ${installResult.error}`);
return {
success: false,
error: `Java ${javaVersion} 安装失败: ${installResult.error}`
};
}
}
// 最终检查是否已取消(在启动游戏前)
if (launchCancelFlag) {
writeLog('[启动] 用户已取消启动(即将启动游戏前)');
return { success: false, error: '启动已取消', cancelled: true };
}
const { jvmArgs, gameArgs, mainClass } = buildLaunchArgs(vd, gameDir, versionId, username, actualWindowMode, memoryMB, serverIp);
const args = [...jvmArgs, mainClass, ...gameArgs];
writeLog(`[启动] Java: ${java}`);
writeLog(`[启动] 主类: ${mainClass}`);
writeLog(`[启动] 窗口模式: ${actualWindowMode}`);
writeLog(`[启动] 参数数: ${args.length}`);
writeLog(`[启动] JVM参数: ${JSON.stringify(jvmArgs)}`);
writeLog(`[启动] 游戏参数: ${JSON.stringify(gameArgs.slice(0, 10))}...`);
writeLog(`[启动] Classpath条目数: ${jvmArgs[2].split(';').length}`);
writeLog(`[启动] Classpath总长度: ${jvmArgs[2].length} 字符`);
let proc;
for (let i = 0; i < 3; i++) {
try {
proc = spawn(java, args, { cwd: gameDir, detached: false, windowsHide: false });
writeLog(`启动游戏成功PID ${proc.pid}`);
break;
} catch (e) {
if (e.code === 'EBUSY' && i < 2) {
await new Promise(r => setTimeout(r, 1500));
continue;
}
writeLog(`启动游戏失败:${e.message}`);
return { success: false, error: e.message };
}
}
currentGameProcess = proc;
currentGamePid = proc.pid;
// 如果已经被取消,立即杀死
if (launchCancelFlag) {
writeLog('[启动] 进程已启动但取消标志已设置,立即终止');
killGameProcessTree(proc.pid);
try { proc.kill(); } catch {}
currentGameProcess = null;
currentGamePid = null;
return { success: false, error: '启动已取消', cancelled: true };
}
// 检测游戏窗口创建成功
await detectGameWindow(proc.pid);
// 检测结束后再次检查取消标志(可能在窗口检测期间被取消)
if (launchCancelFlag) {
writeLog('[启动] 窗口检测结束后检测到取消标志');
return { success: false, error: '启动已取消', cancelled: true };
}
// 捕获输出用于调试
let stdoutData = '';
let stderrData = '';
proc.stdout?.on('data', d => {
const text = d.toString();
stdoutData += text;
if (mainWindow) {
mainWindow.webContents.send('game-log', text);
}
if (text.includes('LWJGL Version') || text.includes('OpenGL version') || text.includes('Window')) {
writeLog(`[启动] 检测到窗口初始化日志`);
if (mainWindow) {
mainWindow.webContents.send('game-window-created', { pid: proc.pid });
}
}
});
proc.stderr?.on('data', d => {
const text = d.toString();
stderrData += text;
if (mainWindow) {
mainWindow.webContents.send('game-log', text);
}
if (text.includes('LWJGL Version') || text.includes('OpenGL version') || text.includes('Window')) {
writeLog(`[启动] 检测到窗口初始化日志`);
if (mainWindow) {
mainWindow.webContents.send('game-window-created', { pid: proc.pid });
}
}
});
proc.on('error', (err) => {
writeLog(`[启动] 进程错误: ${err.message}`);
});
proc.on('exit', (code) => {
writeLog(`[启动] 进程退出,代码: ${code}`);
if (code !== 0 && stderrData) {
writeLog(`[启动] 错误输出: ${stderrData.slice(0, 1000)}`);
}
if (currentGamePid === proc.pid) {
currentGameProcess = null;
currentGamePid = null;
}
if (mainWindow) {
mainWindow.webContents.send('game-closed', code);
}
});
return { success: true, pid: proc.pid };
} catch (e) {
writeLog(`启动游戏异常:${e.message}`);
return { success: false, error: e.message };
}
});
// 检测游戏窗口创建成功
async function detectGameWindow(pid) {
writeLog(`[启动] 开始检测游戏窗口PID: ${pid}`);
return new Promise((resolve) => {
let resolved = false;
const done = () => {
if (resolved) return;
resolved = true;
resolve();
};
const cancelCheck = setInterval(() => {
if (launchCancelFlag) {
writeLog(`[启动] 窗口检测期间检测到取消标志,立即终止游戏进程并退出`);
clearInterval(cancelCheck);
try { detector.kill(); } catch {}
killGameProcessTree(pid);
try {
if (currentGameProcess) { currentGameProcess.kill(); }
} catch {}
currentGameProcess = null;
currentGamePid = null;
done();
}
}, 100);
const timeout = setTimeout(() => {
writeLog(`[启动] 游戏窗口检测超时8秒继续启动流程`);
clearInterval(cancelCheck);
try { detector.kill(); } catch {}
if (mainWindow && !launchCancelFlag) {
mainWindow.webContents.send('game-window-created', { pid, timeout: true });
}
done();
}, 8000);
const script = `
$deadline = (Get-Date).AddSeconds(7)
while ((Get-Date) -lt $deadline) {
try {
$p = Get-Process -Id ${pid} -ErrorAction Stop
if ($p.MainWindowHandle -ne 0) { Write-Output "FOUND"; exit 0 }
} catch { Write-Output "GONE"; exit 1 }
Start-Sleep -Milliseconds 150
}
Write-Output "TIMEOUT"; exit 2
`;
const detector = spawn('powershell', ['-NoProfile', '-Command', script], { windowsHide: true });
let output = '';
detector.stdout.on('data', d => { output += d.toString(); });
detector.stderr.on('data', d => { output += d.toString(); });
detector.on('close', (code) => {
clearInterval(cancelCheck);
clearTimeout(timeout);
if (launchCancelFlag) { done(); return; }
if (output.includes('FOUND')) {
writeLog(`[启动] 检测到游戏窗口MainWindowHandle != 0`);
} else if (output.includes('GONE')) {
writeLog(`[启动] 进程已退出`);
} else {
writeLog(`[启动] 窗口检测结束code=${code}output=${output.trim()}`);
}
if (mainWindow) {
mainWindow.webContents.send('game-window-created', { pid, windowDetected: output.includes('FOUND') });
}
done();
});
detector.on('error', (err) => {
clearInterval(cancelCheck);
clearTimeout(timeout);
writeLog(`[启动] 窗口检测器启动失败: ${err.message}`);
if (mainWindow && !launchCancelFlag) {
mainWindow.webContents.send('game-window-created', { pid, error: true });
}
done();
});
});
}
// IPC: 下载并安装Java运行时
ipcMain.handle('install-java', async (_, javaVersion) => {
try {
writeLog(`[Java] 开始安装 Java ${javaVersion}`);
// 发送下载开始消息
if (mainWindow) {
mainWindow.webContents.send('java-download-started', { javaVersion });
}
const result = await downloadAndInstallJava(javaVersion);
if (result.success) {
writeLog(`[Java] Java ${javaVersion} 安装成功: ${result.javaPath}`);
if (mainWindow) {
mainWindow.webContents.send('java-download-completed', {
javaVersion,
javaPath: result.javaPath
});
}
return result;
} else {
writeLog(`[Java] Java ${javaVersion} 安装失败: ${result.error}`);
if (mainWindow) {
mainWindow.webContents.send('java-download-failed', {
javaVersion,
error: result.error
});
}
return result;
}
} catch (e) {
writeLog(`[Java] 安装Java异常: ${e.message}`);
if (mainWindow) {
mainWindow.webContents.send('java-download-failed', {
javaVersion,
error: e.message
});
}
return { success: false, error: e.message };
}
});
let currentSettings = null;
ipcMain.handle('get-settings', () => {
if (!currentSettings) {
currentSettings = loadSettings();
}
return currentSettings;
});
ipcMain.handle('save-settings', (_, settings) => {
currentSettings = mergeWithDefaults(settings);
const success = saveSettings(currentSettings);
return { success, settings: currentSettings };
});
ipcMain.handle('reset-settings', () => {
currentSettings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
const success = saveSettings(currentSettings);
return { success, settings: currentSettings };
});
ipcMain.handle('restart-app', () => {
writeLog('[重启] 正在重启应用...');
app.relaunch();
app.exit();
});
ipcMain.handle('set-developer-mode', (_, enabled) => {
isDeveloperMode = !!enabled;
writeLog('[开发者模式] ' + (isDeveloperMode ? '已启用' : '已禁用'));
return { success: true };
});
let activeAnimWindow = null;
function cleanupAnimTempDir(dir) {
try {
if (fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true, force: true });
}
} catch (e) {}
}
ipcMain.handle('play-startup-animation', async () => {
if (activeAnimWindow && !activeAnimWindow.isDestroyed()) {
try { activeAnimWindow.close(); } catch (e) {}
activeAnimWindow = null;
}
writeLog('[启动动画] 开始播放启动动画');
const iconDir = path.join(__dirname, 'static', 'icon');
const tempAnimDir = path.join(app.getPath('temp'), 'gpcl-anim-' + Date.now());
try { fs.mkdirSync(tempAnimDir, { recursive: true }); } catch (e) {}
var imageNames = ['caellab.png', 'gamets.png', 'gamets.ico', 'GPCL.png'];
for (var _i = 0; _i < imageNames.length; _i++) {
var src = path.join(iconDir, imageNames[_i]);
if (fs.existsSync(src)) {
try { fs.copyFileSync(src, path.join(tempAnimDir, imageNames[_i])); } catch (e) {}
}
}
var hasCaelab = fs.existsSync(path.join(tempAnimDir, 'caellab.png'));
var hasGamets = fs.existsSync(path.join(tempAnimDir, 'gamets.png')) || fs.existsSync(path.join(tempAnimDir, 'gamets.ico'));
var hasGpcl = fs.existsSync(path.join(tempAnimDir, 'GPCL.png'));
var gametsFile = fs.existsSync(path.join(tempAnimDir, 'gamets.png')) ? 'gamets.png' : 'gamets.ico';
writeLog('[启动动画] 资源: caellab=' + hasCaelab + ' gamets=' + hasGamets + ' gpcl=' + hasGpcl);
var ANIM_DURATION = 30;
var htmlContent = '<!DOCTYPE html>\n<html><head><meta charset="utf-8"><style>\n' +
'*{margin:0;padding:0;box-sizing:border-box}\n' +
'html,body{width:100%;height:100%;overflow:hidden;background:#0a0a0a}\n' +
'canvas{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1}\n' +
'.scene{position:fixed;top:0;left:0;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;z-index:10;opacity:0}\n' +
'#welcome-txt{text-align:center;font-family:"Microsoft YaHei","Segoe UI",sans-serif}\n' +
'#welcome-txt h1{font-size:56px;font-weight:700;color:#fff;letter-spacing:3px;text-shadow:0 0 25px rgba(91,135,49,0.6),0 0 50px rgba(91,135,49,0.2);margin-bottom:16px}\n' +
'#welcome-txt p{font-size:20px;color:rgba(180,180,180,0.7);letter-spacing:5px}\n' +
'.logo-row{display:flex;flex-direction:row;align-items:center;justify-content:center}\n' +
'#logo-caellab img{max-height:130px;max-width:300px;object-fit:contain;filter:drop-shadow(0 0 18px rgba(91,135,49,0.35))}\n' +
'#logo-both .logo-row{gap:50px}\n' +
'#logo-both img{max-height:110px;max-width:240px;object-fit:contain}\n' +
'#logo-both .caellab-img{filter:drop-shadow(0 0 18px rgba(91,135,49,0.35))}\n' +
'#logo-both .gamets-img{filter:drop-shadow(0 0 14px rgba(255,255,255,0.25))}\n' +
'#logo-gpcl img{max-height:170px;max-width:340px;object-fit:contain;filter:drop-shadow(0 0 25px rgba(91,135,49,0.4))}\n' +
'.logo-text{text-align:center;font-family:"Microsoft YaHei","Segoe UI",sans-serif;margin-top:18px}\n' +
'.logo-text-bottom{position:absolute;bottom:40px;left:0;width:100%;margin-top:0}\n' +
'.logo-title{font-size:20px;color:rgba(200,200,200,0.85);letter-spacing:4px;margin-bottom:6px}\n' +
'.footer-link{font-size:14px;color:rgba(91,135,49,0.7);text-decoration:underline;pointer-events:auto;cursor:pointer}\n' +
'.copyright{font-size:13px;color:rgba(160,160,160,0.6);letter-spacing:1px;margin-bottom:6px}\n' +
'.license-text{font-size:12px;color:rgba(140,140,140,0.5);margin:0}\n' +
'.license-text .footer-link{font-size:12px}\n' +
'.disclaimer{font-size:12px;color:rgba(120,120,120,0.45);max-width:600px;line-height:1.6;margin:40px auto 0;text-align:center}\n' +
'#sweep-line{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:25;opacity:0}\n' +
'#sweep-line .band{position:absolute;top:0;height:100%;width:100px;background:linear-gradient(90deg,transparent,rgba(91,135,49,0.1),rgba(127,255,0,0.12),rgba(91,135,49,0.1),transparent)}\n' +
'#fade{position:fixed;top:0;left:0;width:100%;height:100%;background:#0a0a0a;opacity:0;pointer-events:none;z-index:30;transition:opacity 1.5s ease-in}\n' +
'#fade.show{opacity:1}\n' +
'#flash{position:fixed;top:0;left:0;width:100%;height:100%;background:#fff;opacity:0;pointer-events:none;z-index:28}\n' +
'</style></head><body>\n' +
'<canvas id="c"></canvas>\n' +
'<div class="scene" id="welcome"><div id="welcome-txt"><h1>Minecraft\u6B22\u8FCE\u4F60\uFF01</h1><p>By GPCL And AllMinecraftPlayer</p></div></div>\n' +
'<div class="scene" id="logo-caellab"><div class="logo-row"><img src="caellab.png" alt="CaelLab"></div><div class="logo-text"><div class="logo-title">\u865A\u821F\u5B9E\u9A8C\u5BA4</div><a href="https://www.caellab.com" target="_blank" class="footer-link">caellab.com</a></div></div>\n' +
'<div class="scene" id="logo-both"><div class="logo-row"><img class="caellab-img" src="caellab.png" alt="CaelLab"><img class="gamets-img" src="' + gametsFile + '" alt="GameTS"></div><div class="logo-text logo-text-bottom"><div class="copyright">Copyright \u00A9 2026 Yunyun(\u4E91\u4E91) By \u865A\u821F\u5B9E\u9A8C\u5BA4(CaelLab) / CaelLabGameTS</div><p class="license-text">Licensed under <a href="https://www.caellab.com/license/bysa-code" target="_blank" class="footer-link">CaelLab BY-SA Code License</a>. Open Source on <a href="https://github.com/yunyun-3782/GoodPlanCraftLaunCher" target="_blank" class="footer-link">GitHub</a>.</p></div></div>\n' +
'<div class="scene" id="logo-gpcl"><img src="GPCL.png" alt="GPCL"><div class="logo-text logo-text-bottom"><div class="disclaimer">GoodPlanCraftLauncher \u4E0E Mojang Studios \u53CA Microsoft\u3001Xbox \u65E0\u5B98\u65B9\u5173\u8054\u3002Minecraft \u4E3A Mojang Studios \u5546\u6807\u3002</div></div></div>\n' +
'<div id="sweep-line"><div class="band" id="sweep-band"></div></div>\n' +
'<div id="flash"></div><div id="fade"></div>\n' +
'<script>\n' +
'(function(){\n' +
'var canvas=document.getElementById("c"),ctx=canvas.getContext("2d");\n' +
'var W,H,dpr=window.devicePixelRatio||1;\n' +
'function resize(){W=window.innerWidth;H=window.innerHeight;canvas.width=W*dpr;canvas.height=H*dpr;ctx.setTransform(dpr,0,0,dpr,0,0)}\n' +
'resize();window.addEventListener("resize",resize);\n' +
'var startTime=performance.now();\n' +
'var DUR=' + ANIM_DURATION + ';\n' +
'function rand(a,b){return a+Math.random()*(b-a)}\n' +
'function easeOut(t){return 1-Math.pow(1-t,3)}\n' +
'function easeInOut(t){return t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2}\n' +
'function clamp(v,a,b){return v<a?a:v>b?b:v}\n' +
'function lerp(a,b,t){return a+(b-a)*t}\n' +
'var BS=24;\n' +
'var MC_COLORS=["#8B6B3D","#6B6B6B","#5B8731","#3E6B1F","#6B4226","#5A5A5A","#7B5B2D","#4A4A4A"];\n' +
'var terrCols=Math.ceil(W/BS)+1;\n' +
'var terrH=[];\n' +
'for(var i=0;i<terrCols;i++){terrH.push(2+~~rand(0,4))}\n' +
'var parts=[];\n' +
'for(var i=0;i<160;i++){\n' +
' var a=rand(0,6.283),sp=rand(0.12,0.6);\n' +
' parts.push({x:rand(0,W),y:rand(0,H),vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,\n' +
' sz:rand(8,18),c:MC_COLORS[~~rand(0,MC_COLORS.length)],a:0,ta:rand(.08,.25),\n' +
' rot:rand(0,6.28),rs:rand(-.012,.012)});\n' +
'}\n' +
'var star={active:false,x:0,y:0,sx:0,sy:0,tx:0,ty:0,t:0,dur:1.6,trail:[]};\n' +
'var sparkles=[];\n' +
'function addSparkle(x,y){for(var i=0;i<6;i++){var a=rand(0,6.28),sp=rand(0.5,2);\n' +
' sparkles.push({x:x,y:y,vx:Math.cos(a)*sp,vy:Math.sin(a)*sp,a:1,sz:rand(3,6)})}}\n' +
'var shockwaves=[];\n' +
'function addShock(x,y,c,mr,d){shockwaves.push({x:x,y:y,r:0,mr:mr,c:c,a:.35,d:d,t:0})}\n' +
'var phaseShown={};\n' +
'function showScene(id,o,d){document.title="show:"+id+":"+o+":"+d}\n' +
'function hideScene(id,d){document.title="hide:"+id+":"+d}\n' +
'var waitingForDismiss=false;\n' +
'window.dismissAnimation=function(){\n' +
' var f=document.getElementById("fade");f.style.transition="opacity 1.2s ease-in";f.classList.add("show");\n' +
' setTimeout(function(){document.title="ended"},1300);\n' +
'};\n' +
'function drawBlock(x,y,sz,color,alpha){\n' +
' ctx.save();ctx.globalAlpha=alpha;\n' +
' ctx.fillStyle=color;ctx.fillRect(x,y,sz,sz);\n' +
' ctx.fillStyle="rgba(255,255,255,0.09)";ctx.fillRect(x,y,sz,1);ctx.fillRect(x,y,1,sz);\n' +
' ctx.fillStyle="rgba(0,0,0,0.18)";ctx.fillRect(x,y+sz-1,sz,1);ctx.fillRect(x+sz-1,y,1,sz);\n' +
' ctx.restore();\n' +
'}\n' +
'function drawTerrain(t){\n' +
' var a=clamp(t*.3,0,.2);\n' +
' for(var c=0;c<terrCols;c++){\n' +
' for(var r=0;r<terrH[c];r++){\n' +
' var y=H-(r+1)*BS;var x=c*BS;\n' +
' var col=r===0?"#3E6B1F":r===1?"#5B4B2D":"#3D3D3D";\n' +
' drawBlock(x,y,BS,col,a);\n' +
' }\n' +
' }\n' +
'}\n' +
'function drawGrid(t){\n' +
' var a=clamp(t*.3,0,.018);\n' +
' ctx.save();ctx.strokeStyle="rgba(255,255,255,"+a+")";ctx.lineWidth=0.5;\n' +
' for(var x=0;x<W;x+=BS){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke()}\n' +
' for(var y=0;y<H;y+=BS){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke()}\n' +
' ctx.restore();\n' +
'}\n' +
'function frame(now){\n' +
' var t=(now-startTime)/1000;\n' +
' ctx.clearRect(0,0,W,H);\n' +
' ctx.fillStyle="rgba(10,10,10,"+clamp(t*.5,0,1)+")";ctx.fillRect(0,0,W,H);\n' +
' drawGrid(t);\n' +
' drawTerrain(t);\n' +
' if(t>1&&t<8){\n' +
' var gp=clamp((t-1)*.6,0,.3);var r=100+gp*120;\n' +
' var g=ctx.createRadialGradient(W/2,H/2,0,W/2,H/2,r);\n' +
' g.addColorStop(0,"rgba(91,135,49,"+(.06*gp)+")");g.addColorStop(1,"rgba(62,107,31,0)");\n' +
' ctx.fillStyle=g;ctx.fillRect(0,0,W,H);\n' +
' }\n' +
' if(t>22&&t<28){\n' +
' var gp2=clamp((t-22)*.5,0,.25);var r2=120+gp2*130;\n' +
' var g2=ctx.createRadialGradient(W/2,H/2,0,W/2,H/2,r2);\n' +
' g2.addColorStop(0,"rgba(91,135,49,"+(.06*gp2)+")");g2.addColorStop(1,"rgba(62,107,31,0)");\n' +
' ctx.fillStyle=g2;ctx.fillRect(0,0,W,H);\n' +
' }\n' +
' for(var i=0;i<parts.length;i++){var p=parts[i];\n' +
' p.a=clamp(p.a+.003,0,p.ta);p.x+=p.vx;p.y+=p.vy;p.rot+=p.rs;\n' +
' p.vx*=.9995;p.vy*=.9995;\n' +
' if(p.x<-30)p.x=W+30;if(p.x>W+30)p.x=-30;\n' +
' if(p.y<-30)p.y=H+30;if(p.y>H+30)p.y=-30;\n' +
' ctx.save();ctx.translate(p.x,p.y);ctx.rotate(p.rot);\n' +
' drawBlock(-p.sz/2,-p.sz/2,p.sz,p.c,p.a);ctx.restore();\n' +
' }\n' +
' if(star.active){\n' +
' var st=clamp((t-star.t)/star.dur,0,1);var e=easeInOut(st);\n' +
' star.x=lerp(star.sx,star.tx,e);star.y=lerp(star.sy,star.ty,e);\n' +
' star.trail.push({x:star.x,y:star.y,a:1});if(star.trail.length>35)star.trail.shift();\n' +
' for(var j=0;j<star.trail.length;j++){var tr=star.trail[j];tr.a*=.93;\n' +
' ctx.save();ctx.globalAlpha=tr.a*.5;ctx.fillStyle="#7FFF00";\n' +
' ctx.fillRect(tr.x-2,tr.y-2,4,4);ctx.restore();}\n' +
' ctx.save();ctx.fillStyle="#7FFF00";ctx.shadowColor="#7FFF00";ctx.shadowBlur=15;\n' +
' ctx.fillRect(star.x-5,star.y-5,10,10);\n' +
' ctx.shadowBlur=30;ctx.fillStyle="rgba(127,255,0,0.12)";\n' +
' ctx.fillRect(star.x-14,star.y-14,28,28);ctx.restore();\n' +
' if(Math.random()>.6)addSparkle(star.x+rand(-8,8),star.y+rand(-8,8));\n' +
' if(st>=1)star.active=false;\n' +
' }\n' +
' for(var s=sparkles.length-1;s>=0;s--){var sp=sparkles[s];\n' +
' sp.x+=sp.vx;sp.y+=sp.vy;sp.a*=.95;sp.vx*=.97;sp.vy*=.97;\n' +
' if(sp.a<.01){sparkles.splice(s,1);continue}\n' +
' ctx.save();ctx.globalAlpha=sp.a;ctx.fillStyle="#7FFF00";\n' +
' ctx.fillRect(sp.x-sp.sz/2,sp.y-sp.sz/2,sp.sz,sp.sz);ctx.restore();\n' +
' }\n' +
' for(var s=shockwaves.length-1;s>=0;s--){var sw=shockwaves[s];\n' +
' sw.t+=1/60;var pct=sw.t/sw.d;if(pct>=1){shockwaves.splice(s,1);continue}\n' +
' sw.r=sw.mr*easeOut(pct);sw.a=.35*(1-pct);\n' +
' ctx.save();ctx.strokeStyle=sw.c;ctx.globalAlpha=sw.a;ctx.lineWidth=2+2*(1-pct);\n' +
' ctx.beginPath();ctx.arc(sw.x,sw.y,sw.r,0,6.28);ctx.stroke();ctx.restore();\n' +
' }\n' +
' if(t>1.5&&!phaseShown.w){phaseShown.w=true;showScene("welcome",1,1)}\n' +
' if(t>7&&!phaseShown.hidew){phaseShown.hidew=true;hideScene("welcome",1)}\n' +
' if(t>8.5&&!phaseShown.star){\n' +
' phaseShown.star=true;\n' +
' star.active=true;star.sx=-50;star.sy=H*.55;star.tx=W/2;star.ty=H/2;star.t=t;star.trail=[];\n' +
' }\n' +
' if(t>10.5&&!phaseShown.ca){phaseShown.ca=true;addShock(W/2,H/2,"rgba(91,135,49,0.3)",400,1.2);showScene("logo-caellab",1,1.2)}\n' +
' if(t>14.5&&!phaseShown.hideca){phaseShown.hideca=true;hideScene("logo-caellab",.8)}\n' +
' if(t>15.5&&!phaseShown.both){phaseShown.both=true;showScene("logo-both",1,1.2)}\n' +
' if(t>20.5&&!phaseShown.hideboth){phaseShown.hideboth=true;hideScene("logo-both",1)}\n' +
' if(t>22&&!phaseShown.gpcl){phaseShown.gpcl=true;addShock(W/2,H/2,"rgba(127,255,0,0.12)",350,1);showScene("logo-gpcl",1,1.5)}\n' +
' if(t>26&&!phaseShown.sweep){\n' +
' phaseShown.sweep=true;\n' +
' var sl=document.getElementById("sweep-line");sl.style.opacity="1";\n' +
' var band=document.getElementById("sweep-band");var swStart=performance.now();\n' +
' function sweepFrame(swNow){var swT=(swNow-swStart)/1500;\n' +
' if(swT>1){sl.style.opacity="0";return}\n' +
' band.style.left=(-200+(W+400)*easeInOut(swT))+"px";\n' +
' requestAnimationFrame(sweepFrame)}\n' +
' requestAnimationFrame(sweepFrame);\n' +
' }\n' +
' if(t>27.5&&!phaseShown.launch){phaseShown.launch=true;document.title="launch"}\n' +
' if(t>28&&!phaseShown.hgpcl){phaseShown.hgpcl=true;hideScene("logo-gpcl",1)}\n' +
' if(t>29.5&&!phaseShown.fadeblk){phaseShown.fadeblk=true;document.getElementById("fade").classList.add("show")}\n' +
' if(t>DUR&&!waitingForDismiss){waitingForDismiss=true;document.title="waiting"}\n' +
' requestAnimationFrame(frame);\n' +
'}\n' +
'document.title="meta:"+DUR;\n' +
'requestAnimationFrame(frame);\n' +
'})();\n' +
'<\/script></body></html>';
var tempHtmlPath = path.join(tempAnimDir, 'index.html');
try { fs.writeFileSync(tempHtmlPath, htmlContent, 'utf8'); } catch (e) {
writeLog('[启动动画] 写入临时文件失败: ' + e.message);
cleanupAnimTempDir(tempAnimDir);
return { success: false, error: '写入临时文件失败', duration: 0 };
}
return new Promise(function(resolve) {
var animWindow = new BrowserWindow({
fullscreen: true,
frame: false,
skipTaskbar: true,
backgroundColor: '#000000',
webPreferences: {
nodeIntegration: false,
contextIsolation: true
}
});
activeAnimWindow = animWindow;
animWindow.setAlwaysOnTop(true, 'screen-saver');
animWindow.webContents.setWindowOpenHandler(function(details) {
shell.openExternal(details.url);
return { action: 'deny' };
});
animWindow.loadFile(tempHtmlPath);
var resolved = false;
animWindow.on('page-title-updated', function(e, title) {
e.preventDefault();
if (title.startsWith('show:')) {
var parts = title.split(':');
var sceneId = parts[1];
var opacity = parts[2];
var dur = parts[3];
animWindow.webContents.executeJavaScript(
'(function(){var el=document.getElementById("' + sceneId + '");' +
'if(el){el.style.transition="opacity ' + dur + 's ease-out";' +
'el.style.opacity="' + opacity + '"}})()'
);
return;
}
if (title.startsWith('hide:')) {
var parts = title.split(':');
var sceneId = parts[1];
var dur = parts[2];
animWindow.webContents.executeJavaScript(
'(function(){var el=document.getElementById("' + sceneId + '");' +
'if(el){el.style.transition="opacity ' + dur + 's ease-in";' +
'el.style.opacity="0"}})()'
);
return;
}
if (title === 'launch' && !resolved) {
resolved = true;
writeLog('[启动动画] 动画即将结束提前启动MC');
resolve({ success: true, duration: ANIM_DURATION });
return;
}
if (title === 'waiting') {
writeLog('[启动动画] 进入黑屏等待阶段');
return;
}
if (title === 'ended') {
writeLog('[启动动画] 动画退场完毕');
try { animWindow.close(); } catch (e) {}
return;
}
});
animWindow.on('closed', function() {
if (activeAnimWindow === animWindow) activeAnimWindow = null;
cleanupAnimTempDir(tempAnimDir);
if (!resolved) {
resolved = true;
resolve({ success: false, error: '动画窗口已关闭', duration: 0 });
}
});
setTimeout(function() {
if (!resolved) {
resolved = true;
writeLog('[启动动画] 动画加载超时');
resolve({ success: false, error: '动画加载超时', duration: 0 });
try { if (!animWindow.isDestroyed()) animWindow.close(); } catch (e) {}
}
}, 35000);
});
});
ipcMain.handle('dismiss-startup-animation', function() {
if (activeAnimWindow && !activeAnimWindow.isDestroyed()) {
writeLog('[启动动画] MC窗口就绪执行退场');
try {
activeAnimWindow.webContents.executeJavaScript(
'if(typeof dismissAnimation==="function")dismissAnimation()'
);
} catch (e) {}
}
});
ipcMain.handle('read-json-file', async (_, subPath, fileName) => {
try {
const dir = path.join(BASE_DIR, 'gpcl', subPath);
const filePath = path.join(dir, fileName);
if (!fs.existsSync(filePath)) {
return null;
}
const content = fs.readFileSync(filePath, 'utf8');
return JSON.parse(content);
} catch (e) {
writeLog(`读取文件失败: ${e.message}`);
return null;
}
});
ipcMain.handle('write-json-file', async (_, subPath, fileName, data) => {
try {
const dir = path.join(BASE_DIR, 'gpcl', subPath);
const filePath = path.join(dir, fileName);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
return true;
} catch (e) {
writeLog(`写入文件失败: ${e.message}`);
return false;
}
});
// 获取 Forge 版本列表
ipcMain.handle('get-forge-versions', async (_, mcVersion) => {
try {
writeLog(`[Forge] 获取版本列表: ${mcVersion}`);
const versions = await fetchForgeVersions(mcVersion);
return { success: true, versions };
} catch (e) {
writeLog(`[Forge] 获取版本列表失败: ${e.message}`);
return { success: false, error: e.message, versions: [] };
}
});
// 从 Forge Maven 获取版本列表
async function fetchForgeVersions(mcVersion) {
return new Promise((resolve, reject) => {
const https = require('https');
const url = `https://files.minecraftforge.net/net/minecraftforge/forge/maven-metadata.json`;
https.get(url, { timeout: 10000 }, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const metadata = JSON.parse(data);
const versions = [];
// 查找对应 MC 版本的 Forge 版本
const versionArray = metadata[mcVersion];
if (versionArray && Array.isArray(versionArray)) {
versionArray.forEach((fullVersion) => {
const parts = fullVersion.split('-');
const forgeVersion = parts.slice(1).join('-');
versions.push({
id: fullVersion,
name: `Forge ${forgeVersion}`,
version: forgeVersion,
mcVersion: mcVersion
});
});
}
// 按版本号排序(最新的在前)
versions.sort((a, b) => compareVersions(b.version, a.version));
// 只返回前 10 个版本
const topVersions = versions.slice(0, 10);
// 标记推荐版本(第一个)
if (topVersions.length > 0) {
topVersions[0].name += ' (推荐)';
}
writeLog(`[Forge] 找到 ${topVersions.length} 个版本`);
resolve(topVersions);
} catch (e) {
writeLog(`[Forge] 解析错误: ${e.message}`);
reject(new Error('解析 Forge 元数据失败'));
}
});
}).on('error', (e) => {
reject(new Error(`请求 Forge 元数据失败: ${e.message}`));
}).on('timeout', () => {
reject(new Error('请求 Forge 元数据超时'));
});
});
}
// 版本号比较函数
function compareVersions(v1, v2) {
const parts1 = v1.split(/[.-]/).map(p => parseInt(p, 10) || 0);
const parts2 = v2.split(/[.-]/).map(p => parseInt(p, 10) || 0);
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
const a = parts1[i] || 0;
const b = parts2[i] || 0;
if (a > b) return 1;
if (a < b) return -1;
}
return 0;
}
async function installVersionWithModLoader(versionId, loaderType, loaderVersion, maxConcurrent) {
try {
writeLog(`[模组加载器] 开始下载: ${loaderType} ${loaderVersion} for MC ${versionId}`);
// 1. 先下载原版游戏
writeLog(`[模组加载器] 下载原版 MC ${versionId}`);
await installVersion(versionId, maxConcurrent);
if (shouldCancelDownload) {
return { success: false, error: '下载已取消' };
}
// 2. 根据模组加载器类型进行安装
let finalVersionId;
if (loaderType === 'forge') {
finalVersionId = await installForge(versionId, loaderVersion);
} else {
return { success: false, error: `不支持的模组加载器: ${loaderType}` };
}
writeLog(`[模组加载器] 安装完成: ${finalVersionId}`);
// 更新版本最后启动时间
updateVersionLastPlayed(finalVersionId);
return { success: true, finalVersionId };
} catch (e) {
writeLog(`[模组加载器] 安装失败: ${e.message}`);
return { success: false, error: e.message };
}
}
// 安装 Forge直接从 Forge Maven 下载完整的版本 JSON
async function installForge(mcVersion, forgeVersion) {
writeLog(`[Forge] 安装: MC ${mcVersion}, Forge ${forgeVersion}`);
const versionDir = path.join(GAME_DIR, 'versions', mcVersion);
if (!fs.existsSync(versionDir)) {
throw new Error(`原版版本 ${mcVersion} 不存在`);
}
// 构造完整的 Forge 版本 ID如 1.20.1-47.2.0
const fullForgeVersionId = `${mcVersion}-${forgeVersion}`;
// Forge 在 Maven 上提供了完整的版本 JSON含 mainClass、libraries、inheritsFrom 等)
const forgeJsonUrl = `https://maven.minecraftforge.net/net/minecraftforge/forge/${fullForgeVersionId}/forge-${fullForgeVersionId}.json`;
writeLog(`[Forge] 下载版本 JSON: ${forgeJsonUrl}`);
let forgeJsonData;
try {
forgeJsonData = await fetchText(forgeJsonUrl);
} catch (e) {
throw new Error(`无法下载 Forge 版本 JSON: ${e.message}`);
}
let forgeVersionData;
try {
forgeVersionData = JSON.parse(forgeJsonData);
} catch (e) {
const snippet = forgeJsonData.substring(0, 100);
writeLog(`[Forge] JSON 解析失败,响应内容: ${snippet}`);
throw new Error(`Forge 版本 JSON 解析失败,服务器可能返回了错误页面`);
}
const forgeJsonId = `${mcVersion}-forge`;
forgeVersionData.id = forgeJsonId;
const forgeJsonPath = path.join(versionDir, `${forgeJsonId}.json`);
fs.writeFileSync(forgeJsonPath, JSON.stringify(forgeVersionData, null, 2));
const gpclDir = path.join(versionDir, 'gpcl');
if (!fs.existsSync(gpclDir)) fs.mkdirSync(gpclDir, { recursive: true });
const configPath = path.join(gpclDir, 'config.ini');
const configContent = `[Version]
Minecraft=${mcVersion}
Forge=${forgeVersion}
`;
fs.writeFileSync(configPath, configContent, 'utf8');
writeLog(`[Forge] 安装完成: ${forgeJsonId}`);
return forgeJsonId;
}
// 读取 config.ini 配置文件
function readVersionConfig(versionId) {
const versionDir = path.join(GAME_DIR, 'versions', versionId);
const configPath = path.join(versionDir, 'gpcl', 'config.ini');
if (!fs.existsSync(configPath)) {
return null;
}
try {
const content = fs.readFileSync(configPath, 'utf8');
const config = {
Minecraft: '',
Forge: ''
};
const lines = content.split('\n');
let inVersionSection = false;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed === '[Version]') {
inVersionSection = true;
continue;
}
if (!inVersionSection) continue;
const parts = trimmed.split('=');
if (parts.length === 2) {
const key = parts[0].trim();
const value = parts[1].trim();
if (config.hasOwnProperty(key)) {
config[key] = value;
}
}
}
return config;
} catch (e) {
writeLog(`[Config] 读取配置失败: ${e.message}`);
return null;
}
}
// 生成版本显示名称
function generateVersionDisplayName(versionId) {
const config = readVersionConfig(versionId);
if (!config) {
return versionId;
}
const parts = [];
parts.push(config.Minecraft || versionId);
if (config.Forge) {
parts.push(`Forge${config.Forge}`);
}
return parts.join(' ');
}
// 生成启动页显示名称
function generateLaunchDisplayName(versionId) {
const config = readVersionConfig(versionId);
if (!config) {
return versionId;
}
const parts = [];
parts.push(config.Minecraft || versionId);
if (config.Forge) {
parts.push(`Forge${config.Forge}`);
}
return parts.join(' ');
}