'use strict'; /* * cfimport.js – Instanzen aus dem CurseForge-Launcher übernehmen * --------------------------------------------------------------- * Unterstützt sowohl den Launcher-Wurzelordner als auch direkt den * Instances-Ordner. Gelesen werden nach Möglichkeit minecraftinstance.json * und die vorhandenen Spieldaten im Profilordner. */ const fs = require('fs'); const os = require('os'); const path = require('path'); const META_FILES = ['minecraftinstance.json', 'instance.json']; function readJson(file, fallback) { try { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); } catch { return fallback; } } function existingMetaFile(dir) { return META_FILES.map((name) => path.join(dir, name)).find((file) => fs.existsSync(file)) || null; } function metaVersion(meta) { return String( (meta && ( meta.gameVersion || meta.minecraftVersion || meta.mcVersion || (meta.installedModpack && meta.installedModpack.gameVersion) )) || '' ).trim(); } function resolveInstancesDir(inputPath) { if (!inputPath || !fs.existsSync(inputPath)) return null; const directNames = new Set(['instances', 'minecraftinstances']); if (directNames.has(path.basename(inputPath).toLowerCase())) return inputPath; const candidates = [ path.join(inputPath, 'Instances'), path.join(inputPath, 'instances'), path.join(inputPath, 'minecraftInstances'), path.join(inputPath, 'Minecraft', 'Instances'), path.join(inputPath, 'minecraft', 'Instances'), ]; const looksLikeWindowsInstall = /curseforge windows/i.test(inputPath) || (fs.existsSync(path.join(inputPath, 'CurseForge.exe')) && fs.existsSync(path.join(inputPath, 'resources'))); if (looksLikeWindowsInstall) { candidates.push( path.join(os.homedir(), 'curseforge', 'minecraft', 'Instances'), path.join(os.homedir(), 'CurseForge', 'minecraft', 'Instances'), ); } return candidates.find((candidate) => fs.existsSync(candidate)) || null; } function normalizeLoaderVersion(loader, rawValue, mcVersion) { let value = String(rawValue || '').trim(); if (!value) return ''; if (loader === 'forge') { value = value.replace(/^forge-/i, ''); if (mcVersion && value.startsWith(mcVersion + '-')) value = value.slice(mcVersion.length + 1); return value; } if (loader === 'neoforge') { return value.replace(/^neoforge-/i, ''); } if (loader === 'fabric') { value = value.replace(/^fabric(?:-loader)?-/i, ''); if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1)); return value; } if (loader === 'quilt') { value = value.replace(/^quilt(?:-loader)?-/i, ''); if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1)); return value; } return value; } function detectLoader(meta, mcVersion) { const raw = meta && ( meta.baseModLoader || meta.modLoader || meta.modloader || meta.loader || meta.modLoaderId ); const value = typeof raw === 'object' ? (raw.name || raw.id || raw.value || raw.version || '') : (raw || ''); const lower = String(value).toLowerCase(); const objectVersion = raw && typeof raw === 'object' ? (raw.version || raw.name || raw.id || '') : ''; if (!lower) return { loader: 'vanilla', loaderVersion: '' }; if (lower.includes('neoforge')) return { loader: 'neoforge', loaderVersion: normalizeLoaderVersion('neoforge', objectVersion || value, mcVersion) }; if (lower.includes('fabric')) return { loader: 'fabric', loaderVersion: normalizeLoaderVersion('fabric', objectVersion || value, mcVersion) }; if (lower.includes('quilt')) return { loader: 'quilt', loaderVersion: normalizeLoaderVersion('quilt', objectVersion || value, mcVersion) }; if (lower.includes('forge')) return { loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', objectVersion || value, mcVersion) }; return { loader: 'vanilla', loaderVersion: '' }; } function detectVersion(meta, loaderVersion) { return metaVersion(meta) || (loaderVersion && loaderVersion.includes('-') ? loaderVersion.split('-')[0] : ''); } function findGameDir(dir) { const nested = ['minecraft', '.minecraft'] .map((name) => path.join(dir, name)) .find((candidate) => fs.existsSync(candidate)); if (nested) return nested; const markers = ['mods', 'config', 'resourcepacks', 'shaderpacks', 'saves', 'options.txt']; return markers.some((name) => fs.existsSync(path.join(dir, name))) ? dir : null; } function scan(inputPath) { const instancesDir = resolveInstancesDir(inputPath); if (!instancesDir) return { ok: false, reason: 'not-found' }; const list = []; for (const entry of fs.readdirSync(instancesDir, { withFileTypes: true })) { if (!entry.isDirectory()) continue; const dir = path.join(instancesDir, entry.name); const metaFile = existingMetaFile(dir); const meta = metaFile ? readJson(metaFile, {}) : {}; const gameDir = findGameDir(dir); if (!metaFile && !gameDir) continue; const versionHint = metaVersion(meta); const loaderInfo = detectLoader(meta, versionHint); list.push({ folder: entry.name, dir, name: meta.name || meta.displayName || entry.name, group: '', notes: meta.notes || meta.summary || '', version: versionHint || detectVersion(meta, loaderInfo.loaderVersion), loader: loaderInfo.loader, loaderVersion: loaderInfo.loaderVersion, javaPath: String(meta.javaPath || meta.javaExecutable || '').trim(), minMemMb: Number(meta.minimumMemory || meta.minMemory || meta.minMemAlloc) || null, maxMemMb: Number(meta.maximumMemory || meta.maxMemory || meta.maxMemAlloc || meta.allocatedMemory) || null, gameDir: gameDir || dir, hasGameData: !!gameDir, }); } list.sort((a, b) => a.name.localeCompare(b.name)); return { ok: true, instancesDir, count: list.length, instances: list }; } function copyInto(sourceDir, targetDir) { fs.mkdirSync(targetDir, { recursive: true }); for (const name of fs.readdirSync(sourceDir)) { if (META_FILES.includes(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, }, }); 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)) { copyInto(entry.gameDir, store.gameDir(created.id)); copied = true; } return { id: created.id, name: created.name, copied }; } module.exports = { scan, importOne, resolveInstancesDir };