working authirization
This commit is contained in:
90
src/main/auth-service.ts
Normal file
90
src/main/auth-service.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { YggdrasilClient, YggrasilAuthentication } from '@xmcl/user';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// Ely.by сервер
|
||||
const ELY_BY_AUTH_SERVER = 'https://authserver.ely.by';
|
||||
|
||||
export class AuthService {
|
||||
private client: YggdrasilClient;
|
||||
|
||||
constructor() {
|
||||
this.client = new YggdrasilClient(ELY_BY_AUTH_SERVER);
|
||||
}
|
||||
|
||||
async login(
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<YggrasilAuthentication> {
|
||||
try {
|
||||
// Генерируем уникальный clientToken
|
||||
const clientToken = uuidv4();
|
||||
|
||||
// Выполняем запрос напрямую к правильному URL
|
||||
const response = await fetch(`${ELY_BY_AUTH_SERVER}/auth/authenticate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password,
|
||||
clientToken,
|
||||
requestUser: true,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Ошибка авторизации: ${response.status} ${errorText}`);
|
||||
}
|
||||
|
||||
const auth = await response.json();
|
||||
console.log(`Аутентификация успешна для ${auth.selectedProfile?.name}`);
|
||||
return auth;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при авторизации:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async validate(accessToken: string, clientToken: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${ELY_BY_AUTH_SERVER}/auth/validate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ accessToken, clientToken }),
|
||||
});
|
||||
|
||||
return response.ok;
|
||||
} catch (error) {
|
||||
console.error('Ошибка при валидации токена:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async refresh(
|
||||
accessToken: string,
|
||||
clientToken: string,
|
||||
): Promise<YggrasilAuthentication | null> {
|
||||
try {
|
||||
const response = await fetch(`${ELY_BY_AUTH_SERVER}/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ accessToken, clientToken }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Ошибка при обновлении токена:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
517
src/main/main.ts
517
src/main/main.ts
@ -14,121 +14,7 @@ import { autoUpdater } from 'electron-updater';
|
||||
import log from 'electron-log';
|
||||
import MenuBuilder from './menu';
|
||||
import { resolveHtmlPath } from './util';
|
||||
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 { Task } from '@xmcl/task';
|
||||
// import findJavaHome from 'find-java-home';
|
||||
|
||||
// Функция для поиска Java
|
||||
async function findJava(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
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)) {
|
||||
return resolve(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',
|
||||
].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)) {
|
||||
return resolve(javaPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Ошибка при сканировании ${basePath}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Пробуем найти java в PATH через команду which/where
|
||||
try {
|
||||
const command =
|
||||
process.platform === 'win32' ? 'where java' : 'which java';
|
||||
const javaPath = execSync(command).toString().trim();
|
||||
|
||||
if (javaPath && fs.existsSync(javaPath)) {
|
||||
return resolve(javaPath);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Ошибка при поиске java через PATH:', err);
|
||||
}
|
||||
|
||||
// Если Java не найдена, выдаем ошибку
|
||||
reject(
|
||||
new Error('Java не найдена. Установите Java и повторите попытку.'),
|
||||
);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
import { initMinecraftHandlers, initAuthHandlers } from './minecraft-launcher';
|
||||
|
||||
class AppUpdater {
|
||||
constructor() {
|
||||
@ -158,402 +44,8 @@ if (isDebug) {
|
||||
require('electron-debug').default();
|
||||
}
|
||||
|
||||
// Minecraft
|
||||
|
||||
const COMFORT_DOWNLOAD_URL =
|
||||
'https://github.com/DIKER0K/Comfort/releases/latest/download/Comfort.zip';
|
||||
const GITHUB_API_RELEASE_URL =
|
||||
'https://api.github.com/repos/DIKER0K/Comfort/releases/latest';
|
||||
const COMFORT_VERSION_FILE = 'comfort_version.txt';
|
||||
|
||||
async function getLatestReleaseVersion(): Promise<string> {
|
||||
try {
|
||||
const response = await fetch(GITHUB_API_RELEASE_URL);
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
// Модифицируем обработчик IPC
|
||||
ipcMain.handle('download-and-extract', async (event) => {
|
||||
try {
|
||||
const appPath = path.dirname(app.getPath('exe'));
|
||||
const minecraftDir = path.join(appPath, '.minecraft');
|
||||
const versionsDir = path.join(minecraftDir, 'versions');
|
||||
const versionFilePath = path.join(minecraftDir, COMFORT_VERSION_FILE);
|
||||
|
||||
// Получаем текущую и последнюю версии
|
||||
const latestVersion = await getLatestReleaseVersion();
|
||||
let currentVersion = '';
|
||||
|
||||
// Проверяем текущую версию, если файл существует
|
||||
if (fs.existsSync(versionFilePath)) {
|
||||
currentVersion = fs.readFileSync(versionFilePath, 'utf-8').trim();
|
||||
}
|
||||
|
||||
// Проверяем, нужно ли обновление
|
||||
if (currentVersion === latestVersion) {
|
||||
return { success: true, updated: false, version: currentVersion };
|
||||
}
|
||||
|
||||
const tempDir = path.join(appPath, 'temp');
|
||||
|
||||
// Создаем/очищаем временную директорию
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true });
|
||||
}
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
|
||||
const zipPath = path.join(tempDir, 'Comfort.zip');
|
||||
|
||||
// Скачиваем файл
|
||||
await downloadFile(COMFORT_DOWNLOAD_URL, 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 });
|
||||
}
|
||||
|
||||
// Распаковываем архив напрямую в папку versions
|
||||
await extract(zipPath, { dir: versionsDir });
|
||||
fs.unlinkSync(zipPath);
|
||||
|
||||
// Сохраняем новую версию
|
||||
fs.writeFileSync(versionFilePath, latestVersion);
|
||||
|
||||
// Удаляем временную директорию
|
||||
fs.rmSync(tempDir, { recursive: true });
|
||||
|
||||
// После распаковки архива и перед запуском
|
||||
const versionsContents = fs.readdirSync(versionsDir);
|
||||
console.log('Доступные версии:', versionsContents);
|
||||
|
||||
return { success: true, updated: true, version: latestVersion };
|
||||
} catch (error) {
|
||||
console.error('Error in download-and-extract:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
ipcMain.handle('launch-minecraft', async (event) => {
|
||||
try {
|
||||
const baseVersion = '1.21.4';
|
||||
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);
|
||||
|
||||
// Найти версию Comfort или версию с Fabric
|
||||
let versionToLaunch = '';
|
||||
if (versionsContents.includes('1.21.4-fabric0.16.14')) {
|
||||
versionToLaunch = '1.21.4-fabric0.16.14';
|
||||
} else if (versionsContents.includes('Comfort')) {
|
||||
versionToLaunch = 'Comfort';
|
||||
} else {
|
||||
versionToLaunch = '1.21.4';
|
||||
}
|
||||
|
||||
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'; // Пробуем использовать системную 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...',
|
||||
});
|
||||
|
||||
const fabricVersions = await getFabricLoaders();
|
||||
const fabricVersion = fabricVersions[0]; // Последняя версия
|
||||
|
||||
if (fabricVersion) {
|
||||
event.sender.send('installation-status', {
|
||||
step: 'fabric-install',
|
||||
message: `Установка Fabric ${fabricVersion.version}...`,
|
||||
});
|
||||
|
||||
await installFabric({
|
||||
minecraftVersion: baseVersion,
|
||||
version: fabricVersion.version,
|
||||
minecraft: minecraftDir,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Ошибка при установке Fabric, продолжаем:', error);
|
||||
}
|
||||
|
||||
// 3. Подготовка версии и установка зависимостей
|
||||
try {
|
||||
// Используем идентификатор Fabric-версии
|
||||
const fabricVersionId = `${baseVersion}-fabric0.16.14`;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 5. Запускаем Minecraft - НЕЗАВИСИМО ОТ ПРЕДЫДУЩИХ ОШИБОК
|
||||
event.sender.send('installation-status', {
|
||||
step: 'launch',
|
||||
message: 'Запуск игры...',
|
||||
});
|
||||
|
||||
const comfortDir = path.join(versionsDir, 'Comfort');
|
||||
const proc = await launch({
|
||||
gamePath: comfortDir,
|
||||
resourcePath: minecraftDir,
|
||||
javaPath,
|
||||
version: '1.21.4-fabric0.16.14',
|
||||
extraJVMArgs: ['-Dlog4j2.formatMsgNoLookups=true', '-Xmx2G'], // Добавляем больше памяти
|
||||
});
|
||||
|
||||
// Логирование
|
||||
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 };
|
||||
}
|
||||
});
|
||||
// Инициализация обработчиков Minecraft
|
||||
initMinecraftHandlers();
|
||||
|
||||
const installExtensions = async () => {
|
||||
const installer = require('electron-devtools-installer');
|
||||
@ -623,6 +115,9 @@ const createWindow = async () => {
|
||||
// Remove this if your app does not use auto updates
|
||||
// eslint-disable-next-line
|
||||
new AppUpdater();
|
||||
|
||||
initAuthHandlers();
|
||||
initMinecraftHandlers();
|
||||
};
|
||||
|
||||
/**
|
||||
|
739
src/main/minecraft-launcher.ts
Normal file
739
src/main/minecraft-launcher.ts
Normal file
@ -0,0 +1,739 @@
|
||||
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 COMFORT_DOWNLOAD_URL =
|
||||
'https://github.com/DIKER0K/Comfort/releases/latest/download/Comfort.zip';
|
||||
const GITHUB_API_RELEASE_URL =
|
||||
'https://api.github.com/repos/DIKER0K/Comfort/releases/latest';
|
||||
const COMFORT_VERSION_FILE = 'comfort_version.txt';
|
||||
const AUTHLIB_INJECTOR_FILENAME = 'authlib-injector-1.2.5.jar';
|
||||
|
||||
// Создаем экземпляр сервиса аутентификации
|
||||
const authService = new AuthService();
|
||||
|
||||
// Функция для получения последней версии релиза
|
||||
export async function getLatestReleaseVersion(): Promise<string> {
|
||||
try {
|
||||
const response = await fetch(GITHUB_API_RELEASE_URL);
|
||||
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 = [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) => {
|
||||
try {
|
||||
const appPath = path.dirname(app.getPath('exe'));
|
||||
const minecraftDir = path.join(appPath, '.minecraft');
|
||||
const versionsDir = path.join(minecraftDir, 'versions');
|
||||
const versionFilePath = path.join(minecraftDir, COMFORT_VERSION_FILE);
|
||||
|
||||
// Получаем текущую и последнюю версии
|
||||
const latestVersion = await getLatestReleaseVersion();
|
||||
let currentVersion = '';
|
||||
|
||||
// Проверяем текущую версию, если файл существует
|
||||
if (fs.existsSync(versionFilePath)) {
|
||||
currentVersion = fs.readFileSync(versionFilePath, 'utf-8').trim();
|
||||
}
|
||||
|
||||
// Проверяем, нужно ли обновление
|
||||
if (currentVersion === latestVersion) {
|
||||
return { success: true, updated: false, version: currentVersion };
|
||||
}
|
||||
|
||||
const tempDir = path.join(appPath, 'temp');
|
||||
|
||||
// Создаем/очищаем временную директорию
|
||||
if (fs.existsSync(tempDir)) {
|
||||
fs.rmSync(tempDir, { recursive: true });
|
||||
}
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
|
||||
const zipPath = path.join(tempDir, 'Comfort.zip');
|
||||
|
||||
// Скачиваем файл
|
||||
await downloadFile(COMFORT_DOWNLOAD_URL, 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 });
|
||||
}
|
||||
|
||||
// Распаковываем архив напрямую в папку versions
|
||||
await extract(zipPath, { dir: versionsDir });
|
||||
fs.unlinkSync(zipPath);
|
||||
|
||||
// Сохраняем новую версию
|
||||
fs.writeFileSync(versionFilePath, latestVersion);
|
||||
|
||||
// Удаляем временную директорию
|
||||
fs.rmSync(tempDir, { recursive: true });
|
||||
|
||||
// После распаковки архива и перед запуском
|
||||
const versionsContents = fs.readdirSync(versionsDir);
|
||||
console.log('Доступные версии:', versionsContents);
|
||||
|
||||
return { success: true, updated: true, version: latestVersion };
|
||||
} catch (error) {
|
||||
console.error('Error in download-and-extract:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик для запуска Minecraft
|
||||
ipcMain.handle('launch-minecraft', async (event, gameConfig) => {
|
||||
try {
|
||||
const baseVersion = '1.21.4';
|
||||
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);
|
||||
|
||||
// Найти версию Comfort или версию с Fabric
|
||||
let versionToLaunch = '';
|
||||
if (versionsContents.includes('1.21.4-fabric0.16.14')) {
|
||||
versionToLaunch = '1.21.4-fabric0.16.14';
|
||||
} else if (versionsContents.includes('Comfort')) {
|
||||
versionToLaunch = 'Comfort';
|
||||
} else {
|
||||
versionToLaunch = '1.21.4';
|
||||
}
|
||||
|
||||
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...',
|
||||
});
|
||||
|
||||
const fabricVersions = await getFabricLoaders();
|
||||
const fabricVersion = fabricVersions[0]; // Последняя версия
|
||||
|
||||
if (fabricVersion) {
|
||||
event.sender.send('installation-status', {
|
||||
step: 'fabric-install',
|
||||
message: `Установка Fabric ${fabricVersion.version}...`,
|
||||
});
|
||||
|
||||
await installFabric({
|
||||
minecraftVersion: baseVersion,
|
||||
version: fabricVersion.version,
|
||||
minecraft: minecraftDir,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Ошибка при установке Fabric, продолжаем:', error);
|
||||
}
|
||||
|
||||
// 3. Подготовка версии и установка зависимостей
|
||||
try {
|
||||
// Используем идентификатор Fabric-версии
|
||||
const fabricVersionId = `${baseVersion}-fabric0.16.14`;
|
||||
|
||||
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 comfortDir = path.join(versionsDir, 'Comfort');
|
||||
const proc = await launch({
|
||||
gamePath: comfortDir,
|
||||
resourcePath: minecraftDir,
|
||||
javaPath,
|
||||
version: versionToLaunch,
|
||||
extraJVMArgs: [
|
||||
'-Dlog4j2.formatMsgNoLookups=true',
|
||||
`-javaagent:${authlibPath}=ely.by`,
|
||||
`-Xmx${gameConfig.memory || 2048}M`,
|
||||
],
|
||||
// Используем данные аутентификации Yggdrasil
|
||||
accessToken: gameConfig.accessToken,
|
||||
gameProfile: {
|
||||
id: gameConfig.uuid,
|
||||
name: gameConfig.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 };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Добавляем обработчики 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;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
@ -1,177 +1,94 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
const useAuth = () => {
|
||||
const [status, setStatus] = useState('');
|
||||
interface AuthSession {
|
||||
accessToken: string;
|
||||
clientToken: string;
|
||||
selectedProfile: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
const validateSession = async (accessToken: string) => {
|
||||
export default function useAuth() {
|
||||
const [status, setStatus] = useState('idle');
|
||||
|
||||
// Проверка валидности токена
|
||||
const validateSession = async (accessToken: string): Promise<boolean> => {
|
||||
try {
|
||||
const response = await fetch('https://authserver.ely.by/auth/validate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
accessToken: accessToken,
|
||||
}),
|
||||
});
|
||||
return response.ok;
|
||||
setStatus('validating');
|
||||
const response = await window.electron.ipcRenderer.invoke(
|
||||
'validate-token',
|
||||
accessToken,
|
||||
);
|
||||
setStatus('idle');
|
||||
return response.valid;
|
||||
} catch (error) {
|
||||
console.log(`Ошибка при проверке токена: ${error.message}`);
|
||||
console.error('Ошибка при валидации токена:', error);
|
||||
setStatus('error');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshSession = async (accessToken: string, clientToken: string) => {
|
||||
// Обновление токена
|
||||
const refreshSession = async (
|
||||
accessToken: string,
|
||||
clientToken: string,
|
||||
): Promise<AuthSession | null> => {
|
||||
try {
|
||||
const refreshData = {
|
||||
accessToken: accessToken,
|
||||
clientToken: clientToken,
|
||||
};
|
||||
|
||||
const response = await fetch('https://authserver.ely.by/auth/refresh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(refreshData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const newAccessToken = data.accessToken;
|
||||
const profile = data.selectedProfile;
|
||||
const uuid = profile.id;
|
||||
const name = profile.name;
|
||||
|
||||
if (newAccessToken && uuid && name) {
|
||||
return {
|
||||
accessToken: newAccessToken,
|
||||
uuid: uuid,
|
||||
username: name,
|
||||
clientToken: clientToken,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
setStatus('refreshing');
|
||||
const response = await window.electron.ipcRenderer.invoke(
|
||||
'refresh-token',
|
||||
{ accessToken, clientToken },
|
||||
);
|
||||
setStatus('idle');
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.log(`Ошибка при обновлении сессии: ${error.message}`);
|
||||
console.error('Ошибка при обновлении токена:', error);
|
||||
setStatus('error');
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Аутентификация в Ely.by
|
||||
const authenticateWithElyBy = async (
|
||||
username: string,
|
||||
password: string,
|
||||
saveConfig: Function,
|
||||
) => {
|
||||
saveConfigFunc: Function,
|
||||
): Promise<AuthSession | null> => {
|
||||
try {
|
||||
const clientToken = crypto.randomUUID();
|
||||
const authData = {
|
||||
username: username,
|
||||
password: password,
|
||||
clientToken: clientToken,
|
||||
requestUser: true,
|
||||
};
|
||||
|
||||
console.log(`Аутентификация пользователя ${username} на Ely.by...`);
|
||||
|
||||
const response = await fetch(
|
||||
'https://authserver.ely.by/auth/authenticate',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(authData),
|
||||
},
|
||||
setStatus('authenticating');
|
||||
const response = await window.electron.ipcRenderer.invoke(
|
||||
'authenticate',
|
||||
{ username, password },
|
||||
);
|
||||
|
||||
const responseData = await response.json();
|
||||
if (response.ok) {
|
||||
const accessToken = responseData.accessToken;
|
||||
const profile = responseData.selectedProfile;
|
||||
const uuid = profile.id;
|
||||
const name = profile.name;
|
||||
if (response && response.accessToken) {
|
||||
// Правильно сохраняем данные в конфигурации
|
||||
saveConfigFunc({
|
||||
username: response.selectedProfile.name, // Имя игрока как строка
|
||||
uuid: response.selectedProfile.id,
|
||||
accessToken: response.accessToken,
|
||||
clientToken: response.clientToken,
|
||||
memory: 4096, // Сохраняем значение по умолчанию или из предыдущей конфигурации
|
||||
});
|
||||
|
||||
if (accessToken && uuid && name) {
|
||||
saveConfig(
|
||||
username,
|
||||
4096, // default memory
|
||||
accessToken,
|
||||
clientToken,
|
||||
'',
|
||||
password,
|
||||
);
|
||||
console.log(`Аутентификация успешна: UUID=${uuid}, Username=${name}`);
|
||||
|
||||
return {
|
||||
accessToken: accessToken,
|
||||
uuid: uuid,
|
||||
username: name,
|
||||
clientToken: clientToken,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (responseData.error === 'Account protected with two factor auth') {
|
||||
const totpToken = prompt(
|
||||
'Введите код двухфакторной аутентификации:',
|
||||
'',
|
||||
);
|
||||
|
||||
if (totpToken) {
|
||||
authData.password = `${password}:${totpToken}`;
|
||||
const totpResponse = await fetch(
|
||||
'https://authserver.ely.by/auth/authenticate',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(authData),
|
||||
},
|
||||
);
|
||||
if (totpResponse.ok) {
|
||||
const totpData = await totpResponse.json();
|
||||
const newAccessToken = totpData.accessToken;
|
||||
const newProfile = totpData.selectedProfile;
|
||||
const newUuid = newProfile.id;
|
||||
const newName = newProfile.name;
|
||||
|
||||
if (newAccessToken && newUuid && newName) {
|
||||
saveConfig(
|
||||
username,
|
||||
4096, // default memory
|
||||
newAccessToken,
|
||||
clientToken,
|
||||
'',
|
||||
password,
|
||||
);
|
||||
return {
|
||||
accessToken: newAccessToken,
|
||||
uuid: newUuid,
|
||||
username: newName,
|
||||
clientToken: clientToken,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(responseData.error || 'Ошибка авторизации');
|
||||
setStatus('authenticated');
|
||||
return response;
|
||||
}
|
||||
|
||||
setStatus('error');
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.log(`Ошибка авторизации: ${error.message}`);
|
||||
console.error('Ошибка при аутентификации:', error);
|
||||
setStatus('error');
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
status,
|
||||
setStatus,
|
||||
validateSession,
|
||||
refreshSession,
|
||||
authenticateWithElyBy,
|
||||
};
|
||||
};
|
||||
|
||||
export default useAuth;
|
||||
}
|
||||
|
@ -1,7 +1,18 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
// Добавляем определение типа Config
|
||||
interface Config {
|
||||
username: string;
|
||||
password: string;
|
||||
memory: number;
|
||||
comfortVersion: string;
|
||||
accessToken: string;
|
||||
clientToken: string;
|
||||
uuid?: string; // Добавляем uuid, который используется для авторизации
|
||||
}
|
||||
|
||||
const useConfig = () => {
|
||||
const [config, setConfig] = useState({
|
||||
const [config, setConfig] = useState<Config>({
|
||||
username: '',
|
||||
password: '',
|
||||
memory: 4096,
|
||||
@ -34,28 +45,10 @@ const useConfig = () => {
|
||||
setConfig(savedConfig);
|
||||
}, []);
|
||||
|
||||
const saveConfig = (
|
||||
username: string,
|
||||
memory: number,
|
||||
accessToken = '',
|
||||
clientToken = '',
|
||||
comfortVersion = '',
|
||||
password = '',
|
||||
) => {
|
||||
try {
|
||||
const newConfig = {
|
||||
username,
|
||||
memory,
|
||||
accessToken: accessToken || config.accessToken,
|
||||
clientToken: clientToken || config.clientToken,
|
||||
comfortVersion: comfortVersion || config.comfortVersion,
|
||||
password: password || config.password,
|
||||
};
|
||||
setConfig(newConfig);
|
||||
localStorage.setItem('launcher_config', JSON.stringify(newConfig));
|
||||
} catch (error) {
|
||||
console.log(`Ошибка при сохранении конфигурации: ${error.message}`);
|
||||
}
|
||||
const saveConfig = (newConfig: Partial<Config>) => {
|
||||
const updatedConfig = { ...config, ...newConfig };
|
||||
setConfig(updatedConfig);
|
||||
localStorage.setItem('launcher_config', JSON.stringify(updatedConfig));
|
||||
};
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
@ -33,22 +33,6 @@ const LaunchPage = () => {
|
||||
navigate('/login');
|
||||
}
|
||||
|
||||
window.electron.ipcRenderer.on('download-progress', (progress: any) => {
|
||||
setDownloadProgress(progress as number);
|
||||
});
|
||||
|
||||
// Добавляем слушатель для статуса установки
|
||||
window.electron.ipcRenderer.on('installation-progress', (data: any) => {
|
||||
setInstallStatus((data as { status: string }).status);
|
||||
});
|
||||
|
||||
// Обновляем слушатель для статуса установки
|
||||
window.electron.ipcRenderer.on('installation-status', (data: any) => {
|
||||
const { step, message } = data as { step: string; message: string };
|
||||
setInstallStep(step);
|
||||
setInstallMessage(message);
|
||||
});
|
||||
|
||||
return () => {
|
||||
window.electron.ipcRenderer.removeAllListeners('download-progress');
|
||||
window.electron.ipcRenderer.removeAllListeners('installation-progress');
|
||||
@ -77,6 +61,11 @@ const LaunchPage = () => {
|
||||
setIsDownloading(true);
|
||||
setDownloadProgress(0);
|
||||
|
||||
// Загружаем настройки и токены
|
||||
const savedConfig = JSON.parse(
|
||||
localStorage.getItem('launcher_config') || '{}',
|
||||
);
|
||||
|
||||
// Сначала проверяем и обновляем файлы
|
||||
const downloadResult = await window.electron.ipcRenderer.invoke(
|
||||
'download-and-extract',
|
||||
@ -95,9 +84,16 @@ const LaunchPage = () => {
|
||||
);
|
||||
}
|
||||
|
||||
// Затем запускаем Minecraft
|
||||
const launchResult =
|
||||
await window.electron.ipcRenderer.invoke('launch-minecraft');
|
||||
// Затем запускаем Minecraft с данными авторизации
|
||||
const launchResult = await window.electron.ipcRenderer.invoke(
|
||||
'launch-minecraft',
|
||||
{
|
||||
accessToken: savedConfig.accessToken,
|
||||
uuid: savedConfig.uuid,
|
||||
username: savedConfig.username,
|
||||
memory: savedConfig.memory || 4096,
|
||||
},
|
||||
);
|
||||
|
||||
if (launchResult?.success) {
|
||||
showNotification('Minecraft успешно запущен!', 'success');
|
||||
|
Reference in New Issue
Block a user