import path from 'path'; import { app, ipcMain } from 'electron'; import fs from 'fs'; import https from 'https'; import extract from 'extract-zip'; import { launch, Version, diagnose } from '@xmcl/core'; import { execSync } from 'child_process'; import { installFabric, getVersionList, installTask, installDependenciesTask, installNeoForged, } from '@xmcl/installer'; import { Agent } from 'undici'; import { spawn } from 'child_process'; import { AuthService } from './auth-service'; import { API_BASE_URL } from '../renderer/api'; app.setName('.popa-popa'); // const CDN = 'https://cdn.minecraft.popa-popa.ru'; // const DOWNLOAD_OPTIONS = { // // assets (objects/) // assetsHost: `${CDN}/assets/objects`, // // библиотеки (jar'ы) // libraryHost(library: { path: any }) { // return `${CDN}/libraries/${library.path}`; // }, // assetsIndexUrl: (version: any) => // `${CDN}/assets/indexes/${version.assetIndex.id}.json`, // // версии // json(versionInfo: { id: any }) { // return `${CDN}/versions/${versionInfo.id}/${versionInfo.id}.json`; // }, // client(resolved: { id: any }) { // return `${CDN}/versions/${resolved.id}/${resolved.id}.jar`; // }, // }; const INSTALL_PHASES = [ { id: 'download', weight: 0.25 }, // 25% — скачивание сборки { id: 'minecraft-install', weight: 0.3 }, // 30% — ваниль { id: 'fabric-install', weight: 0.15 }, // 15% — Fabric { id: 'dependencies', weight: 0.25 }, // 25% — библиотеки/ресурсы { id: 'launch', weight: 0.05 }, // 5% — запуск ] as const; type InstallPhaseId = (typeof INSTALL_PHASES)[number]['id']; function getGlobalProgress(phaseId: InstallPhaseId, localProgress01: number) { let offset = 0; let weight = 0; for (const phase of INSTALL_PHASES) { if (phase.id === phaseId) { weight = phase.weight; break; } offset += phase.weight; } if (!weight) return 100; const clampedLocal = Math.max(0, Math.min(1, localProgress01)); const global = (offset + clampedLocal * weight) * 100; return Math.round(Math.max(0, Math.min(global, 100))); } // Константы const AUTHLIB_INJECTOR_FILENAME = 'authlib-injector-1.2.5.jar'; const MCSTATUS_API_URL = 'https://api.mcstatus.io/v2/status/java/'; // Создаем экземпляр сервиса аутентификации const authService = new AuthService(); const agent = new Agent({ connections: 16, // максимум 16 одновременных соединений (скачиваний) // тут можно задать и другие параметры при необходимости }); let currentMinecraftProcess: any | null = null; // Модифицированная функция для получения последней версии релиза с произвольного URL export async function getLatestReleaseVersion(apiUrl: string): Promise { try { const response = await fetch(apiUrl); const data = await response.json(); return data.tag_name || '0.0.0'; } catch (error) { console.error('Failed to fetch latest version:', error); return '0.0.0'; } } // Функция для загрузки файла export async function downloadFile( url: string, dest: string, progressCallback: (progress: number) => void, ): Promise { return new Promise((resolve, reject) => { const file = fs.createWriteStream(dest); let downloadedSize = 0; let totalSize = 0; let redirectCount = 0; const makeRequest = (requestUrl: string) => { https .get(requestUrl, (response) => { // Обрабатываем редиректы if (response.statusCode === 301 || response.statusCode === 302) { if (redirectCount++ > 5) { reject(new Error('Too many redirects')); return; } makeRequest(response.headers.location!); return; } if (response.statusCode !== 200) { reject(new Error(`Server returned ${response.statusCode}`)); return; } totalSize = parseInt(response.headers['content-length'] || '0', 10); response.on('data', (chunk) => { downloadedSize += chunk.length; const progress = Math.round((downloadedSize / totalSize) * 100); progressCallback(progress); }); response.pipe(file); file.on('finish', () => { file.close(); // Проверяем, что файл скачан полностью if (downloadedSize !== totalSize && totalSize > 0) { fs.unlink(dest, () => { reject(new Error('File download incomplete')); }); return; } resolve(); }); }) .on('error', (err) => { fs.unlink(dest, () => reject(err)); }); }; makeRequest(url); }); } // Добавим функцию для определения версии Java export async function getJavaVersion( javaPath: string, ): Promise<{ version: string; majorVersion: number }> { return new Promise((resolve, reject) => { const process = spawn(javaPath, ['-version']); let output = ''; process.stderr.on('data', (data) => { output += data.toString(); }); process.on('close', (code) => { if (code === 0) { // Извлекаем версию из вывода (например, "java version "1.8.0_291"") const versionMatch = output.match(/version "([^"]+)"/); if (versionMatch) { const version = versionMatch[1]; // Определяем major версию let majorVersion = 8; // По умолчанию предполагаем Java 8 if (version.startsWith('1.8')) { majorVersion = 8; } else if (version.match(/^(9|10|11|12|13|14|15|16)/)) { majorVersion = parseInt(version.split('.')[0], 10); } else if (version.match(/^1\.(9|10|11|12|13|14|15|16)/)) { majorVersion = parseInt(version.split('.')[1], 10); } else if (version.match(/^([0-9]+)/)) { majorVersion = parseInt(version.match(/^([0-9]+)/)?.[1] || '0', 10); } resolve({ version, majorVersion }); } else { reject(new Error('Unable to parse Java version')); } } else { reject(new Error(`Java process exited with code ${code}`)); } }); }); } // Модифицируем функцию findJava, чтобы она находила все версии Java и определяла их версии export async function findJavaVersions(): Promise< Array<{ path: string; version: string; majorVersion: number }> > { const javaPaths: string[] = []; const results: Array<{ path: string; version: string; majorVersion: number; }> = []; try { // 1. Сначала проверяем переменную JAVA_HOME if (process.env.JAVA_HOME) { const javaPath = path.join( process.env.JAVA_HOME, 'bin', 'java' + (process.platform === 'win32' ? '.exe' : ''), ); if (fs.existsSync(javaPath)) { javaPaths.push(javaPath); } } // 2. Проверяем стандартные пути установки в зависимости от платформы const checkPaths: string[] = []; if (process.platform === 'win32') { // Windows const programFiles = process.env['ProgramFiles'] || 'C:\\Program Files'; const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)'; // JDK пути [ 'Java', 'AdoptOpenJDK', 'Eclipse Adoptium', 'BellSoft', 'Zulu', 'Amazon Corretto', 'Microsoft', ].forEach((vendor) => { checkPaths.push(path.join(programFiles, vendor)); checkPaths.push(path.join(programFilesX86, vendor)); }); } else if (process.platform === 'darwin') { // macOS checkPaths.push('/Library/Java/JavaVirtualMachines'); checkPaths.push('/System/Library/Java/JavaVirtualMachines'); checkPaths.push('/usr/libexec/java_home'); } else { // Linux checkPaths.push('/usr/lib/jvm'); checkPaths.push('/usr/java'); checkPaths.push('/opt/java'); } // Проверяем каждый путь for (const basePath of checkPaths) { if (fs.existsSync(basePath)) { try { // Находим подпапки с JDK/JRE const entries = fs.readdirSync(basePath, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { // Проверяем наличие исполняемого файла java в bin const javaPath = path.join( basePath, entry.name, 'bin', 'java' + (process.platform === 'win32' ? '.exe' : ''), ); if (fs.existsSync(javaPath)) { javaPaths.push(javaPath); } } } } catch (err) { console.error(`Ошибка при сканировании ${basePath}:`, err); } } } // 3. Пробуем найти java в PATH через команду which/where try { const command = process.platform === 'win32' ? 'where java' : 'which java'; const javaPathFromCmd = execSync(command).toString().trim().split('\n'); javaPathFromCmd.forEach((path) => { if (path && fs.existsSync(path) && !javaPaths.includes(path)) { javaPaths.push(path); } }); } catch (err) { console.error('Ошибка при поиске java через PATH:', err); } // Получаем информацию о версиях найденных Java for (const javaPath of javaPaths) { try { const versionInfo = await getJavaVersion(javaPath); results.push({ path: javaPath, version: versionInfo.version, majorVersion: versionInfo.majorVersion, }); } catch (err) { console.error(`Ошибка при определении версии для ${javaPath}:`, err); } } // Сортируем результаты по majorVersion (от меньшей к большей) return results.sort((a, b) => a.majorVersion - b.majorVersion); } catch (error) { console.error('Ошибка при поиске версий Java:', error); return results; } } // Обновим функцию findJava для использования Java 8 export async function findJava(): Promise { try { console.log('Поиск доступных версий Java...'); const javaVersions = await findJavaVersions(); if (javaVersions.length === 0) { throw new Error('Java не найдена. Установите Java и повторите попытку.'); } console.log('Найденные версии Java:'); javaVersions.forEach((java) => { console.log( `- Java ${java.majorVersion} (${java.version}) по пути: ${java.path}`, ); }); // Предпочитаем Java 21 или 17 для совместимости с authlib-injector const preferredVersions = [24, 21, 17, 11]; for (const preferredVersion of preferredVersions) { const preferred = javaVersions.find( (java) => java.majorVersion === preferredVersion, ); if (preferred) { console.log( `Выбрана предпочтительная версия Java ${preferredVersion}: ${preferred.path}`, ); return preferred.path; } } // Если не нашли предпочтительные версии, берем самую старую версию console.log(`Выбрана доступная версия Java: ${javaVersions[0].path}`); return javaVersions[0].path; } catch (error) { console.error('Ошибка при поиске Java:', error); throw error; } } // Добавим функцию для проверки/копирования authlib-injector (как в C#) async function ensureAuthlibInjectorExists(appPath: string): Promise { const authlibPath = path.join(appPath, AUTHLIB_INJECTOR_FILENAME); // Проверяем, существует ли файл if (fs.existsSync(authlibPath)) { console.log( `Файл ${AUTHLIB_INJECTOR_FILENAME} уже существует: ${authlibPath}`, ); return authlibPath; } // Ищем authlib в ресурсах приложения const resourcePath = path.join(app.getAppPath(), AUTHLIB_INJECTOR_FILENAME); if (fs.existsSync(resourcePath)) { console.log(`Копирование ${AUTHLIB_INJECTOR_FILENAME} из ресурсов...`); fs.copyFileSync(resourcePath, authlibPath); console.log(`Файл успешно скопирован: ${authlibPath}`); return authlibPath; } // Если не нашли локальный файл - скачиваем его console.log(`Скачивание ${AUTHLIB_INJECTOR_FILENAME}...`); await downloadFile( `https://github.com/yushijinhun/authlib-injector/releases/download/v1.2.5/${AUTHLIB_INJECTOR_FILENAME}`, authlibPath, (progress) => { console.log(`Прогресс скачивания: ${progress}%`); }, ); return authlibPath; } // Инициализация IPC обработчиков export function initMinecraftHandlers() { // Обработчик для скачивания и распаковки ipcMain.handle('download-and-extract', async (event, options) => { try { const { downloadUrl = 'https://github.com/DIKER0K/Comfort/releases/latest/download/Comfort.zip', apiReleaseUrl = 'https://api.github.com/repos/DIKER0K/Comfort/releases/latest', versionFileName = 'comfort_version.txt', packName = 'Comfort', preserveFiles = [], // Новый параметр: список файлов/папок для сохранения } = options || {}; const userDataPath = app.getPath('userData'); const minecraftDir = path.join(app.getPath('userData'), 'minecraft'); const versionsDir = path.join(minecraftDir, 'versions'); const versionFilePath = path.join(minecraftDir, versionFileName); // Получаем текущую и последнюю версии const latestVersion = await getLatestReleaseVersion(apiReleaseUrl); let currentVersion = ''; // Проверяем текущую версию, если файл существует if (fs.existsSync(versionFilePath)) { currentVersion = fs.readFileSync(versionFilePath, 'utf-8').trim(); } // Проверяем, нужно ли обновление if (currentVersion === latestVersion) { return { success: true, updated: false, version: currentVersion, packName, }; } const tempDir = path.join(userDataPath, 'temp'); const packDir = path.join(versionsDir, packName); // Директория пакета // Создаем/очищаем временную директорию if (fs.existsSync(tempDir)) { fs.rmSync(tempDir, { recursive: true }); } fs.mkdirSync(tempDir, { recursive: true }); const zipPath = path.join(tempDir, `${packName}.zip`); // Скачиваем файл await downloadFile(downloadUrl, zipPath, (progress) => { event.sender.send('download-progress', progress); const global = getGlobalProgress('download', progress / 100); event.sender.send('overall-progress', global); }); // Проверяем архив const fileStats = fs.statSync(zipPath); if (fileStats.size < 1024) { throw new Error('Downloaded file is too small, likely corrupted'); } // Создаем папку versions если её нет if (!fs.existsSync(versionsDir)) { fs.mkdirSync(versionsDir, { recursive: true }); } // Сохраняем файлы/папки, которые нужно оставить const backupDir = path.join(tempDir, 'backup'); fs.mkdirSync(backupDir, { recursive: true }); // Проверка и бэкап указанных файлов/папок for (const filePath of preserveFiles) { const fullPath = path.join(packDir, filePath); if (fs.existsSync(fullPath)) { const backupPath = path.join(backupDir, filePath); // Создаем необходимые директории для бэкапа const backupDirPath = path.dirname(backupPath); if (!fs.existsSync(backupDirPath)) { fs.mkdirSync(backupDirPath, { recursive: true }); } // Копируем файл или директорию if (fs.lstatSync(fullPath).isDirectory()) { fs.cpSync(fullPath, backupPath, { recursive: true }); console.log(`Директория ${filePath} сохранена во временный бэкап`); } else { fs.copyFileSync(fullPath, backupPath); console.log(`Файл ${filePath} сохранен во временный бэкап`); } } } // Распаковываем архив напрямую в папку versions await extract(zipPath, { dir: versionsDir }); fs.unlinkSync(zipPath); // Восстанавливаем файлы/папки из бэкапа for (const filePath of preserveFiles) { const backupPath = path.join(backupDir, filePath); if (fs.existsSync(backupPath)) { const targetPath = path.join(packDir, filePath); // Создаем необходимые директории для восстановления const targetDirPath = path.dirname(targetPath); if (!fs.existsSync(targetDirPath)) { fs.mkdirSync(targetDirPath, { recursive: true }); } // Копируем обратно файл или директорию if (fs.lstatSync(backupPath).isDirectory()) { fs.cpSync(backupPath, targetPath, { recursive: true }); console.log(`Директория ${filePath} восстановлена из бэкапа`); } else { fs.copyFileSync(backupPath, targetPath); console.log(`Файл ${filePath} восстановлен из бэкапа`); } } } // Сохраняем новую версию fs.writeFileSync(versionFilePath, latestVersion); // Удаляем временную директорию fs.rmSync(tempDir, { recursive: true }); return { success: true, updated: true, version: latestVersion, packName }; } catch (error) { console.error('Error in download-and-extract:', error); throw error; } }); // Обработчик для запуска Minecraft ipcMain.handle('launch-minecraft', async (event, gameConfig) => { try { const { accessToken, uuid, username, memory = 4096, baseVersion = '1.21.4', fabricVersion = null, neoForgeVersion = null, packName = 'Comfort', // имя сборки (папка с модами) versionToLaunchOverride = '', // переопределение версии для запуска (например, 1.21.10 для ванили) serverIp = 'popa-popa.ru', serverPort, isVanillaVersion = false, loaderType = 'fabric', } = gameConfig || {}; const userDataPath = app.getPath('userData'); const minecraftDir = path.join(app.getPath('userData'), 'minecraft'); const versionsDir = path.join(minecraftDir, 'versions'); fs.mkdirSync(versionsDir, { recursive: true }); // gamePath: // - ваниль → .popa-popa // - модпак → .popa-popa/versions/Comfort (или другое packName) const packDir = isVanillaVersion ? minecraftDir : path.join(versionsDir, packName); if (!fs.existsSync(packDir)) { fs.mkdirSync(packDir, { recursive: true }); } const versionsContents = fs.existsSync(versionsDir) ? fs.readdirSync(versionsDir) : []; console.log('Доступные версии:', versionsContents); // --- Определяем базовую / фактическую версию --- let effectiveBaseVersion = baseVersion; // Для ванили считаем базовой именно ту, которую хотим запустить if (isVanillaVersion && versionToLaunchOverride) { effectiveBaseVersion = versionToLaunchOverride; } let versionToLaunch: string | undefined = versionToLaunchOverride || undefined; if (!versionToLaunch) { if (isVanillaVersion) { // Ваниль — запускаем baseVersion (или override) versionToLaunch = effectiveBaseVersion; } else { // Определяем ID версии в зависимости от типа загрузчика if (loaderType === 'neoforge' && neoForgeVersion) { // NeoForge создает версию с ID "neoforge-{version}" const neoForgeId = `neoforge-${neoForgeVersion}`; // Проверяем, существует ли такая версия if (versionsContents.includes(neoForgeId)) { versionToLaunch = neoForgeId; } else { // Если не существует, пробуем комбинированный ID для совместимости const combinedId = `${effectiveBaseVersion}-neoforge${neoForgeVersion}`; versionToLaunch = combinedId; // Логируем для отладки console.log( 'NeoForge версия не найдена, используем комбинированный ID:', combinedId, ); } } else if (fabricVersion) { // Fabric создает версию с ID "{minecraftVersion}-fabric{fabricVersion}" const fabricId = `${effectiveBaseVersion}-fabric${fabricVersion}`; if (versionsContents.includes(fabricId)) { versionToLaunch = fabricId; } else { versionToLaunch = fabricId; } } else { versionToLaunch = effectiveBaseVersion; } } } console.log('Конфигурация:', { loaderType, neoForgeVersion, fabricVersion, effectiveBaseVersion, versionToLaunch, versionsContents, }); // --- Поиск Java --- event.sender.send('installation-status', { step: 'java', message: 'Поиск Java...', }); console.log('Поиск Java...'); let javaPath = 'java'; try { javaPath = await findJava(); } catch (error) { console.warn('Ошибка при поиске Java:', error); event.sender.send('installation-status', { step: 'java-error', message: 'Не удалось найти Java. Попробуем запустить с системной Java.', }); } console.log('Используем Java:', javaPath); // --- 1. Установка ванильного Minecraft (effectiveBaseVersion) --- let resolvedVersion: any; try { event.sender.send('installation-status', { step: 'minecraft-list', message: 'Получение списка версий Minecraft...', }); console.log('Получение списка версий Minecraft...'); const versionList = await getVersionList(); const minecraftVersion = versionList.versions.find( (v) => v.id === effectiveBaseVersion, ); console.log('minecraftVersion:', minecraftVersion); if (minecraftVersion) { const installMcTask = installTask(minecraftVersion, minecraftDir, { skipRevalidate: true, assetsDownloadConcurrency: 2, librariesDownloadConcurrency: 2, dispatcher: agent, }); console.log('installMcTask started for', minecraftVersion.id); event.sender.send('installation-status', { step: 'minecraft-install', message: `Установка Minecraft ${minecraftVersion.id}...`, }); await installMcTask.startAndWait({ onUpdate(task, chunkSize) { // локальный прогресс инсталлятора XMCL const local = installMcTask.total > 0 ? installMcTask.progress / installMcTask.total : 0; const global = getGlobalProgress('minecraft-install', local); event.sender.send('overall-progress', global); }, onFailed(task, error) { const stepName = (task as any).path || task.name || 'unknown'; console.warn( `[minecraft-install] step "${stepName}" failed: ${ (error as any).code ?? '' } ${(error as any).message}`, ); event.sender.send('installation-status', { step: `minecraft-install.${stepName}`, message: `Ошибка: ${(error as any).message}`, }); }, }); } else { console.warn( `Версия ${effectiveBaseVersion} не найдена в списке версий Minecraft. Предполагаем, что она уже установлена.`, ); } } catch (error: any) { const agg = error as any; const innerCount = Array.isArray(agg?.errors) ? agg.errors.length : 0; console.log( 'Ошибка при установке ванильного Minecraft, продолжаем:', agg?.message || String(agg), innerCount ? `(внутренних ошибок: ${innerCount})` : '', ); } // --- 2. Установка Fabric (только для модпаков) --- if (!isVanillaVersion) { if (loaderType === 'neoforge' && neoForgeVersion) { try { event.sender.send('installation-status', { step: 'neoforge-install', message: `Установка NeoForge ${neoForgeVersion}...`, }); event.sender.send( 'overall-progress', getGlobalProgress('fabric-install', 0), ); console.log('installNeoForged:', { project: 'neoforge', version: neoForgeVersion, minecraftVersion: effectiveBaseVersion, minecraftDir, }); // Установка NeoForge await installNeoForged('neoforge', neoForgeVersion, minecraftDir, { minecraft: effectiveBaseVersion, java: javaPath, side: 'client', }); console.log('NeoForge установлен успешно!'); event.sender.send( 'overall-progress', getGlobalProgress('fabric-install', 1), ); } catch (error) { console.error('Ошибка при установке NeoForge:', error); event.sender.send('installation-status', { step: 'neoforge-error', message: `Ошибка установки NeoForge: ${error.message}`, }); } } else if (fabricVersion) { // Существующий код для Fabric try { event.sender.send('installation-status', { step: 'fabric-install', message: `Установка Fabric ${fabricVersion}...`, }); event.sender.send( 'overall-progress', getGlobalProgress('fabric-install', 0), ); console.log('installFabric:', { minecraftVersion: effectiveBaseVersion, fabricVersion, minecraftDir, }); await installFabric({ minecraftVersion: effectiveBaseVersion, version: fabricVersion, minecraft: minecraftDir, }); event.sender.send( 'overall-progress', getGlobalProgress('fabric-install', 1), ); } catch (error) { console.log('Ошибка при установке Fabric, продолжаем:', error); } } } // --- 3. Установка зависимостей для versionToLaunch --- try { if (!versionToLaunch) { throw new Error('versionToLaunch не определён'); } console.log('version-parse:', { minecraftDir, versionToLaunch, }); event.sender.send('installation-status', { step: 'version-parse', message: 'Подготовка версии...', }); resolvedVersion = await Version.parse(minecraftDir, versionToLaunch); event.sender.send('installation-status', { step: 'dependencies', message: 'Установка библиотек и ресурсов...', }); const depsTask = installDependenciesTask(resolvedVersion, { skipRevalidate: true, prevalidSizeOnly: true, dispatcher: agent, assetsDownloadConcurrency: 2, librariesDownloadConcurrency: 2, checksumValidatorResolver: () => ({ async validate() { // Игнорируем sha1, чтобы не падать из-за несоответствий }, }), // ...DOWNLOAD_OPTIONS, }); await depsTask.startAndWait({ onUpdate(task, chunkSize) { const local = depsTask.total > 0 ? depsTask.progress / depsTask.total : 0; const global = getGlobalProgress('dependencies', local); event.sender.send('overall-progress', global); }, onFailed(task, error) { const stepName = (task as any).path || task.name || 'unknown'; console.warn( `[deps] step "${stepName}" failed: ${ (error as any).code ?? '' } ${(error as any).message}`, ); event.sender.send('installation-status', { step: `dependencies.${stepName}`, message: `Ошибка: ${(error as any).message}`, }); }, }); } catch (error: any) { console.log( 'Ошибка при подготовке версии/зависимостей, продолжаем запуск:', error.message || error, ); } // --- authlib-injector --- const authlibPath = await ensureAuthlibInjectorExists(userDataPath); console.log('authlibPath:', authlibPath); event.sender.send('installation-status', { step: 'authlib-injector', message: 'authlib-injector готов', }); // --- Запуск игры --- console.log('Запуск игры...'); event.sender.send('installation-status', { step: 'launch', message: 'Запуск игры...', }); event.sender.send('overall-progress', getGlobalProgress('launch', 0)); const proc = await launch({ gamePath: packDir, resourcePath: minecraftDir, javaPath, version: versionToLaunch!, launcherName: 'popa-popa', extraJVMArgs: [ '-Dlog4j2.formatMsgNoLookups=true', `-javaagent:${authlibPath}=${API_BASE_URL}`, `-Xmx${memory}M`, '-Dauthlibinjector.skinWhitelist=https://minecraft.api.popa-popa.ru/', '-Dauthlibinjector.debug=verbose,authlib', '-Dauthlibinjector.legacySkinPolyfill=enabled', '-Dauthlibinjector.mojangAntiFeatures=disabled', '-Dcom.mojang.authlib.disableSecureProfileEndpoints=true', ], extraMCArgs: [ '--quickPlayMultiplayer', `${serverIp}:${serverPort || 25565}`, ], accessToken, gameProfile: { id: uuid, name: username, }, }); event.sender.send('minecraft-started', { pid: proc.pid }); currentMinecraftProcess = proc; event.sender.send('overall-progress', getGlobalProgress('launch', 1)); let stderrBuffer = ''; proc.stdout?.on('data', (data) => { console.log(`Minecraft stdout: ${data}`); }); proc.stderr?.on('data', (data) => { const text = data.toString(); console.error(`Minecraft stderr: ${text}`); stderrBuffer += text; // Пробрасываем сырой лог клиенту (если захочешь где-то выводить) event.sender.send('minecraft-log', text); // Если это ошибка — сразу уведомим пользователя if (text.toLowerCase().includes('error')) { event.sender.send('minecraft-error', { message: text, }); } }); proc.on('exit', (code) => { console.log('Minecraft exited with code', code); currentMinecraftProcess = null; event.sender.send('minecraft-stopped', { code }); if (code !== 0) { event.sender.send('installation-status', { step: 'error', message: `Minecraft завершился с ошибкой (код ${code})`, }); event.sender.send('minecraft-error', { message: `Minecraft завершился с ошибкой (код ${code})`, stderr: stderrBuffer, code, }); } }); console.log('Запуск игры...'); return { success: true, pid: proc.pid }; } catch (error: any) { console.error('Ошибка при запуске Minecraft:', error); event.sender.send('installation-status', { step: 'error', message: `Ошибка запуска: ${error.message || String(error)}`, }); return { success: false, error: error.message || String(error) }; } }); ipcMain.handle('stop-minecraft', async (event) => { try { if (currentMinecraftProcess && !currentMinecraftProcess.killed) { console.log('Останавливаем Minecraft по запросу пользователя...'); // На Windows этого обычно достаточно currentMinecraftProcess.kill(); // Можно чуть подождать, но не обязательно return { success: true }; } return { success: false, error: 'Minecraft сейчас не запущен' }; } catch (error: any) { console.error('Ошибка при остановке Minecraft:', error); return { success: false, error: error.message || String(error) }; } }); // Добавьте в функцию initMinecraftHandlers или создайте новую ipcMain.handle('get-pack-files', async (event, packName) => { try { const userDataPath = app.getPath('userData'); const minecraftDir = path.join(app.getPath('userData'), 'minecraft'); const packDir = path.join(minecraftDir, 'versions', packName); if (!fs.existsSync(packDir)) { return { success: false, error: 'Директория сборки не найдена' }; } // Функция для рекурсивного обхода директории const scanDir: any = (dir: any, basePath: any = '') => { const result = []; const items = fs.readdirSync(dir); for (const item of items) { const itemPath = path.join(dir, item); const relativePath = basePath ? path.join(basePath, item) : item; const isDirectory = fs.statSync(itemPath).isDirectory(); result.push({ name: item, path: relativePath, isDirectory, // Если это директория, рекурсивно сканируем ее children: isDirectory ? scanDir(itemPath, relativePath) : [], }); } return result; }; const files = scanDir(packDir); return { success: true, files }; } catch (error) { console.error('Ошибка при получении файлов сборки:', error); return { success: false, error: error.message }; } }); // ПРОБЛЕМА: У вас два обработчика для одного и того же канала 'get-installed-versions' // РЕШЕНИЕ: Объединим логику в один обработчик, а из второго обработчика вызовем функцию getInstalledVersions // Сначала создаем общую функцию для получения установленных версий function getInstalledVersions() { try { const userDataPath = app.getPath('userData'); const minecraftDir = path.join(app.getPath('userData'), 'minecraft'); const versionsDir = path.join(minecraftDir, 'versions'); if (!fs.existsSync(versionsDir)) { return { success: true, versions: [] }; } const items = fs.readdirSync(versionsDir); const versions = []; for (const item of items) { const versionPath = path.join(versionsDir, item); if (!fs.statSync(versionPath).isDirectory()) continue; // ❗ Прячем технические версии загрузчиков if (item.includes('-fabric') || item.includes('neoforge')) { continue; } const files = fs.readdirSync(versionPath); const jsonFile = files.find((f) => f.endsWith('.json')); if (!jsonFile) continue; const versionJsonPath = path.join(versionPath, jsonFile); let versionInfo = { id: item, name: item, version: item, }; if (fs.existsSync(versionJsonPath)) { try { const versionData = JSON.parse( fs.readFileSync(versionJsonPath, 'utf8'), ); versionInfo.version = versionData.id || item; } catch (error) { console.warn(`Ошибка при чтении файла версии ${item}:`, error); } } versions.push(versionInfo); } return { success: true, versions }; } catch (error) { console.error('Ошибка при получении установленных версий:', error); return { success: false, error: error.message, versions: [] }; } } // Регистрируем обработчик для get-installed-versions ipcMain.handle('get-installed-versions', async () => { return getInstalledVersions(); }); // Обработчик get-available-versions использует функцию getInstalledVersions ipcMain.handle('get-available-versions', async (event, { gistUrl }) => { try { // Используем URL из параметров или значение по умолчанию const url = gistUrl || 'https://gist.githubusercontent.com/DIKER0K/06cd12fb3a4d08b1f0f8c763a7d05e06/raw/versions.json'; const response = await fetch(url); if (!response.ok) { throw new Error( `Failed to fetch versions from Gist: ${response.status} ${response.statusText}`, ); } const versions = await response.json(); // Получаем уже установленные версии const installedResult = getInstalledVersions(); const installedVersions = installedResult.success ? installedResult.versions : []; // Добавляем флаг installed к каждой версии const versionsWithInstallStatus = versions.map((version: any) => { const isInstalled = installedVersions.some( (installed: any) => installed.id === version.id, ); return { ...version, installed: isInstalled, }; }); return { success: true, versions: versionsWithInstallStatus, }; } catch (error) { console.error('Ошибка при получении доступных версий:', error); return { success: false, error: error.message, versions: [] }; } }); } // Добавляем обработчики IPC для аутентификации export function initAuthHandlers() { // Аутентификация ipcMain.handle('authenticate', async (event, { username, password }) => { try { const auth = await authService.login(username, password); return { accessToken: auth.accessToken, clientToken: auth.clientToken, selectedProfile: auth.selectedProfile, }; } catch (error) { console.error('Ошибка аутентификации:', error); throw error; } }); // Валидация токена ipcMain.handle( 'validate-token', async (event, { accessToken, clientToken }) => { try { const valid = await authService.validate(accessToken, clientToken); return { valid }; } catch (error) { console.error('Ошибка валидации токена:', error); return { valid: false }; } }, ); // Обновление токена ipcMain.handle( 'refresh-token', async (event, { accessToken, clientToken }) => { try { const auth = await authService.refresh(accessToken, clientToken); if (!auth) return null; return { accessToken: auth.accessToken, clientToken: auth.clientToken, selectedProfile: auth.selectedProfile, }; } catch (error) { console.error('Ошибка обновления токена:', error); return null; } }, ); } // Функция для получения статуса сервера export function initServerStatusHandler() { ipcMain.handle('get-server-status', async (event, { host, port }) => { try { // Формируем адрес с портом, если указан const serverAddress = port ? `${host}:${port}` : host; // Делаем запрос к API mcstatus.io const response = await fetch(`${MCSTATUS_API_URL}${serverAddress}`); // Проверяем статус ответа if (!response.ok) { const errorText = await response.text(); throw new Error(`API вернул ошибку ${response.status}: ${errorText}`); } const data = await response.json(); if (data.online) { return { success: true, online: data.players?.online || 0, max: data.players?.max || 0, version: data.version?.name_clean || 'Unknown', icon: data.icon || null, // Возвращаем иконку motd: data.motd?.clean || '', // Название сервера }; } else { return { success: false, error: 'Сервер не доступен' }; } } catch (error) { console.error('Ошибка при получении статуса сервера:', error); return { success: false, error: error.message }; } }); } // Функция для работы с конфигурацией сборки export function initPackConfigHandlers() { // Файл конфигурации const CONFIG_FILENAME = 'popa-launcher-config.json'; // Обработчик для сохранения настроек сборки ipcMain.handle('save-pack-config', async (event, { packName, config }) => { try { const userDataPath = app.getPath('userData'); const minecraftDir = path.join(app.getPath('userData'), 'minecraft'); const packDir = path.join(minecraftDir, 'versions', packName); // Создаем папку для сборки, если она не существует if (!fs.existsSync(packDir)) { fs.mkdirSync(packDir, { recursive: true }); } const configPath = path.join(packDir, CONFIG_FILENAME); // Сохраняем конфигурацию в файл fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); // Добавляем файл конфигурации в список файлов, которые не удаляются if (!config.preserveFiles.includes(CONFIG_FILENAME)) { config.preserveFiles.push(CONFIG_FILENAME); // Перезаписываем файл с обновленным списком fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); } return { success: true }; } catch (error) { console.error('Ошибка при сохранении настроек сборки:', error); return { success: false, error: error.message }; } }); // Обработчик для загрузки настроек сборки ipcMain.handle('load-pack-config', async (event, { packName }) => { try { const userDataPath = app.getPath('userData'); const minecraftDir = path.join(app.getPath('userData'), 'minecraft'); const packDir = path.join(minecraftDir, 'versions', packName); const configPath = path.join(packDir, CONFIG_FILENAME); // Проверяем существование файла конфигурации if (!fs.existsSync(configPath)) { // Если файла нет, возвращаем дефолтную конфигурацию return { success: true, config: { memory: 4096, preserveFiles: [CONFIG_FILENAME], // По умолчанию сохраняем файл конфигурации }, }; } // Читаем и парсим конфигурацию const configData = fs.readFileSync(configPath, 'utf8'); const config = JSON.parse(configData); // Добавляем файл конфигурации в список сохраняемых файлов, если его там нет if (!config.preserveFiles.includes(CONFIG_FILENAME)) { config.preserveFiles.push(CONFIG_FILENAME); } return { success: true, config }; } catch (error) { console.error('Ошибка при загрузке настроек сборки:', error); return { success: false, error: error.message, // Возвращаем дефолтную конфигурацию при ошибке config: { memory: 4096, preserveFiles: [CONFIG_FILENAME], }, }; } }); } // Добавляем после обработчика get-available-versions ipcMain.handle('get-version-config', async (event, { versionId }) => { try { const userDataPath = app.getPath('userData'); const minecraftDir = path.join(app.getPath('userData'), 'minecraft'); const versionsDir = path.join(minecraftDir, 'versions'); const versionPath = path.join(versionsDir, versionId); // Проверяем существование директории версии if (!fs.existsSync(versionPath)) { return { success: false, error: `Версия ${versionId} не найдена` }; } // Проверяем конфигурационный файл версии const configPath = path.join(versionPath, 'popa-launcher-config.json'); // Определяем базовые настройки по умолчанию let config = { downloadUrl: '', apiReleaseUrl: '', versionFileName: `${versionId}_version.txt`, packName: versionId, memory: 4096, baseVersion: '1.21.10', serverIp: 'popa-popa.ru', fabricVersion: null, neoForgeVersion: null, loaderType: 'fabric', // 'fabric', 'neoforge', 'vanilla' preserveFiles: ['popa-launcher-config.json'], }; // Если это Comfort, используем настройки по умолчанию if (versionId === 'Comfort') { config.downloadUrl = 'https://github.com/DIKER0K/Comfort/releases/latest/download/Comfort.zip'; config.apiReleaseUrl = 'https://api.github.com/repos/DIKER0K/Comfort/releases/latest'; config.baseVersion = '1.21.10'; config.fabricVersion = '0.18.1'; config.loaderType = 'fabric'; } // Если это NeoForge сборка, добавьте соответствующие настройки if (versionId === 'MyNeoForgePack') { config.downloadUrl = 'https://github.com/YOUR_USERNAME/YOUR_NEOFORGE_PACK/releases/latest/download/MyNeoForgePack.zip'; config.apiReleaseUrl = 'https://api.github.com/repos/YOUR_USERNAME/YOUR_NEOFORGE_PACK/releases/latest'; config.baseVersion = '1.21.1'; config.fabricVersion = null; config.neoForgeVersion = '20.6.2'; config.loaderType = 'neoforge'; config.memory = 6144; // NeoForge может требовать больше памяти } // Если есть конфигурационный файл, загружаем из него if (fs.existsSync(configPath)) { try { const savedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); config = { ...config, ...savedConfig }; } catch (error) { console.warn(`Ошибка чтения конфигурации ${versionId}:`, error); } } return { success: true, config }; } catch (error) { console.error('Ошибка получения настроек версии:', error); return { success: false, error: error.message }; } });