import { spawn } from 'child_process' import { parse as parseToml } from 'smol-toml' import { paths } from './paths' import { config } from '../shared/config' import { fetchText } from './download' import { emit } from './events' import type { PackMeta } from '../shared/ipc' export type { PackMeta } /** * Gestion du modpack côté launcher : * - lecture du pack.toml packwiz distant (versions MC/NeoForge = source de vérité) * - sync incrémentale des mods/configs via packwiz-installer-bootstrap. * * packwiz-installer garde un manifeste local (packwiz.json dans l'instance) et * ne re-télécharge QUE les fichiers dont le hash a changé — c'est exactement le * comportement "pas de re-download complet à chaque update" recherché. */ /** Dernière PackMeta lue avec succès (pour l'affichage UI, y compris hors flux Jouer). */ let lastMeta: PackMeta | null = null /** Renvoie la dernière PackMeta connue, ou la récupère si jamais lue. Tolérant au offline. */ export async function getPackMetaCached(): Promise { if (lastMeta) return lastMeta try { return await fetchPackMeta() } catch { return null } } /** Télécharge et parse le pack.toml distant. */ export async function fetchPackMeta(): Promise { emit.progress({ phase: 'pack-meta', message: 'Lecture du modpack…', progress: undefined }) if (!config.packTomlUrl || config.packTomlUrl.includes('CHANGE_ME')) { throw new Error( 'URL du pack.toml non configurée. Renseigne packTomlUrl dans src/shared/config.ts.' ) } const raw = await fetchText(config.packTomlUrl) const data = parseToml(raw) as { name?: string version?: string versions?: { minecraft?: string; neoforge?: string } } const minecraft = data.versions?.minecraft const neoforge = data.versions?.neoforge if (!minecraft || !neoforge) { throw new Error( 'pack.toml invalide : [versions] doit contenir "minecraft" et "neoforge".' ) } lastMeta = { name: data.name ?? 'Modpack', version: data.version ?? '0', minecraft, neoforge } return lastMeta } /** * Lance packwiz-installer-bootstrap pour synchroniser l'instance. * `java` = chemin du binaire Java 21 géré (réutilisé pour faire tourner le jar). */ export async function syncModpack(javaPath: string): Promise { emit.progress({ phase: 'modpack', message: 'Synchronisation du modpack…', progress: undefined }) await new Promise((resolve, reject) => { const child = spawn( javaPath, ['-jar', paths.packwizBootstrapJar, '-g', '-s', 'client', config.packTomlUrl], { cwd: paths.instanceDir } ) // On garde les dernières lignes de sortie pour pouvoir afficher la VRAIE // erreur de packwiz si le process échoue. const recent: string[] = [] const onLine = (buf: Buffer): void => { for (const line of buf.toString().split(/\r?\n/)) { const text = line.trim() if (!text) continue recent.push(text) if (recent.length > 25) recent.shift() emit.progress({ phase: 'modpack', message: text, progress: undefined }) // Visible aussi dans la console du launcher. emit.gameLog({ stream: 'stdout', line: `[packwiz] ${text}` }) } } child.stdout.on('data', onLine) child.stderr.on('data', onLine) child.on('error', reject) child.on('close', (code) => { if (code === 0) return resolve() const tail = recent.slice(-12).join('\n') reject( new Error( `packwiz-installer a échoué (code ${code}).` + (tail ? `\nDernières lignes :\n${tail}` : '') ) ) }) }) emit.progress({ phase: 'modpack', message: 'Modpack à jour.', progress: 1 }) }