import { existsSync } from 'fs' import { join } from 'path' import { getVersionList, installTask, installNeoForgedTask } from '@xmcl/installer' import type { Task } from '@xmcl/task' import { paths } from './paths' import { emit } from './events' import { type LaunchPhase } from '../shared/ipc' import { downloadDispatcher, DOWNLOAD_CONCURRENCY, withRetries } from './net' /** Options de téléchargement communes : dispatcher tolérant + concurrence bridée. */ const downloadOptions = { dispatcher: downloadDispatcher, assetsDownloadConcurrency: DOWNLOAD_CONCURRENCY, librariesDownloadConcurrency: DOWNLOAD_CONCURRENCY } /** * Exécute une task @xmcl en émettant sa progression réelle (0..1) vers le * renderer. On lit la progression cumulée de la task racine ; on n'émet qu'au * changement de pourcentage entier pour ne pas inonder l'IPC. */ async function runWithProgress(phase: LaunchPhase, message: string, task: Task): Promise { let lastPct = -1 return task.startAndWait({ onUpdate() { if (task.total <= 0) return const ratio = task.progress / task.total const pct = Math.floor(ratio * 100) if (pct === lastPct) return lastPct = pct emit.progress({ phase, message, progress: Math.min(1, ratio) }) } }) } /** * Installe le runtime Minecraft (vanilla 1.21.1) puis NeoForge dans * paths.gameRoot. Les deux étapes sont idempotentes : si la version est déjà * présente sur le disque, on ne refait pas le travail. * * Retourne l'id de version NeoForge à lancer (ex. "neoforge-21.1.73"). */ /** true si un dossier de version avec son JSON existe déjà. */ function versionInstalled(id: string): boolean { return existsSync(join(paths.gameRoot, 'versions', id, `${id}.json`)) } /** * Installe Minecraft vanilla (json + jar + assets + libraries). * `force` saute le court-circuit "déjà installé" pour revalider/réparer les * fichiers (l'install @xmcl ignore les fichiers déjà valides par checksum). */ export async function installMinecraft(version: string, force = false): Promise { if (!force && versionInstalled(version)) return emit.progress({ phase: 'minecraft', message: `Minecraft ${version} : métadonnées…`, progress: undefined }) const list = await getVersionList() const meta = list.versions.find((v) => v.id === version) if (!meta) throw new Error(`Version Minecraft introuvable : ${version}`) const label = force ? `Vérification de Minecraft ${version}…` : `Installation de Minecraft ${version} (assets + libs)…` // ~3700 assets : on réessaie l'install entière plusieurs fois. Chaque passe // ignore les fichiers déjà valides et ne reprend que les manquants. await withRetries( () => runWithProgress('minecraft', label, installTask(meta, paths.gameRoot, downloadOptions)), 5, (attempt) => emit.progress({ phase: 'minecraft', message: `Reprise des téléchargements (tentative ${attempt + 1})…`, progress: undefined }) ) } /** * Installe NeoForge par-dessus Minecraft . * Nécessite un Java (les "processors" de l'installeur tournent en Java). */ export async function installNeoForge( neoforge: string, minecraft: string, javaPath: string, force = false ): Promise { // @xmcl nomme la version installée "neoforge-" (ex. neoforge-21.1.224). // Si elle est déjà présente, on saute l'install (idempotent, comme Minecraft). const expectedId = `neoforge-${neoforge}` if (!force && versionInstalled(expectedId)) { void minecraft return expectedId } const label = force ? `Vérification de NeoForge ${neoforge}…` : `Installation de NeoForge ${neoforge}…` // installNeoForgedTask retourne l'id de version installée à lancer. const versionId = await withRetries( () => runWithProgress( 'neoforge', label, installNeoForgedTask('neoforge', neoforge, paths.gameRoot, { java: javaPath, dispatcher: downloadDispatcher, librariesDownloadConcurrency: DOWNLOAD_CONCURRENCY }) ), 3, (attempt) => emit.progress({ phase: 'neoforge', message: `Reprise de l'installation NeoForge (tentative ${attempt + 1})…`, progress: undefined }) ) if (!versionInstalled(versionId)) { throw new Error(`NeoForge installé mais version "${versionId}" introuvable sur le disque.`) } void minecraft // (info de contexte ; la version NeoForge hérite déjà de Minecraft) return versionId }