Upload via GUI (39 Dateien)
This commit is contained in:
+199
-4
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user