196 lines
7.0 KiB
JavaScript
196 lines
7.0 KiB
JavaScript
'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 }; |