'use strict'; /* * technicimport.js – Instanzen aus dem Technic Launcher übernehmen * ------------------------------------------------------------------- * Der Technic Launcher legt jedes installierte Modpack als eigenen Ordner * unter "/modpacks/" an (Standard unter Windows: * %APPDATA%\.technic\modpacks). Darin liegen "bin" (Launcher-interner * Jar-/Versions-Cache), "config" und "mods" als Geschwisterordner – Welten, * Ressourcenpakete usw. landen ebenfalls direkt im Pack-Ordner, genau wie * bei einem klassischen .minecraft. * * Es gibt keine für uns nutzbare, stabil dokumentierte JSON-Datei mit einem * schönen Anzeigenamen – "installedPacks" im Wurzelordner listet Packs nur * über ihren Ordnernamen (Slug). Wir lesen sie deshalb nur als optionale, * fehlertolerante Ergänzung; die eigentliche Erkennung läuft – wie bei den * anderen Importern – über die tatsächlich vorhandenen Ordner/Dateien. */ const fs = require('fs'); const path = require('path'); const forge = require('./forge'); // Ordner, die zum Technic-internen Verwaltungs-/Jar-Cache gehören und beim // Übernehmen NICHT mit ins Spielverzeichnis der neuen Instanz sollen. const SKIP_DIRS = new Set(['bin']); function technicRoot() { return path.join(process.env.APPDATA || '', '.technic'); } function readJson(file) { try { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^/, '')); } catch { return null; } } // findet den modpacks-Ordner, egal ob Technic-Wurzel, modpacks-Ordner direkt // oder ein einzelner Pack-Ordner angegeben wurde function resolveModpacksDir(inputPath) { const root = inputPath && inputPath.trim() ? inputPath.trim() : technicRoot(); if (!fs.existsSync(root)) return null; const direct = path.join(root, 'modpacks'); if (fs.existsSync(direct)) return direct; if (path.basename(root).toLowerCase() === 'modpacks') return root; return root; // z. B. ein selbst gewählter Ordner mit Pack-Unterordnern } // ein Pack-Ordner hat i. d. R. bin/, config/ und/oder mods/ als Geschwister function looksLikePack(dir) { const markers = ['bin', 'mods', 'config', 'saves', 'resourcepacks', 'options.txt']; return markers.some((name) => fs.existsSync(path.join(dir, name))); } // optionale Anreicherung aus der "installedPacks"-Datei der Technic-Wurzel; // bewusst rein additiv und fehlertolerant, da das Format nicht offiziell // als stabile API dokumentiert ist function readInstalledPacksMeta(root) { const data = readJson(path.join(root, 'installedPacks')); const packs = data && data.installedPacks; return packs && typeof packs === 'object' ? packs : {}; } // versucht, aus bin/modpack.jar (version.json) oder der alten bin/version // -Textdatei Minecraft-Version und Mod-Loader zu bestimmen. Rein // bestmöglich – schlägt die Erkennung fehl, bleibt die Instanz einfach // ohne Version/Loader stehen und lässt sich manuell nachtragen. function detectVersion(packDir) { const bin = path.join(packDir, 'bin'); const jarFile = path.join(bin, 'modpack.jar'); if (fs.existsSync(jarFile)) { try { const buf = fs.readFileSync(jarFile); const idx = forge.readZipIndex(buf); const entry = idx['version.json']; if (entry) { // manche Jars speichern JSON mit BOM – ohne Entfernen bricht JSON.parse const text = forge.readZipEntry(buf, entry).toString('utf8').replace(/^/, ''); const profile = JSON.parse(text); const libs = Array.isArray(profile.libraries) ? profile.libraries : []; let loader = 'vanilla'; let loaderVersion = ''; for (const lib of libs) { const name = String((lib && lib.name) || ''); if (/net\.minecraftforge:forge:/i.test(name)) { loader = 'forge'; loaderVersion = (name.split(':')[2] || '').split('-').pop() || ''; } else if (/net\.neoforged:neoforge:/i.test(name) && loader === 'vanilla') { loader = 'neoforge'; loaderVersion = name.split(':')[2] || ''; } } const id = String(profile.id || profile.inheritsFrom || ''); const version = String(profile.inheritsFrom || id.split('-')[0] || '').trim(); if (version) return { version, loader, loaderVersion }; } } catch { /* modpack.jar nicht lesbar/kein gültiges ZIP – weiter unten Fallback */ } } // ältere Packs hinterlegen nur eine reine Textdatei mit der MC-Version const legacy = path.join(bin, 'version'); try { if (fs.existsSync(legacy)) { const version = fs.readFileSync(legacy, 'utf8').trim(); if (version) return { version, loader: 'vanilla', loaderVersion: '' }; } } catch { /* egal, bleibt leer */ } return { version: '', loader: 'vanilla', loaderVersion: '' }; } function scan(inputPath) { const modpacksDir = resolveModpacksDir(inputPath); if (!modpacksDir || !fs.existsSync(modpacksDir)) return { ok: false, reason: 'not-found' }; const root = path.basename(modpacksDir).toLowerCase() === 'modpacks' ? path.dirname(modpacksDir) : modpacksDir; const meta = readInstalledPacksMeta(root); const list = []; for (const entry of fs.readdirSync(modpacksDir, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const dir = path.join(modpacksDir, entry.name); if (!looksLikePack(dir)) continue; const { version, loader, loaderVersion } = detectVersion(dir); const info = meta[entry.name] || {}; list.push({ folder: entry.name, dir, name: info.displayName || entry.name, group: '', notes: '', version, loader, loaderVersion, gameDir: dir, hasGameData: true, }); } list.sort((a, b) => a.name.localeCompare(b.name)); return { ok: true, instancesDir: modpacksDir, count: list.length, instances: list }; } // kopiert den Pack-Ordner, lässt aber den Technic-internen bin-Cache weg function copyInto(sourceDir, targetDir) { fs.mkdirSync(targetDir, { recursive: true }); for (const name of fs.readdirSync(sourceDir)) { if (SKIP_DIRS.has(name)) continue; fs.cpSync(path.join(sourceDir, name), path.join(targetDir, name), { recursive: true }); } } function importOne(store, entry, copyData = true) { const created = store.createOrReplaceImportedInstance({ name: entry.name, group: entry.group, notes: entry.notes, minecraft: { version: entry.version, loader: entry.loader, loaderVersion: entry.loaderVersion, }, }); let copied = false; if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) { copyInto(entry.gameDir, store.gameDir(created.id)); copied = true; } return { id: created.id, name: created.name, copied }; } module.exports = { scan, importOne, resolveModpacksDir, technicRoot, detectVersion };