diff --git a/src/main/minecraft-launcher.ts b/src/main/minecraft-launcher.ts index aa33fe3..1aacbe9 100644 --- a/src/main/minecraft-launcher.ts +++ b/src/main/minecraft-launcher.ts @@ -42,6 +42,36 @@ import { API_BASE_URL } from '../renderer/api'; // }, // }; +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/'; @@ -54,6 +84,8 @@ const agent = new Agent({ // тут можно задать и другие параметры при необходимости }); +let currentMinecraftProcess: any | null = null; + // Модифицированная функция для получения последней версии релиза с произвольного URL export async function getLatestReleaseVersion(apiUrl: string): Promise { try { @@ -419,6 +451,9 @@ export function initMinecraftHandlers() { // Скачиваем файл await downloadFile(downloadUrl, zipPath, (progress) => { event.sender.send('download-progress', progress); + + const global = getGlobalProgress('download', progress / 100); + event.sender.send('overall-progress', global); }); // Проверяем архив @@ -617,7 +652,6 @@ export function initMinecraftHandlers() { assetsDownloadConcurrency: 2, librariesDownloadConcurrency: 2, dispatcher: agent, - // ...DOWNLOAD_OPTIONS, }); console.log('installMcTask started for', minecraftVersion.id); @@ -628,6 +662,16 @@ export function initMinecraftHandlers() { }); 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( @@ -666,6 +710,11 @@ export function initMinecraftHandlers() { message: `Установка Fabric ${fabricVersion}...`, }); + event.sender.send( + 'overall-progress', + getGlobalProgress('fabric-install', 0), + ); + console.log('installFabric:', { minecraftVersion: effectiveBaseVersion, fabricVersion, @@ -677,6 +726,10 @@ export function initMinecraftHandlers() { version: fabricVersion, minecraft: minecraftDir, }); + event.sender.send( + 'overall-progress', + getGlobalProgress('fabric-install', 1), + ); } catch (error) { console.log('Ошибка при установке Fabric, продолжаем:', error); } @@ -720,6 +773,13 @@ export function initMinecraftHandlers() { }); 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( @@ -758,6 +818,8 @@ export function initMinecraftHandlers() { message: 'Запуск игры...', }); + event.sender.send('overall-progress', getGlobalProgress('launch', 0)); + const proc = await launch({ gamePath: packDir, resourcePath: minecraftDir, @@ -785,6 +847,12 @@ export function initMinecraftHandlers() { }, }); + 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) => { @@ -810,6 +878,9 @@ export function initMinecraftHandlers() { 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', @@ -838,6 +909,24 @@ export function initMinecraftHandlers() { } }); + 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 { diff --git a/src/main/preload.ts b/src/main/preload.ts index a8b4f4f..e39343f 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -15,7 +15,11 @@ export type Channels = | 'get-installed-versions' | 'get-available-versions' | 'minecraft-log' - | 'minecraft-error'; + | 'minecraft-error' + | 'overall-progress' + | 'stop-minecraft' + | 'minecraft-started' + | 'minecraft-stopped'; const electronHandler = { ipcRenderer: { diff --git a/src/renderer/pages/LaunchPage.tsx b/src/renderer/pages/LaunchPage.tsx index 001bb49..d6834d3 100644 --- a/src/renderer/pages/LaunchPage.tsx +++ b/src/renderer/pages/LaunchPage.tsx @@ -59,7 +59,7 @@ const LaunchPage = ({ preserveFiles: [], }); const [isDownloading, setIsDownloading] = useState(false); - const [downloadProgress, setDownloadProgress] = useState(0); + const [progress, setProgress] = useState(0); const [buffer, setBuffer] = useState(10); const [installStatus, setInstallStatus] = useState(''); const [notification, setNotification] = useState<{ @@ -70,6 +70,7 @@ const LaunchPage = ({ const [installStep, setInstallStep] = useState(''); const [installMessage, setInstallMessage] = useState(''); const [open, setOpen] = React.useState(false); + const [isGameRunning, setIsGameRunning] = useState(false); const handleOpen = () => setOpen(true); const handleClose = () => setOpen(false); @@ -79,10 +80,10 @@ const LaunchPage = ({ navigate('/login'); } - const progressListener = (...args: unknown[]) => { - const progress = args[0] as number; - setDownloadProgress(progress); - setBuffer(Math.min(progress + 10, 100)); + const overallProgressListener = (...args: unknown[]) => { + const value = args[0] as number; // 0..100 + setProgress(value); + setBuffer(Math.min(value + 10, 100)); }; const statusListener = (...args: unknown[]) => { @@ -106,18 +107,34 @@ const LaunchPage = ({ ); }; + const minecraftStartedListener = () => { + setIsGameRunning(true); + }; + + const minecraftStoppedListener = () => { + setIsGameRunning(false); + }; + + window.electron.ipcRenderer.on('overall-progress', overallProgressListener); window.electron.ipcRenderer.on('minecraft-error', minecraftErrorListener); - window.electron.ipcRenderer.on('download-progress', progressListener); window.electron.ipcRenderer.on('installation-status', statusListener); + window.electron.ipcRenderer.on( + 'minecraft-started', + minecraftStartedListener, + ); + window.electron.ipcRenderer.on( + 'minecraft-stopped', + minecraftStoppedListener, + ); return () => { // Удаляем только конкретных слушателей, а не всех // Это безопаснее, чем removeAllListeners const cleanup = window.electron.ipcRenderer.on; if (typeof cleanup === 'function') { - cleanup('download-progress', progressListener); cleanup('installation-status', statusListener); cleanup('minecraft-error', statusListener); + cleanup('overall-progress', overallProgressListener); } // Удаляем использование removeAllListeners }; @@ -220,7 +237,6 @@ const LaunchPage = ({ const handleLaunchMinecraft = async () => { try { setIsDownloading(true); - setDownloadProgress(0); setBuffer(10); // Используем настройки выбранной версии или дефолтные @@ -305,6 +321,28 @@ const LaunchPage = ({ } }; + const handleStopMinecraft = async () => { + try { + const result = await window.electron.ipcRenderer.invoke('stop-minecraft'); + + if (result?.success) { + showNotification('Minecraft остановлен', 'info'); + setIsGameRunning(false); + } else if (result?.error) { + showNotification( + `Не удалось остановить Minecraft: ${result.error}`, + 'error', + ); + } + } catch (error: any) { + console.error('Ошибка при остановке Minecraft:', error); + showNotification( + `Ошибка при остановке Minecraft: ${error.message || String(error)}`, + 'error', + ); + } + }; + // Функция для сохранения настроек const savePackConfig = async () => { try { @@ -370,7 +408,7 @@ const LaunchPage = ({ @@ -378,7 +416,7 @@ const LaunchPage = ({ {`${Math.round(downloadProgress)}%`} + >{`${Math.round(progress)}%`} @@ -394,25 +432,42 @@ const LaunchPage = ({ {/* Вторая кнопка — квадратная, фиксированного размера (ширина = высоте) */}