Files
AeroMc-Launcher/src/mcimport.js
2026-08-02 10:14:02 +02:00

217 lines
7.5 KiB
JavaScript

'use strict';
const fs = require('fs');
const os = require('os');
const path = require('path');
const PROFILE_FILES = [
'launcher_profiles.json',
'launcher_profiles_microsoft_store.json',
'launcher_profiles_microsoft_store_2.json',
];
const ROOT_COPY_DIRS = new Set([
'config', 'defaultconfigs', 'kubejs', 'mods', 'resourcepacks', 'screenshots', 'shaderpacks', 'saves',
]);
const ROOT_COPY_FILE_PATTERNS = [
/^options.*\.(txt|of)$/i,
/^servers\.dat(?:_old)?$/i,
/^usercache\.json$/i,
/^tl_skin_cape\.json$/i,
/^journeymap.*\.(json|txt|cfg)$/i,
];
function readJson(file, fallback) {
try {
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
} catch {
return fallback;
}
}
function resolveLauncherRoot(inputPath) {
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
const defaultRoot = path.join(appData, '.minecraft');
const candidates = [];
if (inputPath && fs.existsSync(inputPath)) candidates.push(inputPath);
candidates.push(defaultRoot);
for (const candidate of candidates) {
for (const profileFile of PROFILE_FILES) {
if (fs.existsSync(path.join(candidate, profileFile))) return candidate;
}
}
return null;
}
function resolveProfileFile(rootDir) {
return PROFILE_FILES.map((name) => path.join(rootDir, name)).find((file) => fs.existsSync(file)) || 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 parseLastVersionId(lastVersionId) {
const raw = String(lastVersionId || '').trim();
if (!raw) return { version: '', loader: 'vanilla', loaderVersion: '' };
let match = raw.match(/^fabric-loader-([^-]+(?:\.[^-]+)*)-(.+)$/i);
if (match) return { version: match[2], loader: 'fabric', loaderVersion: normalizeLoaderVersion('fabric', match[1], match[2]) };
match = raw.match(/^quilt-loader-([^-]+(?:\.[^-]+)*)-(.+)$/i);
if (match) return { version: match[2], loader: 'quilt', loaderVersion: normalizeLoaderVersion('quilt', match[1], match[2]) };
match = raw.match(/^(.+)-forge-([\w.-]+)$/i);
if (match) return { version: match[1], loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', match[2], match[1]) };
match = raw.match(/^forge-(.+)-([\w.-]+)$/i);
if (match) return { version: match[1], loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', match[2], match[1]) };
match = raw.match(/^(.+)-neoforge-([\w.-]+)$/i);
if (match) return { version: match[1], loader: 'neoforge', loaderVersion: normalizeLoaderVersion('neoforge', match[2], match[1]) };
if (/^\d+(?:\.\d+)+(?:-[\w.]+)?$/i.test(raw)) return { version: raw, loader: 'vanilla', loaderVersion: '' };
return { version: raw, loader: 'vanilla', loaderVersion: '' };
}
function parseJavaSettings(javaDir, javaArgs) {
const args = String(javaArgs || '').trim();
const minMatch = args.match(/(?:^|\s)-Xms(\d+)([mMgG])/);
const maxMatch = args.match(/(?:^|\s)-Xmx(\d+)([mMgG])/);
const toMb = (match) => {
if (!match) return null;
const num = Number(match[1]);
if (!Number.isFinite(num)) return null;
return match[2].toLowerCase() === 'g' ? num * 1024 : num;
};
return {
path: String(javaDir || '').trim(),
minMemMb: toMb(minMatch),
maxMemMb: toMb(maxMatch),
extraArgs: args
.replace(/(?:^|\s)-Xms\d+[mMgG]/g, ' ')
.replace(/(?:^|\s)-Xmx\d+[mMgG]/g, ' ')
.replace(/\s+/g, ' ')
.trim(),
};
}
function isImportableProfile(profile) {
const type = String((profile && profile.type) || '').trim().toLowerCase();
if (type === 'latest-release' || type === 'latest-snapshot') return false;
const name = String((profile && profile.name) || '').trim();
const lastVersionId = String((profile && profile.lastVersionId) || '').trim();
return !!(name || lastVersionId);
}
function shouldCopyRootEntry(name) {
if (ROOT_COPY_DIRS.has(name)) return true;
return ROOT_COPY_FILE_PATTERNS.some((pattern) => pattern.test(name));
}
function scan(inputPath) {
const rootDir = resolveLauncherRoot(inputPath);
if (!rootDir) return { ok: false, reason: 'not-found' };
const profileFile = resolveProfileFile(rootDir);
const json = readJson(profileFile, {});
const profiles = json.profiles || {};
const list = [];
const defaultRoot = path.normalize(rootDir).toLowerCase();
for (const [id, profile] of Object.entries(profiles)) {
if (!isImportableProfile(profile)) continue;
const name = String(profile.name || '').trim() || String(profile.lastVersionId || '').trim() || id;
const parsed = parseLastVersionId(profile.lastVersionId);
const gameDir = path.normalize(String(profile.gameDir || rootDir));
const java = parseJavaSettings(profile.javaDir, profile.javaArgs);
list.push({
id,
name,
group: '',
notes: 'Importiert aus dem Minecraft Launcher',
version: parsed.version,
loader: parsed.loader,
loaderVersion: parsed.loaderVersion,
javaPath: java.path,
minMemMb: java.minMemMb,
maxMemMb: java.maxMemMb,
extraJavaArgs: java.extraArgs,
gameDir,
hasGameData: fs.existsSync(gameDir),
usesDefaultGameDir: gameDir.toLowerCase() === defaultRoot,
rootDir,
});
}
list.sort((a, b) => a.name.localeCompare(b.name));
return { ok: true, rootDir, count: list.length, instances: list };
}
function copyInto(sourceDir, targetDir, usesDefaultGameDir) {
fs.mkdirSync(targetDir, { recursive: true });
for (const name of fs.readdirSync(sourceDir)) {
if (/^launcher_profiles.*\.json$/i.test(name)) continue;
if (name === 'versions' || name === 'libraries' || name === 'assets' || name === 'runtime' || name === 'webcache2') continue;
if (usesDefaultGameDir && !shouldCopyRootEntry(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 || entry.extraJavaArgs) {
patch.java = {
path: entry.javaPath || '',
minMemMb: entry.minMemMb || null,
maxMemMb: entry.maxMemMb || null,
extraArgs: entry.extraJavaArgs || '',
};
}
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), !!entry.usesDefaultGameDir);
copied = true;
}
return { id: created.id, name: created.name, copied };
}
module.exports = { scan, importOne, parseLastVersionId, resolveLauncherRoot };