Upload via GUI (39 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-02 17:01:18 +02:00
parent a803f297ea
commit eff6403dd5
26 changed files with 3536 additions and 134 deletions
+199 -4
View File
@@ -19,10 +19,50 @@ const { execFileSync, spawnSync } = require('child_process');
const UA = 'AeroMC-Launcher/0.2 (Minecraft-Launcher; contact: viper)';
// ---------------------------------------------------------------------------
// Arbeitsspeicher
// ---------------------------------------------------------------------------
/*
* Prüft die Speicherangaben einer Instanz gegen den vorhandenen Arbeitsspeicher.
* Ohne diese Prüfung startet Java mit einem kryptischen Fehler, den niemand
* mit "zu viel RAM eingetragen" in Verbindung bringt.
*
* -> { ok, message } bei Fehlern, { ok: true, warnung } bei knappen Angaben
*/
function pruefeSpeicher(minMb, maxMb, gesamtMb) {
const min = Number(minMb) || 0;
const max = Number(maxMb) || 0;
const gesamt = Number(gesamtMb) || 0;
if (min && max && min > max) {
return { ok: false, message: `Der Mindestspeicher (${min} MB) ist größer als das Maximum (${max} MB).` };
}
if (max && max < 512) {
return { ok: false, message: `${max} MB sind zu wenig Minecraft braucht mindestens 512 MB.` };
}
if (gesamt && max && max > gesamt) {
return { ok: false, message: `${max} MB übersteigen den vorhandenen Arbeitsspeicher (${gesamt} MB). Minecraft würde nicht starten.` };
}
// Dem System muss etwas bleiben: Windows und der Launcher selbst brauchen auch Platz
if (gesamt && max && max > gesamt * 0.8) {
return {
ok: true,
warnung: `${max} MB sind mehr als 80 % des Arbeitsspeichers (${gesamt} MB). `
+ `Für Windows und den Launcher sollte etwas übrig bleiben üblich sind höchstens ${Math.floor(gesamt * 0.7 / 256) * 256} MB.`,
};
}
return { ok: true };
}
// ---------------------------------------------------------------------------
// Java-Erkennung
// ---------------------------------------------------------------------------
// vom Launcher selbst heruntergeladene Laufzeiten (wird beim Start gesetzt)
let managedJavaDir = null;
function setManagedJavaDir(dir) { managedJavaDir = dir; }
function javaCandidateDirs() {
const pf = process.env['ProgramFiles'] || 'C:\\Program Files';
const pfx = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)';
@@ -37,6 +77,8 @@ function javaCandidateDirs() {
// Runtimes der offiziellen Minecraft-Launcher
if (appdata) roots.push(path.join(appdata, '.minecraft', 'runtime'));
if (localappdata) roots.push(path.join(localappdata, 'Packages'));
// eigene, vom Launcher geholte Laufzeiten
if (managedJavaDir) roots.push(managedJavaDir);
return roots;
}
@@ -238,8 +280,9 @@ async function modrinthResolveFile({ projectId, gameVersion, loader }) {
};
}
async function downloadFile(url, destPath) {
const res = await fetch(url, { headers: { 'User-Agent': UA } });
// extraHeaders: z. B. der CurseForge-API-Key, den das CDN seit Juli 2026 verlangt
async function downloadFile(url, destPath, extraHeaders) {
const res = await fetch(url, { headers: Object.assign({ 'User-Agent': UA }, extraHeaders || {}) });
if (!res.ok) throw new Error('Download fehlgeschlagen: HTTP ' + res.status);
const buf = Buffer.from(await res.arrayBuffer());
fs.mkdirSync(path.dirname(destPath), { recursive: true });
@@ -273,6 +316,156 @@ async function modrinthInstall({ projectId, title, gameVersion, loader, modsDir
};
}
/*
* Prüft die installierten Mods auf neuere Dateien.
* Von Hand abgelegte .jar-Dateien (ohne projectId) bleiben unangetastet
* für die weiß niemand, woher sie stammen.
* -> [{ projectId, title, alt: {...}, neu: {...} }]
*/
async function modrinthFindUpdates(mods, gameVersion, loader, onProgress) {
const kandidaten = (mods || []).filter((m) => m && m.projectId);
const treffer = [];
let geprueft = 0;
// in kleinen Gruppen abfragen: schneller als nacheinander, ohne die API zu fluten
const GRUPPE = 4;
for (let i = 0; i < kandidaten.length; i += GRUPPE) {
await Promise.all(kandidaten.slice(i, i + GRUPPE).map(async (m) => {
let file = null;
try {
file = await modrinthResolveFile({ projectId: m.projectId, gameVersion, loader });
} catch { /* Netzfehler -> diese Mod überspringen */ }
geprueft++;
if (onProgress) onProgress({ geprueft, gesamt: kandidaten.length, titel: m.title || m.projectId });
if (!file || !file.versionId) return;
// schon aktuell?
if (m.versionId && file.versionId === m.versionId) return;
if (file.filename === m.filename) return;
treffer.push({
projectId: m.projectId,
title: m.title || m.projectId,
alt: { filename: m.filename, versionNumber: m.versionNumber || '?' },
neu: { filename: file.filename, versionNumber: file.versionNumber, versionId: file.versionId, size: file.size },
});
}));
}
treffer.sort((a, b) => String(a.title).localeCompare(String(b.title)));
return { geprueft: kandidaten.length, uebersprungen: (mods || []).length - kandidaten.length, treffer };
}
// ---------------------------------------------------------------------------
// CurseForge (Modpack-Dateien auflösen)
// ---------------------------------------------------------------------------
const CURSEFORGE = 'https://api.curseforge.com/v1';
/*
* Vorgaben, die beim Bauen gesetzt wurden (scripts/write-build-config.js).
* Fehlt die Datei etwa beim Start aus dem Quelltext , gilt: keine Vorgabe.
*/
let BUILD_VORGABEN = {};
try { BUILD_VORGABEN = require('./build-config.json'); } catch { /* nicht erzeugt */ }
/*
* Adresse der eigenen CurseForge-Weiterleitung. Die darf fest hier stehen
* sie ist kein Geheimnis, der API-Schlüssel liegt auf dem Server.
* Über AEROMC_CF_PROXY_URL beim Bauen überschreibbar.
*/
const STANDARD_WEITERLEITUNG = String(BUILD_VORGABEN.curseforgeProxyUrl || 'https://curse.viper.ipv64.net');
// Das Token dagegen kommt ausschließlich aus dem Build (AEROMC_CF_PROXY_TOKEN)
// und steht bewusst nicht im Quelltext.
const STANDARD_TOKEN = String(BUILD_VORGABEN.curseforgeProxyToken || '');
/*
* Entscheidet, wie CurseForge angesprochen wird.
* Eigener Schlüssel hat Vorrang: wer einen hat, geht direkt und belastet
* kein fremdes Kontingent.
* -> { modus, basis, kopfzeilen, downloadAdresse(url) } oder null
*/
function curseforgeZugang(settings) {
const s = settings || {};
const key = String(s.curseforgeApiKey || '').trim();
if (key) {
return {
modus: 'direkt',
basis: CURSEFORGE,
kopfzeilen: { 'x-api-key': key },
downloadAdresse: (url) => url,
downloadKopfzeilen: { 'x-api-key': key },
};
}
const eigeneAdresse = String(s.curseforgeProxyUrl || '').trim().replace(/\/+$/, '');
const weiter = eigeneAdresse || STANDARD_WEITERLEITUNG;
if (weiter) {
// Zur eigenen Adresse gehört auch das eigene Token sonst schickte der
// Launcher das Token unseres Servers an einen fremden.
const token = eigeneAdresse
? String(s.curseforgeProxyToken || '').trim()
: (String(s.curseforgeProxyToken || '').trim() || STANDARD_TOKEN);
const kopf = token ? { 'x-aeromc-token': token } : {};
return {
modus: 'weiterleitung',
basis: weiter + '/v1',
kopfzeilen: kopf,
downloadAdresse: (url) => `${weiter}/v1/download?url=${encodeURIComponent(url)}`,
downloadKopfzeilen: kopf,
};
}
return null;
}
/*
* Ausweichadresse auf dem CurseForge-CDN. Manche Autoren untersagen den
* Download über die API; dann liefert downloadUrl null, die Datei liegt aber
* unter diesem berechenbaren Pfad.
*/
function forgeCdnUrl(fileId, fileName) {
const s = String(fileId || '');
if (s.length < 5 || !fileName) return null;
return `https://edge.forgecdn.net/files/${s.slice(0, 4)}/${Number(s.slice(4))}/${encodeURIComponent(fileName)}`;
}
/*
* Löst die Dateien eines CurseForge-Modpacks auf.
* zugang: aus curseforgeZugang(); files: [{ projectID, fileID }]
* -> Map fileID -> { fileName, url, kopfzeilen }
*/
async function curseforgeResolveFiles(zugang, files, onProgress) {
if (!zugang) throw new Error('Kein Zugang zu CurseForge eingerichtet.');
const ids = (files || []).map((f) => Number(f.fileID)).filter(Boolean);
const map = new Map();
const PAKET = 100;
for (let i = 0; i < ids.length; i += PAKET) {
const teil = ids.slice(i, i + PAKET);
const res = await fetch(zugang.basis + '/mods/files', {
method: 'POST',
headers: Object.assign({ 'Content-Type': 'application/json', Accept: 'application/json', 'User-Agent': UA }, zugang.kopfzeilen),
body: JSON.stringify({ fileIds: teil }),
});
if (res.status === 401 || res.status === 403) {
throw new Error(zugang.modus === 'weiterleitung'
? 'Der CurseForge-Server weist die Anfrage zurück (HTTP ' + res.status + ').'
: 'CurseForge weist den API-Key zurück (HTTP ' + res.status + ').');
}
if (res.status === 429) throw new Error('Zu viele Anfragen bitte einen Moment warten.');
if (!res.ok) throw new Error('CurseForge-Abfrage fehlgeschlagen: HTTP ' + res.status);
const daten = await res.json();
for (const d of (daten && daten.data) || []) {
const roh = d.downloadUrl || forgeCdnUrl(d.id, d.fileName);
if (!d.fileName || !roh) continue;
map.set(Number(d.id), {
fileName: d.fileName,
url: zugang.downloadAdresse(roh),
kopfzeilen: zugang.downloadKopfzeilen,
});
}
if (onProgress) onProgress({ aufgeloest: Math.min(i + PAKET, ids.length), gesamt: ids.length });
}
return map;
}
// Projekttitel nachschlagen (für Abhängigkeiten, die nur eine ID liefern)
async function modrinthProjectTitle(projectId) {
try {
@@ -284,7 +477,9 @@ async function modrinthProjectTitle(projectId) {
}
module.exports = {
scanJava, probeJava, setCacheDir,
scanJava, probeJava, setCacheDir, setManagedJavaDir, pruefeSpeicher,
getMcVersions,
modrinthSearch, modrinthResolveFile, modrinthInstall, modrinthProjectTitle, downloadFile,
modrinthSearch, modrinthResolveFile, modrinthInstall, modrinthProjectTitle,
modrinthFindUpdates, downloadFile,
curseforgeResolveFiles, forgeCdnUrl, curseforgeZugang,
};