'use strict'; /* * mmcimport.js – Instanzen aus MultiMC/Prism übernehmen * ------------------------------------------------------ * Es genügt der Pfad zum MultiMC-Ordner (oder direkt zum instances-Ordner). * Ausgelesen werden: * instance.cfg – Name, Gruppe, Java-Pfad, Arbeitsspeicher, Notizen * mmc-pack.json – Minecraft-Version und Mod-Loader * .minecraft/ – Spielverzeichnis (Welten, Mods, Konfigurationen) */ const fs = require('fs'); const path = require('path'); // uid der Loader in mmc-pack.json -> unsere Bezeichnung const LOADER_UIDS = { 'net.fabricmc.fabric-loader': 'fabric', 'org.quiltmc.quilt-loader': 'quilt', 'net.minecraftforge': 'forge', 'net.neoforged': 'neoforge', }; function parseCfg(file) { const out = {}; try { for (const line of fs.readFileSync(file, 'utf8').split(/\r?\n/)) { const i = line.indexOf('='); if (i > 0) out[line.slice(0, i).trim()] = line.slice(i + 1).trim(); } } catch { /* Datei fehlt */ } return out; } // Gruppen stehen zentral in instances/instgroups.json function readGroups(instancesDir) { const map = {}; try { const raw = JSON.parse(fs.readFileSync(path.join(instancesDir, 'instgroups.json'), 'utf8')); for (const [group, data] of Object.entries(raw.groups || {})) { for (const inst of data.instances || []) map[inst] = group; } } catch { /* keine Gruppen hinterlegt */ } return map; } function readPack(dir) { const res = { version: '', loader: 'vanilla', loaderVersion: '' }; try { const pack = JSON.parse(fs.readFileSync(path.join(dir, 'mmc-pack.json'), 'utf8')); for (const c of pack.components || []) { if (c.uid === 'net.minecraft') res.version = c.version || c.cachedVersion || ''; const loader = LOADER_UIDS[c.uid]; if (loader) { res.loader = loader; res.loaderVersion = c.version || ''; } } } catch { /* alte Instanz ohne mmc-pack.json */ } return res; } // findet den instances-Ordner, egal ob MultiMC-Wurzel oder instances direkt angegeben wurde function resolveInstancesDir(inputPath) { if (!inputPath) return null; const direct = path.join(inputPath, 'instances'); if (fs.existsSync(direct)) return direct; if (path.basename(inputPath).toLowerCase() === 'instances' && fs.existsSync(inputPath)) return inputPath; return fs.existsSync(inputPath) ? inputPath : null; } /* * Liest einen einzelnen Instanzordner aus. null, wenn dort keine Instanz liegt. * Wird sowohl beim Ordner-Import als auch beim ZIP-Import verwendet. */ function readInstance(dir, folderName, group) { const cfgFile = path.join(dir, 'instance.cfg'); if (!fs.existsSync(cfgFile)) return null; // kein Instanzordner const cfg = parseCfg(cfgFile); const pack = readPack(dir); // MultiMC nutzt .minecraft, Prism minecraft const gameDir = ['.minecraft', 'minecraft'] .map((n) => path.join(dir, n)) .find((p) => fs.existsSync(p)) || null; return { folder: folderName, dir, name: cfg.name || folderName, group: group || '', notes: cfg.notes || '', version: pack.version || cfg.IntendedVersion || '', loader: pack.loader, loaderVersion: pack.loaderVersion, javaPath: cfg.OverrideJavaLocation === 'true' ? (cfg.JavaPath || '') : '', minMemMb: cfg.OverrideMemory === 'true' ? Number(cfg.MinMemAlloc) || null : null, maxMemMb: cfg.OverrideMemory === 'true' ? Number(cfg.MaxMemAlloc) || null : null, gameDir, hasGameData: !!gameDir, }; } /* * Listet die gefundenen Instanzen auf (ohne etwas zu kopieren). */ function scan(inputPath) { const instancesDir = resolveInstancesDir(inputPath); if (!instancesDir) return { ok: false, reason: 'not-found' }; const groups = readGroups(instancesDir); const list = []; for (const entry of fs.readdirSync(instancesDir, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const inst = readInstance(path.join(instancesDir, entry.name), entry.name, groups[entry.name]); if (inst) list.push(inst); } list.sort((a, b) => a.name.localeCompare(b.name)); return { ok: true, instancesDir, count: list.length, instances: list }; } /* * Sucht einen Instanzordner unterhalb von root. Deckt beide Zip-Varianten ab: * mit umgebendem Ordner ("26.2/instance.cfg") und ohne ("instance.cfg"). * Die Tiefe ist begrenzt, damit nicht das ganze .minecraft durchsucht wird. */ function findInstanceDir(root, tiefe = 2) { if (!root || !fs.existsSync(root)) return null; if (fs.existsSync(path.join(root, 'instance.cfg'))) return root; if (tiefe <= 0) return null; let eintraege = []; try { eintraege = fs.readdirSync(root, { withFileTypes: true }); } catch { return null; } for (const e of eintraege) { if (!e.isDirectory()) continue; // .minecraft enthält nie eine Instanz und ist der mit Abstand größte Ordner if (e.name === '.minecraft' || e.name === 'minecraft') continue; const treffer = findInstanceDir(path.join(root, e.name), tiefe - 1); if (treffer) return treffer; } return null; } /* * Liest eine einzelne, ausgepackte MultiMC/Prism-Instanz. * -> { ok, instance } oder { ok: false, reason: 'not-found' } */ function scanSingle(root) { const dir = findInstanceDir(root); if (!dir) return { ok: false, reason: 'not-found' }; const inst = readInstance(dir, path.basename(dir), ''); return inst ? { ok: true, instance: inst } : { ok: false, reason: 'not-found' }; } /* * Übernimmt eine Instanz in den Launcher. * store: unser Datenspeicher; copyData: Spielverzeichnis mitkopieren */ 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, }, }); const patch = {}; if (entry.javaPath || entry.minMemMb || entry.maxMemMb) { patch.java = { path: entry.javaPath || '', minMemMb: entry.minMemMb || null, maxMemMb: entry.maxMemMb || null, extraArgs: '', }; } if (Object.keys(patch).length) store.updateInstance(created.id, patch); let copied = false; if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) { try { fs.cpSync(entry.gameDir, store.gameDir(created.id), { recursive: true }); copied = true; } catch { /* Spieldaten konnten nicht kopiert werden */ } } return { id: created.id, name: created.name, copied }; } /* * Was beim Übernehmen kopiert wird: [{ von, name }] – `name` ist der Zielname * im Spielordner der neuen Instanz. Das Kopieren selbst erledigt kopieren.js * asynchron mit Fortschritt (main.js → fuehreImportAus). */ function kopierQuellen(entry) { if (!entry.gameDir || !fs.existsSync(entry.gameDir)) return []; return fs.readdirSync(entry.gameDir).map((name) => ({ von: path.join(entry.gameDir, name), name })); } module.exports = { scan, scanSingle, importOne, kopierQuellen, resolveInstancesDir, readInstance, findInstanceDir };