Files
popa-launcher/src/main/minecraft-launcher.ts
2026-01-01 22:43:07 +05:00

1390 lines
50 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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

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<string> {
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<void> {
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<string> {
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<string> {
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 };
}
});