Upload via GUI (43 Dateien)

This commit is contained in:
Git Manager GUI
2026-08-05 09:04:48 +02:00
parent ec4992c5c7
commit bafad25059
12 changed files with 1433 additions and 79 deletions
+175
View File
@@ -0,0 +1,175 @@
'use strict';
/*
* technicdiscover.js – Technic-Modpacks direkt über die Technic-Plattform
* durchsuchen und installieren, ohne vorher manuell eine Datei herunterladen
* zu müssen. Genutzt wird dieselbe öffentliche API wie bei Prism Launcher:
*
* Suche: GET https://api.technicpack.net/search?build=multimc&q=<term>
* Beliebt: GET https://api.technicpack.net/trending?build=multimc
* Pack-Infos: GET https://api.technicpack.net/modpack/<slug>?build=multimc
* -> entweder "url" (ein fertiges ZIP) oder "solder" (Solder-Server)
* Solder: GET <solder>modpack/<slug> – Liste der Builds
* GET <solder>modpack/<slug>/<build> – Mods dieser Fassung
* { minecraft, forge, mods: [{ name, url, ... }] }
*
* "build=multimc" ist die Kennung, mit der sich auch andere Drittlauncher
* bei der Technic-Plattform melden – kein echter Minecraft-Build, sondern
* nur ein Client-Identifier, den die API erwartet.
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const forge = require('./forge');
const technicimport = require('./technicimport');
const API_BASE = 'https://api.technicpack.net/';
const API_BUILD = 'multimc';
const UA = 'AeroMC-Launcher/1.0';
async function getJson(url) {
const res = await fetch(url, { headers: { 'User-Agent': UA, Accept: 'application/json' } });
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
}
async function getBuffer(url) {
const res = await fetch(url, { headers: { 'User-Agent': UA } });
if (!res.ok) throw new Error('HTTP ' + res.status);
return Buffer.from(await res.arrayBuffer());
}
/*
* query leer -> beliebte Packs (trending), sonst Suchtreffer.
* -> [{ slug, name, iconUrl }]
*/
async function search(query) {
const q = String(query || '').trim();
const url = q
? `${API_BASE}search?build=${API_BUILD}&q=${encodeURIComponent(q)}`
: `${API_BASE}trending?build=${API_BUILD}`;
const data = await getJson(url);
const packs = Array.isArray(data.modpacks) ? data.modpacks : [];
return packs
.filter((p) => p && p.slug && p.slug !== 'vanilla')
.map((p) => ({ slug: p.slug, name: p.name || p.slug, iconUrl: p.iconUrl || '' }));
}
/*
* Entpackt einen ZIP-Buffer nach destDir.
* Enthält der ZIP-Inhalt bereits eine erkennbare Ordnerstruktur (mods/,
* config/, …), wird sie 1:1 übernommen. Liegen nur einzelne Dateien direkt
* im ZIP-Wurzelverzeichnis (typisch für ältere, einzeln gepackte Solder-
* Mods), landen sie stattdessen im mods-Ordner.
*/
function extractZipBuffer(buf, destDir) {
const idx = forge.readZipIndex(buf);
const namen = Object.keys(idx);
const hatOrdnerStruktur = namen.some((n) => /^(mods|config|resources|resourcepacks|scripts|structures|bin)\//i.test(n));
for (const name of namen) {
if (name.endsWith('/')) continue; // reines Verzeichnis – wird beim Schreiben automatisch angelegt
const relPfad = hatOrdnerStruktur ? name : path.posix.join('mods', name);
const zielPfad = path.join(destDir, ...relPfad.split('/'));
if (!zielPfad.startsWith(destDir + path.sep)) continue; // Pfad-Ausbruch verhindern
fs.mkdirSync(path.dirname(zielPfad), { recursive: true });
fs.writeFileSync(zielPfad, forge.readZipEntry(buf, idx[name]));
}
}
// Solder-Basis-URLs enden mal mit "/api/", mal ohne abschließenden Slash
function solderUrl(basis, pfad) {
const b = String(basis || '').replace(/\/+$/, '');
return `${b}/${pfad}`;
}
async function installSolderBuild(solderBasis, slug, gameDir, onProgress) {
const info = await getJson(solderUrl(solderBasis, 'modpack/' + encodeURIComponent(slug)));
const build = info.recommended || info.latest || (info.builds || [])[0];
if (!build) throw new Error('Keine Fassung (Build) für dieses Modpack gefunden.');
const daten = await getJson(solderUrl(solderBasis, `modpack/${encodeURIComponent(slug)}/${encodeURIComponent(build)}`));
const mods = Array.isArray(daten.mods) ? daten.mods : [];
for (let i = 0; i < mods.length; i++) {
const mod = mods[i];
if (onProgress) onProgress({ current: i + 1, total: mods.length, name: mod.name || '' });
if (!mod || !mod.url) continue;
try {
const buf = await getBuffer(mod.url);
extractZipBuffer(buf, gameDir);
} catch { /* einzelner Mod fehlgeschlagen – Rest trotzdem weiter versuchen */ }
}
let loader = 'vanilla';
const loaderVersion = daten.forge || '';
if (loaderVersion) loader = 'forge';
return { version: daten.minecraft || '', loader, loaderVersion };
}
async function installSingleZip(url, gameDir) {
const buf = await getBuffer(url);
// in einen Temp-Ordner entpacken, damit wir wie beim Ordner-Import
// (technicimport.js) Version/Loader aus bin/modpack.jar erkennen können,
// bevor die Dateien an ihren endgültigen Platz kommen
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'technic-dl-'));
try {
extractZipBuffer(buf, tmp);
let base = tmp;
if (!fs.existsSync(path.join(base, 'bin')) && !fs.existsSync(path.join(base, 'mods'))) {
// manche Packs haben einen umschließenden Ordner im ZIP
const sub = fs.readdirSync(tmp).map((n) => path.join(tmp, n))
.find((p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } });
if (sub) base = sub;
}
const { version, loader, loaderVersion } = technicimport.detectVersion(base);
fs.cpSync(base, gameDir, { recursive: true });
return { version, loader, loaderVersion };
} finally {
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* egal */ }
}
}
/*
* Lädt ein Modpack von der Technic-Plattform und legt daraus eine neue
* Instanz an. onProgress(info) wird bei Solder-Packs pro Mod aufgerufen.
* -> { ok, instance } oder { ok: false, message }
*/
async function installFromPlatform(store, slug, onProgress) {
let created = null;
try {
const detail = await getJson(`${API_BASE}modpack/${encodeURIComponent(slug)}?build=${API_BUILD}`);
// die Technic-API antwortet bei unbekanntem Slug mit HTTP 200 und
// {"error": "..."} statt einem Fehlerstatus – daher explizit prüfen
if (detail.error) throw new Error(detail.error);
created = store.createOrReplaceImportedInstance({
name: detail.displayName || detail.name || slug,
notes: detail.description || '',
});
const gameDir = store.gameDir(created.id);
fs.mkdirSync(gameDir, { recursive: true });
let ergebnis;
if (detail.solder) {
ergebnis = await installSolderBuild(detail.solder, slug, gameDir, onProgress);
} else if (detail.url) {
ergebnis = await installSingleZip(detail.url, gameDir);
} else {
throw new Error('Für dieses Modpack ist kein Download hinterlegt.');
}
store.updateInstance(created.id, {
minecraft: {
version: ergebnis.version || detail.minecraft || '',
loader: ergebnis.loader || 'vanilla',
loaderVersion: ergebnis.loaderVersion || '',
},
});
return { ok: true, instance: store.getInstance(created.id) };
} catch (err) {
// fehlgeschlagene Installation nicht als leere Karteileiche stehen lassen
if (created) { try { store.deleteInstance(created.id); } catch { /* egal */ } }
return { ok: false, message: 'Installation fehlgeschlagen: ' + err.message };
}
}
module.exports = { search, installFromPlatform };