'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= * Beliebt: GET https://api.technicpack.net/trending?build=multimc * Pack-Infos: GET https://api.technicpack.net/modpack/?build=multimc * -> entweder "url" (ein fertiges ZIP) oder "solder" (Solder-Server) * Solder: GET modpack/ – Liste der Builds * GET modpack// – 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 };