diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22e5f1c --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +dist/ +build/ +out/ +.env +.env.local +.DS_Store +*.log +start.bat +push.bat +GoodPlanCraftLauncher/ \ No newline at end of file diff --git a/README.md b/README.md index 17eb0db..7e3f911 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,22 @@ -# GoodPlanCraftLaunCher (GPCL) +# GoodPlanCraftLauncher (GPCL) 简洁高效的 Minecraft 启动器。 -代码优先挂 - -https://git.caellab.com/yunyun/GoodPlanCraftLaunCher - -这个可能很久都没更新了 - - ## 制作方法 -使用[Electron](https://www.electronjs.org/zh/docs/latest/)进行制作。 +使用 [Node.js](https://nodejs.org) + [Electron](https://www.electronjs.org/zh/docs/latest/) 进行制作。 ## 安装 ### 从源码构建 ```bash -git clone https://github.com/yunyun-3782/GoodPlanCraftLaunCher.git +git clone https://git.caellab.com/yunyun/GoodPlanCraftLauncher.git ``` ### 预编译版本 -前往 [Releases](https://github.com/yunyun-3782/GoodPlanCraftLaunCher/releases/) 下载对可执行文件。 +前往 [Releases](https://git.caellab.com/yunyun/GoodPlanCraftLauncher/releases) 下载对可执行文件。 ## 使用 @@ -49,14 +42,17 @@ npm run build ## 贡献 -欢迎提交 不同意见,同时欢迎开发 衍生 版本。 +欢迎提交 [不同意见](https://f.caellab.com/t/problem) 或 [Issue](https://git.caellab.com/yunyun/GoodPlanCraftLauncher/issues) ,同时欢迎开发 衍生 版本。 ## 许可证 本项目采用 [CaelLab BY-SA Code License](https://www.caellab.com/license/bysa-code) 授权。 -具体可查看 [LICENSE](https://github.com/yunyun-3782/GoodPlanCraftLaunCher/blob/main/LICENSE) 。 +具体可查看 [LICENSE](https://git.caellab.com/yunyun/GoodPlanCraftLauncher/src/branch/main/LICENSE) 。 + +--- +很高兴能帮助到你!祝你在MC中玩的愉快! --- -**GoodPlanCraftLaunCher** — 让 Minecraft 启动更简单。 +**GoodPlanCraftLauncher** — 让 Minecraft 启动更简单。 diff --git a/icon.ico b/icon.ico new file mode 100644 index 0000000..dd1da75 Binary files /dev/null and b/icon.ico differ diff --git a/main.js b/main.js new file mode 100644 index 0000000..7670056 --- /dev/null +++ b/main.js @@ -0,0 +1,3408 @@ +/* + * CaelLab BY-SA Code License + * Copyright (c) 2026 Yunyun(云云) By 虚舟实验室(CaelLab) / CaelLabGameTS + * Source: https://git.caellab.com/yunyun/GoodPlanCraftLauncher + * 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; +ipcMain.handle('cancel-launch', () => { + launchCancelFlag = true; + writeLog('[启动] 用户取消启动'); + 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.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', () => { + try { + const packagePath = path.join(__dirname, 'package.json'); + if (fs.existsSync(packagePath)) { + const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + return pkg.version || '1.0.0'; + } + } catch (e) { + writeLog('[版本] 读取package.json失败: ' + e.message); + } + return '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}`); + } + } + // Natives(Windows 平台) + 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 }; + } + } + + // 检测游戏窗口创建成功 + await detectGameWindow(proc.pid); + + // 捕获输出用于调试 + let stdoutData = ''; + let stderrData = ''; + proc.stdout?.on('data', d => { + const text = d.toString(); + stdoutData += text; + mainWindow.webContents.send('game-log', text); + + // 检测Minecraft窗口创建成功的标志 + 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; + mainWindow.webContents.send('game-log', text); + + // 检测Minecraft窗口创建成功的标志 + 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)}`); + } + }); + + 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) => { +<<<<<<< Updated upstream + // 设置超时(最多等待5秒,比原来快3秒) + const timeout = setTimeout(() => { + writeLog(`[启动] 游戏窗口检测超时,继续启动流程`); + if (mainWindow) { + mainWindow.webContents.send('game-window-created', { pid: pid, timeout: true }); + } + resolve(); + }, 5000); + + // 使用更快速的检测方式 + let checkCount = 0; + const maxChecks = 50; // 最多检查50次(5秒) + let detected = false; + + // 首先立即发送一次,不等待检测完成 + writeLog(`[启动] 快速发送创建事件(立即检测)`); + setTimeout(() => { + if (!detected) { + detected = true; + if (mainWindow) { + mainWindow.webContents.send('game-window-created', { pid: pid, fast: true }); + } + } + }, 1500); // 1.5秒后直接认为启动成功 + + const checkInterval = setInterval(() => { + checkCount++; + + if (detected) { + clearInterval(checkInterval); + clearTimeout(timeout); + resolve(); + return; +======= + let resolved = false; + const done = () => { + if (resolved) return; + resolved = true; + resolve(); + }; + + const cancelCheck = setInterval(() => { + if (launchCancelFlag) { + writeLog(`[启动] 窗口检测期间检测到取消标志,立即退出`); + clearInterval(cancelCheck); + try { detector.kill(); } catch {} + 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 }); +>>>>>>> Stashed changes + } + 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 = '\n
\n' + +'\n' + +'By GPCL And AllMinecraftPlayer


Licensed under CaelLab BY-SA Code License. Open Source on GitCaelLab.

+