217 lines
7.5 KiB
JavaScript
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 }; |