'use strict'; /* * launcher.js – Vanilla-Start-Pipeline (Phase 2) * ---------------------------------------------- * Lädt Version-JSON, Client-JAR, Libraries, Natives und Assets herunter, * wählt eine passende Java-Version und baut das Start-Kommando. * * Der eigentliche Prozess-Start ist an ein Konto gebunden (opts.auth): * Ohne gültigen Microsoft-Login (Phase 3) wird alles vorbereitet, aber * NICHT gestartet – wir bauen bewusst keinen Offline-/Crack-Bypass. * * Gemeinsame Spieldateien (versions/libraries/assets) liegen geteilt unter * , damit mehrere Instanzen sie sich teilen (spart Platz). */ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { spawn, spawnSync } = require('child_process'); const services = require('./services'); const loaders = require('./loaders'); const forge = require('./forge'); const processwatchdog = require('./processwatchdog'); const UA = 'AeroMC-Launcher/0.2'; const RESOURCES = 'https://resources.download.minecraft.net'; // --------------------------------------------------------------------------- // kleine Helfer // --------------------------------------------------------------------------- function ensureDir(d) { fs.mkdirSync(d, { recursive: true }); } function sha1File(file) { try { const h = crypto.createHash('sha1'); h.update(fs.readFileSync(file)); return h.digest('hex'); } catch { return null; } } async function fetchJson(url) { const res = await fetch(url, { headers: { 'User-Agent': UA } }); if (!res.ok) throw new Error('HTTP ' + res.status + ' bei ' + url); return res.json(); } // lädt eine Datei, überspringt sie bei passender sha1/Existenz async function downloadIfNeeded(url, dest, sha1) { if (fs.existsSync(dest)) { if (!sha1 || sha1File(dest) === sha1) return { skipped: true, bytes: 0 }; } ensureDir(path.dirname(dest)); const res = await fetch(url, { headers: { 'User-Agent': UA } }); if (!res.ok) throw new Error('Download HTTP ' + res.status + ': ' + url); const buf = Buffer.from(await res.arrayBuffer()); fs.writeFileSync(dest, buf); return { skipped: false, bytes: buf.length }; } // begrenzte Parallelität für viele kleine Downloads (Assets) async function runPool(items, worker, concurrency, onEach) { let idx = 0, done = 0; const total = items.length; async function next() { while (idx < total) { const i = idx++; await worker(items[i]); done++; if (onEach) onEach(done, total); } } const runners = []; for (let c = 0; c < Math.min(concurrency, total); c++) runners.push(next()); await Promise.all(runners); } // --------------------------------------------------------------------------- // Regeln (Libraries & Argumente) // --------------------------------------------------------------------------- const OS_NAME = 'windows'; function rulesAllow(rules) { if (!rules || !rules.length) return true; let action = 'disallow'; for (const r of rules) { let matches = true; if (r.os && r.os.name && r.os.name !== OS_NAME) matches = false; if (r.features) matches = false; // Demo-/Auflösungs-Features aus if (matches) action = r.action; } return action === 'allow'; } // --------------------------------------------------------------------------- // Version-JSON // --------------------------------------------------------------------------- async function getVersionJson(versionId, userDataDir, sharedDir) { const dest = path.join(sharedDir, 'versions', versionId, versionId + '.json'); if (fs.existsSync(dest)) { try { return JSON.parse(fs.readFileSync(dest, 'utf8')); } catch { /* neu laden */ } } // URL aus dem Manifest suchen (bei Bedarf frisch laden, falls url fehlt) let manifest = await services.getMcVersions(userDataDir); let entry = (manifest.versions || []).find((v) => v.id === versionId); if (!entry || !entry.url) { manifest = await services.getMcVersions(userDataDir, 0); // erzwinge Refresh entry = (manifest.versions || []).find((v) => v.id === versionId); } if (!entry || !entry.url) throw new Error('Version nicht im Mojang-Manifest gefunden: ' + versionId); const vj = await fetchJson(entry.url); ensureDir(path.dirname(dest)); fs.writeFileSync(dest, JSON.stringify(vj)); return vj; } // --------------------------------------------------------------------------- // Libraries + Natives -> Classpath // --------------------------------------------------------------------------- async function prepareLibraries(vj, sharedDir, nativesDir, onProgress) { const libDir = path.join(sharedDir, 'libraries'); const classpath = []; const libs = (vj.libraries || []).filter((l) => rulesAllow(l.rules)); let i = 0; for (const lib of libs) { i++; onProgress && onProgress({ phase: 'libraries', current: i, total: libs.length, detail: lib.name }); const dl = lib.downloads || {}; // normales Artefakt -> Classpath if (dl.artifact && dl.artifact.path) { const dest = path.join(libDir, dl.artifact.path.replace(/\//g, path.sep)); await downloadIfNeeded(dl.artifact.url, dest, dl.artifact.sha1); classpath.push(dest); } // klassische Natives (vor 1.19) -> in nativesDir extrahieren if (lib.natives && lib.natives[OS_NAME] && dl.classifiers) { const key = lib.natives[OS_NAME].replace('${arch}', '64'); const nat = dl.classifiers[key]; if (nat) { const dest = path.join(libDir, nat.path.replace(/\//g, path.sep)); await downloadIfNeeded(nat.url, dest, nat.sha1); await extractNatives(dest, nativesDir, lib.extract); } } } return classpath; } function classpathArtifactKey(filePath, sharedDir) { const libDir = path.join(sharedDir, 'libraries'); const rel = path.relative(libDir, filePath); if (rel.startsWith('..') || path.isAbsolute(rel)) return path.normalize(filePath).toLowerCase(); const parts = rel.split(path.sep); if (parts.length < 4) return rel.replace(/\\/g, '/').toLowerCase(); const fileName = parts[parts.length - 1]; const version = parts[parts.length - 2]; const artifact = parts[parts.length - 3]; const group = parts.slice(0, -3).join('/').toLowerCase(); const base = fileName.replace(/\.[^.]+$/, ''); const prefix = `${artifact}-${version}`; const classifier = base.startsWith(prefix + '-') ? base.slice(prefix.length + 1).toLowerCase() : ''; return `${group}:${artifact.toLowerCase()}:${classifier}`; } function dedupeClasspath(classpath, sharedDir) { const seen = new Set(); const out = []; for (const filePath of classpath) { const key = classpathArtifactKey(filePath, sharedDir); if (seen.has(key)) continue; seen.add(key); out.push(filePath); } return out; } // entpackt eine Natives-JAR (ZIP) nach nativesDir (ohne externe Abhängigkeit) async function extractNatives(jarPath, nativesDir, extractRule) { ensureDir(nativesDir); const exclude = (extractRule && extractRule.exclude) || ['META-INF/']; const data = fs.readFileSync(jarPath); // ZIP End-of-Central-Directory finden let eocd = -1; for (let i = data.length - 22; i >= 0 && i > data.length - 22 - 65536; i--) { if (data.readUInt32LE(i) === 0x06054b50) { eocd = i; break; } } if (eocd < 0) return; const count = data.readUInt16LE(eocd + 10); let off = data.readUInt32LE(eocd + 16); const zlib = require('zlib'); for (let n = 0; n < count; n++) { if (data.readUInt32LE(off) !== 0x02014b50) break; const method = data.readUInt16LE(off + 10); const compSize = data.readUInt32LE(off + 20); const nameLen = data.readUInt16LE(off + 28); const extraLen = data.readUInt16LE(off + 30); const commentLen = data.readUInt16LE(off + 32); const localOff = data.readUInt32LE(off + 42); const name = data.toString('utf8', off + 46, off + 46 + nameLen); off += 46 + nameLen + extraLen + commentLen; if (name.endsWith('/')) continue; if (exclude.some((ex) => name.startsWith(ex))) continue; // lokalen Header lesen, um Datenoffset zu bestimmen const lNameLen = data.readUInt16LE(localOff + 26); const lExtraLen = data.readUInt16LE(localOff + 28); const dataStart = localOff + 30 + lNameLen + lExtraLen; const raw = data.subarray(dataStart, dataStart + compSize); let out; try { out = method === 0 ? raw : zlib.inflateRawSync(raw); } catch { continue; } const target = path.join(nativesDir, path.basename(name)); fs.writeFileSync(target, out); } } // --------------------------------------------------------------------------- // Assets // --------------------------------------------------------------------------- async function prepareAssets(vj, sharedDir, onProgress) { const ai = vj.assetIndex; if (!ai) return; const indexFile = path.join(sharedDir, 'assets', 'indexes', ai.id + '.json'); // Merker: einmal vollständig geprüfte Assets nicht erneut durchgehen (spart ~1,5 s pro Start) const doneMark = path.join(sharedDir, 'assets', 'indexes', ai.id + '.complete'); if (fs.existsSync(doneMark) && fs.existsSync(indexFile)) return; await downloadIfNeeded(ai.url, indexFile, ai.sha1); const index = JSON.parse(fs.readFileSync(indexFile, 'utf8')); const objects = index.objects || {}; const entries = Object.values(objects); const objDir = path.join(sharedDir, 'assets', 'objects'); await runPool(entries, async (o) => { const sub = o.hash.slice(0, 2); const dest = path.join(objDir, sub, o.hash); if (fs.existsSync(dest)) return; await downloadIfNeeded(`${RESOURCES}/${sub}/${o.hash}`, dest, o.hash); }, 16, (done, total) => onProgress && onProgress({ phase: 'assets', current: done, total, detail: 'Assets' })); try { fs.writeFileSync(doneMark, new Date().toISOString()); } catch { /* egal */ } } // --------------------------------------------------------------------------- // Java-Auswahl // --------------------------------------------------------------------------- function pickJava(vj, preferredPath) { const required = (vj.javaVersion && vj.javaVersion.majorVersion) || 8; let preferred = null; if (preferredPath && preferredPath.trim()) { const info = services.probeJava(preferredPath); if (info) { preferred = { path: preferredPath, major: info.major, forced: true }; if (info.major === required) return preferred; } } const jvms = services.scanJava(); // Mod-Loader und Mods unterstützen neuere Java-Versionen nicht automatisch. // Deshalb ausschließlich die von Mojang angegebene Major-Version verwenden. const x64 = jvms.filter((j) => j.arch === 'x64'); const exact = x64.find((j) => j.major === required); if (exact) return { path: exact.path, major: exact.major }; const fallback = preferred || x64[0] || jvms[0]; return fallback ? { path: fallback.path, major: fallback.major, forced: !!fallback.forced, mismatch: required, } : null; } // --------------------------------------------------------------------------- // Argumente // --------------------------------------------------------------------------- function substitute(str, map) { return str.replace(/\$\{([^}]+)\}/g, (m, k) => (k in map ? map[k] : m)); } function collectArgs(argList, map) { const out = []; for (const a of argList || []) { if (typeof a === 'string') { out.push(substitute(a, map)); continue; } if (a && a.rules && !rulesAllow(a.rules)) continue; const val = a && a.value; if (typeof val === 'string') out.push(substitute(val, map)); else if (Array.isArray(val)) val.forEach((v) => out.push(substitute(v, map))); } return out; } // führt Vanilla-Version + Loader-Profil zusammen (mainClass + Argumente) function mergeLoader(vj, lp) { const merged = Object.assign({}, vj, { mainClass: lp.mainClass || vj.mainClass }); merged.arguments = { jvm: [...((vj.arguments && vj.arguments.jvm) || []), ...((lp.arguments && lp.arguments.jvm) || [])], game: [...((vj.arguments && vj.arguments.game) || []), ...((lp.arguments && lp.arguments.game) || [])], }; return merged; } function buildArgs(vj, ctx) { const map = { auth_player_name: ctx.auth.name, version_name: vj.id, library_directory: path.join(ctx.sharedDir, 'libraries'), classpath_separator: path.delimiter, game_directory: ctx.gameDir, assets_root: path.join(ctx.sharedDir, 'assets'), assets_index_name: vj.assets || (vj.assetIndex && vj.assetIndex.id) || 'legacy', auth_uuid: ctx.auth.uuid, auth_access_token: ctx.auth.accessToken, clientid: ctx.auth.clientId || '', auth_xuid: ctx.auth.xuid || '', user_type: ctx.auth.userType || 'msa', version_type: vj.type || 'release', natives_directory: ctx.nativesDir, launcher_name: 'AeroMC', launcher_version: '0.2', classpath: ctx.classpath.join(path.delimiter), game_assets: path.join(ctx.sharedDir, 'assets', 'virtual', 'legacy'), auth_session: 'token:' + ctx.auth.accessToken, user_properties: '{}', }; let jvm, game; if (vj.arguments) { jvm = collectArgs(vj.arguments.jvm, map); game = collectArgs(vj.arguments.game, map); } else { // altes Format (< 1.13) jvm = ['-Djava.library.path=' + ctx.nativesDir, '-cp', ctx.classpath.join(path.delimiter)]; game = (vj.minecraftArguments || '').split(' ').filter(Boolean).map((a) => substitute(a, map)); } // RAM jvm = ['-Xms' + ctx.minMemMb + 'M', '-Xmx' + ctx.maxMemMb + 'M', ...jvm]; if (ctx.extraArgs) jvm = [...ctx.extraArgs.split(' ').filter(Boolean), ...jvm]; return [...jvm, vj.mainClass, ...game]; } // --------------------------------------------------------------------------- // Hauptfunktion // --------------------------------------------------------------------------- /* * opts: { versionId, gameDir, sharedDir, userDataDir, javaPath, minMemMb, maxMemMb, extraArgs, auth } * auth: null (kein Login) ODER { name, uuid, accessToken, userType } * onProgress: (evt) => void */ async function prepareAndLaunch(opts, onProgress) { const emit = (phase, extra) => onProgress && onProgress(Object.assign({ phase }, extra)); emit('version', { detail: 'Version-Daten laden …' }); const vj = await getVersionJson(opts.versionId, opts.userDataDir, opts.sharedDir); const nativesDir = path.join(opts.sharedDir, 'versions', vj.id, 'natives'); ensureDir(nativesDir); // Client-JAR emit('client', { detail: 'Client herunterladen …' }); const clientJar = path.join(opts.sharedDir, 'versions', vj.id, vj.id + '.jar'); if (vj.downloads && vj.downloads.client) { await downloadIfNeeded(vj.downloads.client.url, clientJar, vj.downloads.client.sha1); } // Libraries + Natives const classpath = await prepareLibraries(vj, opts.sharedDir, nativesDir, onProgress); classpath.push(clientJar); // Assets await prepareAssets(vj, opts.sharedDir, onProgress); // Java (VOR dem Loader – Forge-Prozessoren brauchen Java) emit('java', { detail: 'Java wählen …' }); let java = pickJava(vj, opts.javaPath); // Nichts Passendes da? Dann darf der Launcher die richtige Fassung selbst // holen – opts.ensureJava wird vom Hauptprozess gestellt. if ((!java || java.mismatch) && opts.ensureJava) { const benoetigt = (vj.javaVersion && vj.javaVersion.majorVersion) || 8; emit('java', { detail: `Java ${benoetigt} wird geladen …` }); try { const geholt = await opts.ensureJava(benoetigt, (p) => emit('java', p)); if (geholt && geholt.path) java = { path: geholt.path, major: geholt.major }; } catch (err) { emit('log', { line: '[Java] Automatischer Download fehlgeschlagen: ' + err.message + '\n' }); } } if (!java) return { ok: false, message: 'Keine Java-Installation gefunden. Bitte in den Einstellungen Java festlegen.' }; if (java.mismatch) { return { ok: false, requiredJava: java.mismatch, message: `Minecraft ${vj.id} benötigt Java ${java.mismatch}, gefunden wurde nur Java ${java.major || 'unbekannt'}. ` + 'Bitte Auto-Java aktivieren oder eine passende Java-Installation auswählen.', }; } // Mod-Loader let effectiveVj = vj; const libDir = path.join(opts.sharedDir, 'libraries'); if (opts.loader === 'fabric' || opts.loader === 'quilt') { emit('loader', { detail: opts.loader + ' vorbereiten …' }); let lp; // Profil zwischenspeichern: spart bei festgelegter Loader-Version die Netzabfrage const cacheFile = opts.loaderVersion ? path.join(opts.sharedDir, 'loaders', `${opts.loader}-${vj.id}-${opts.loaderVersion}.json`) : null; if (cacheFile && fs.existsSync(cacheFile)) { try { lp = JSON.parse(fs.readFileSync(cacheFile, 'utf8')); } catch { lp = null; } } if (!lp) { try { const r = await loaders.resolveLoaderProfile(opts.loader, vj.id, opts.loaderVersion); lp = r.profile; const target = cacheFile || path.join(opts.sharedDir, 'loaders', `${opts.loader}-${vj.id}-${r.loaderVersion}.json`); try { ensureDir(path.dirname(target)); fs.writeFileSync(target, JSON.stringify(lp)); } catch { /* egal */ } } catch (err) { return { ok: false, message: err.message }; } } const loaderLibs = lp.libraries || []; let li = 0; for (const lib of loaderLibs) { li++; onProgress && onProgress({ phase: 'loader', current: li, total: loaderLibs.length, detail: lib.name }); const { relPath, url } = loaders.mavenToUrl(lib.url, lib.name); const dest = path.join(libDir, relPath.replace(/\//g, path.sep)); await downloadIfNeeded(url, dest, lib.sha1 || null); classpath.unshift(dest); } effectiveVj = mergeLoader(vj, lp); } else if (opts.loader === 'forge' || opts.loader === 'neoforge') { let lv = opts.loaderVersion; if (!lv) { const list = await loaders.listLoaderVersions(opts.loader, vj.id); if (!list.length) return { ok: false, message: `Keine ${opts.loader}-Version für ${vj.id} gefunden.` }; lv = list[0].version; } let fvj; try { fvj = await forge.installForge({ loader: opts.loader, mcVersion: vj.id, loaderVersion: lv, sharedDir: opts.sharedDir, javaPath: java.path, clientJar, clientMappingsUrl: vj.downloads && vj.downloads.client_mappings && vj.downloads.client_mappings.url, onProgress, }); } catch (err) { return { ok: false, message: (opts.loader === 'forge' ? 'Forge' : 'NeoForge') + '-Installation fehlgeschlagen: ' + err.message }; } for (const lib of fvj.libraries || []) { const art = lib.downloads && lib.downloads.artifact; const rel = (art && art.path) || forge.mavenPath(lib.name); classpath.unshift(path.join(libDir, rel.replace(/\//g, path.sep))); } effectiveVj = mergeLoader(vj, fvj); } else if (opts.loader === 'optifine') { /* * OptiFine hat keine öffentliche Installations-API (Downloads laufen bewusst * über eine Werbeseite). AeroMC lädt hier nichts selbst herunter – der Nutzer * hat den offiziellen Installer bereits einmal sichtbar laufen lassen (siehe * optifine:setup/optifine:confirm in main.js), der dabei ein normales * Versions-Profil unter /versions// abgelegt hat, * genau wie Fabric/Forge es tun. Wir lesen es nur noch ein und mergen es. */ const ofId = opts.loaderVersion; if (!ofId) { return { ok: false, message: 'Für diese Instanz ist keine OptiFine-Version hinterlegt. Bitte im Bearbeiten-Dialog einrichten.' }; } const ofJsonPath = path.join(opts.sharedDir, 'versions', ofId, ofId + '.json'); if (!fs.existsSync(ofJsonPath)) { return { ok: false, message: `OptiFine-Profil "${ofId}" wurde nicht gefunden. Bitte die Einrichtung im Bearbeiten-Dialog wiederholen.` }; } let ofvj; try { ofvj = JSON.parse(fs.readFileSync(ofJsonPath, 'utf8')); } catch (err) { return { ok: false, message: 'OptiFine-Profil ist beschädigt: ' + err.message }; } for (const lib of ofvj.libraries || []) { const art = lib.downloads && lib.downloads.artifact; const rel = (art && art.path) || forge.mavenPath(lib.name); const dest = path.join(libDir, rel.replace(/\//g, path.sep)); if (!fs.existsSync(dest) && art && art.url) { await downloadIfNeeded(art.url, dest, art.sha1 || null); } classpath.unshift(dest); } effectiveVj = mergeLoader(vj, ofvj); } const finalClasspath = dedupeClasspath(classpath, opts.sharedDir); // Ohne Login: alles vorbereitet, aber kein Start (kein Crack-Bypass) if (!opts.auth) { return { ok: false, needAuth: true, prepared: true, java, requiredJava: (vj.javaVersion && vj.javaVersion.majorVersion) || 8, message: 'Alles heruntergeladen und startklar ✔ – bitte oben rechts über „👤 Anmelden" mit deinem Microsoft-Konto einloggen, dann startet Minecraft.', }; } // Start const args = buildArgs(effectiveVj, { auth: opts.auth, gameDir: opts.gameDir, sharedDir: opts.sharedDir, nativesDir, classpath: finalClasspath, minMemMb: opts.minMemMb || 2048, maxMemMb: opts.maxMemMb || 4096, extraArgs: opts.extraArgs || '', }); // Fenstergröße bzw. maximiert starten (Minecraft-Argumente) if (opts.mcMaximized) args.push('--fullscreen'); else if (opts.mcWidth && opts.mcHeight) args.push('--width', String(opts.mcWidth), '--height', String(opts.mcHeight)); // Ab 1.20: direkt einem Server beitreten (Quick Play) if (opts.quickPlayServer) { args.push('--quickPlayMultiplayer', String(opts.quickPlayServer)); } ensureDir(opts.gameDir); // Pre-Launch-Befehl (bricht bei Fehler ab) if (opts.preLaunchCommand && opts.preLaunchCommand.trim()) { emit('log', { line: '[Pre-Launch] ' + opts.preLaunchCommand + '\n' }); const pr = spawnSync(opts.preLaunchCommand, { cwd: opts.gameDir, shell: true, encoding: 'utf8' }); if (pr.stdout) emit('log', { line: pr.stdout }); if (pr.stderr) emit('log', { line: pr.stderr }); if (pr.status !== 0) return { ok: false, message: 'Pre-Launch-Befehl fehlgeschlagen (Exit ' + pr.status + ').' }; } emit('launch', { detail: 'Starte Minecraft …' }); let cmd = java.path; let spawnArgs = args; if (opts.wrapperCommand && opts.wrapperCommand.trim()) { const w = opts.wrapperCommand.trim().split(/\s+/); cmd = w[0]; spawnArgs = [...w.slice(1), java.path, ...args]; } const child = spawn(cmd, spawnArgs, { cwd: opts.gameDir, detached: false }); const shutdownWatchdog = opts.wrapperCommand && opts.wrapperCommand.trim() ? null : processwatchdog.createProcessWatchdog({ isRunning: () => child.exitCode === null && child.signalCode === null, kill: () => child.kill(), onForce: (graceMs) => emit('log', { line: `[Launcher] Regulärer Client-Shutdown war nach ${graceMs / 1000} Sekunden noch nicht beendet; ` + 'verbliebenen Java-Prozess beendet.\n', }), onError: () => emit('log', { line: '[Launcher] Der nach dem regulären Client-Shutdown verbliebene Java-Prozess konnte nicht beendet werden.\n', }), }); child.stdout.on('data', (d) => { emit('log', { line: d.toString() }); if (shutdownWatchdog) shutdownWatchdog.feed(d, 'stdout'); }); child.stderr.on('data', (d) => { emit('log', { line: d.toString() }); if (shutdownWatchdog) shutdownWatchdog.feed(d, 'stderr'); }); // Ende genau einmal melden – 'exit' und 'error' können beide kommen let beendetGemeldet = false; const meldeEnde = (code, cleanShutdown) => { if (beendetGemeldet) return; beendetGemeldet = true; emit('exit', { code, cleanShutdown: !!cleanShutdown }); // Post-Exit-Befehl if (opts.postExitCommand && opts.postExitCommand.trim()) { emit('log', { line: '[Post-Exit] ' + opts.postExitCommand + '\n' }); try { const po = spawnSync(opts.postExitCommand, { cwd: opts.gameDir, shell: true, encoding: 'utf8' }); if (po.stdout) emit('log', { line: po.stdout }); if (po.stderr) emit('log', { line: po.stderr }); } catch { /* egal */ } } }; child.on('exit', (code) => { // vor end() lesen: sawCleanShutdown wird zwar nicht von end()/clear() // zurückgesetzt, aber so bleibt die Reihenfolge unabhängig von internen Details const cleanShutdown = !!(shutdownWatchdog && shutdownWatchdog.sawCleanShutdown); if (shutdownWatchdog) shutdownWatchdog.end(); meldeEnde(shutdownWatchdog && shutdownWatchdog.forced ? 0 : code, cleanShutdown); }); // ohne diesen Listener würde ein Fehlstart (z. B. falscher Java-Pfad) als // unbehandeltes 'error'-Ereignis den Hauptprozess beenden child.on('error', (err) => { if (shutdownWatchdog) shutdownWatchdog.end(); emit('log', { line: '[Fehler] Minecraft konnte nicht gestartet werden: ' + err.message + '\n' }); meldeEnde(-1, false); }); return { ok: true, pid: child.pid, java }; } module.exports = { prepareAndLaunch, getVersionJson, buildArgs, pickJava, rulesAllow, prepareLibraries, prepareAssets, downloadIfNeeded, extractNatives, mergeLoader, dedupeClasspath, classpathArtifactKey, };