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 { installDependencies, installFabric, getFabricLoaders, getVersionList, install, installTask, installDependenciesTask, } from '@xmcl/installer'; import { spawn } from 'child_process'; import { AuthService } from './auth-service'; // Константы 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(); // Модифицированная функция для получения последней версии релиза с произвольного 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 = [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 appPath = path.dirname(app.getPath('exe')); const minecraftDir = path.join(appPath, '.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(appPath, '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 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 = 'fabric0.16.14', packName = 'Comfort', // Название основной сборки versionToLaunchOverride = '', // Возможность переопределить версию для запуска serverIp = 'popa-popa.ru', serverPort, // Добавляем опциональный порт без значения по умолчанию } = gameConfig || {}; const appPath = path.dirname(app.getPath('exe')); const minecraftDir = path.join(appPath, '.minecraft'); const versionsDir = path.join(minecraftDir, 'versions'); // Определяем версию для запуска const versionsContents = fs.existsSync(versionsDir) ? fs.readdirSync(versionsDir) : []; console.log('Доступные версии:', versionsContents); // Найти версию пакета, Fabric или базовую версию let versionToLaunch = versionToLaunchOverride; if (!versionToLaunch) { if ( versionsContents.includes(`${baseVersion}-fabric${fabricVersion}`) ) { versionToLaunch = `${baseVersion}-fabric${fabricVersion}`; } else if (versionsContents.includes(packName)) { versionToLaunch = packName; } else { versionToLaunch = baseVersion; } } console.log('Запускаем версию:', versionToLaunch); // Находим путь к Java event.sender.send('installation-status', { step: 'java', message: 'Поиск Java...', }); let javaPath; try { javaPath = await findJava(); } catch (error) { console.warn('Ошибка при поиске Java:', error); event.sender.send('installation-status', { step: 'java-error', message: 'Не удалось найти Java. Используем системную Java.', }); javaPath = 'java'; } // Далее пробуем установить Minecraft, но продолжаем даже при ошибках let resolvedVersion; try { // 1. Получаем список версий и устанавливаем ванильный Minecraft event.sender.send('installation-status', { step: 'minecraft-list', message: 'Получение списка версий Minecraft...', }); const versionList = await getVersionList(); const minecraftVersion = versionList.versions.find( (v) => v.id === baseVersion, ); if (minecraftVersion) { // Устанавливаем базовую версию Minecraft event.sender.send('installation-status', { step: 'minecraft-install', message: `Установка Minecraft ${baseVersion}...`, }); try { const installMcTask = installTask(minecraftVersion, minecraftDir, { skipRevalidate: true, }); await installMcTask.startAndWait({ onStart(task) { event.sender.send('installation-status', { step: `minecraft-install.${task.path}`, message: `Начало: ${task.name || task.path}`, }); }, onUpdate(task) { const percentage = Math.round( (installMcTask.progress / installMcTask.total) * 100, ) || 0; event.sender.send('download-progress', percentage); event.sender.send('installation-status', { step: `minecraft-install.${task.path}`, message: `Прогресс ${task.name || task.path}: ${percentage}% (${installMcTask.progress}/${installMcTask.total})`, }); }, onFailed(task, error) { console.warn( `Ошибка при установке ${task.path}, продолжаем:`, error, ); event.sender.send('installation-status', { step: `minecraft-install.${task.path}`, message: `Ошибка: ${error.message}`, }); }, onSucceed(task) { event.sender.send('installation-status', { step: `minecraft-install.${task.path}`, message: `Завершено: ${task.name || task.path}`, }); }, }); } catch (error) { console.warn('Ошибка при установке Minecraft, продолжаем:', error); } // 2. Устанавливаем Fabric try { event.sender.send('installation-status', { step: 'fabric-list', message: 'Получение списка версий Fabric...', }); if (fabricVersion) { event.sender.send('installation-status', { step: 'fabric-install', message: `Установка Fabric ${fabricVersion}...`, }); await installFabric({ minecraftVersion: baseVersion, version: fabricVersion, // Используйте напрямую, без .version minecraft: minecraftDir, }); } } catch (error) { console.warn('Ошибка при установке Fabric, продолжаем:', error); } // 3. Подготовка версии и установка зависимостей try { // Используем идентификатор Fabric-версии const fabricVersionId = `${baseVersion}-fabric${fabricVersion}`; event.sender.send('installation-status', { step: 'version-parse', message: 'Подготовка версии...', }); resolvedVersion = await Version.parse( minecraftDir, fabricVersionId, ); event.sender.send('installation-status', { step: 'dependencies', message: 'Установка библиотек и ресурсов...', }); const depsTask = installDependenciesTask(resolvedVersion, { assetsDownloadConcurrency: 4, skipRevalidate: true, prevalidSizeOnly: true, checksumValidatorResolver: (checksum) => ({ validate: async () => { /* void */ }, }), }); try { await depsTask.startAndWait({ onStart(task) { event.sender.send('installation-status', { step: `dependencies.${task.path}`, message: `Начало: ${task.name || task.path}`, }); }, onUpdate(task) { const percentage = Math.round((depsTask.progress / depsTask.total) * 100) || 0; event.sender.send('download-progress', percentage); event.sender.send('installation-status', { step: `dependencies.${task.path}`, message: `Установка ${task.name || task.path}: ${percentage}%`, }); }, onFailed(task, error) { console.warn( `Ошибка при установке ${task.path}, продолжаем:`, error, ); event.sender.send('installation-status', { step: `dependencies.${task.path}`, message: `Ошибка: ${error.message}`, }); }, onSucceed(task) { event.sender.send('installation-status', { step: `dependencies.${task.path}`, message: `Завершено: ${task.name || task.path}`, }); }, }); } catch (error) { console.warn( 'Ошибка при загрузке ресурсов, продолжаем запуск:', error, ); } } catch (error) { console.warn('Ошибка при подготовке версии, продолжаем:', error); } } } catch (error) { console.warn('Произошла ошибка при подготовке Minecraft:', error); } // Загрузка и проверка authlib-injector const authlibPath = await ensureAuthlibInjectorExists(appPath); event.sender.send('installation-status', { step: 'authlib-injector', message: 'authlib-injector готов', }); // Запускаем Minecraft с authlib-injector для Ely.by event.sender.send('installation-status', { step: 'launch', message: 'Запуск игры...', }); // При запуске используем переданные параметры const packDir = path.join(versionsDir, packName); // При формировании конфигурации запуска создаем объект server только с нужными параметрами const serverConfig: any = { ip: serverIp }; // Добавляем порт только если он был передан if (serverPort) { serverConfig.port = serverPort; } const proc = await launch({ gamePath: packDir, resourcePath: minecraftDir, javaPath, version: versionToLaunch, launcherName: 'popa-popa', server: serverConfig, // Используем созданный объект конфигурации extraJVMArgs: [ '-Dlog4j2.formatMsgNoLookups=true', `-javaagent:${authlibPath}=ely.by`, `-Xmx${memory}M`, ], // Используем данные аутентификации Yggdrasil accessToken, gameProfile: { id: uuid, name: username, }, }); // Логирование proc.stdout?.on('data', (data) => { console.log(`Minecraft stdout: ${data}`); }); proc.stderr?.on('data', (data) => { console.error(`Minecraft stderr: ${data}`); }); return { success: true, pid: proc.pid }; } catch (error) { console.error('Ошибка при запуске Minecraft:', error); event.sender.send('installation-status', { step: 'error', message: `Ошибка запуска: ${error.message}`, }); return { success: false, error: error.message }; } }); // Добавьте в функцию initMinecraftHandlers или создайте новую ipcMain.handle('get-pack-files', async (event, packName) => { try { const appPath = path.dirname(app.getPath('exe')); const minecraftDir = path.join(appPath, '.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 }; } }); // Добавьте в функцию initMinecraftHandlers новый обработчик ipcMain.handle('get-available-versions', async (event) => { try { const appPath = path.dirname(app.getPath('exe')); const minecraftDir = path.join(appPath, '.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()) { // Проверяем, есть ли конфигурация для пакета const versionJsonPath = path.join(versionPath, `${item}.json`); 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 }; } }); } // Добавляем обработчики 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) => { try { const clientToken = JSON.parse( fs.readFileSync( path.join(app.getPath('userData'), 'config.json'), 'utf8', ), ).clientToken; 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 appPath = path.dirname(app.getPath('exe')); const minecraftDir = path.join(appPath, '.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 appPath = path.dirname(app.getPath('exe')); const minecraftDir = path.join(appPath, '.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 appPath = path.dirname(app.getPath('exe')); const minecraftDir = path.join(appPath, '.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.4', serverIp: 'popa-popa.ru', fabricVersion: '0.16.14', 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'; } // Если есть конфигурационный файл, загружаем из него 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 }; } });