Upload via GUI (19 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-01 07:03:08 +02:00
parent fb6da6562e
commit ab502f44ef
19 changed files with 10983 additions and 21 deletions
+455
View File
@@ -0,0 +1,455 @@
'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
* <sharedDir>, 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 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;
}
// 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');
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' }));
}
// ---------------------------------------------------------------------------
// Java-Auswahl
// ---------------------------------------------------------------------------
function pickJava(vj, preferredPath) {
if (preferredPath && preferredPath.trim()) {
const info = services.probeJava(preferredPath);
if (info) return { path: preferredPath, major: info.major, forced: true };
}
const required = (vj.javaVersion && vj.javaVersion.majorVersion) || 8;
const jvms = services.scanJava();
// exakt passende oder nächsthöhere 64-bit-Version
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 higher = x64.filter((j) => j.major >= required).sort((a, b) => a.major - b.major)[0];
if (higher) return { path: higher.path, major: higher.major };
const any = jvms[0];
return any ? { path: any.path, major: any.major, 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 …' });
const java = pickJava(vj, opts.javaPath);
if (!java) return { ok: false, message: 'Keine Java-Installation gefunden. Bitte in den Einstellungen Java festlegen.' };
// 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;
try { lp = (await loaders.resolveLoaderProfile(opts.loader, vj.id, opts.loaderVersion)).profile; }
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);
}
// 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,
minMemMb: opts.minMemMb || 2048,
maxMemMb: opts.maxMemMb || 4096,
extraArgs: opts.extraArgs || '',
});
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 });
child.stdout.on('data', (d) => emit('log', { line: d.toString() }));
child.stderr.on('data', (d) => emit('log', { line: d.toString() }));
child.on('exit', (code) => {
emit('exit', { code });
// 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 */ }
}
});
return { ok: true, pid: child.pid, java };
}
module.exports = {
prepareAndLaunch, getVersionJson, buildArgs, pickJava, rulesAllow, prepareLibraries, prepareAssets,
downloadIfNeeded, extractNatives,
};