5 Commits
Author SHA1 Message Date
M_Viper 425b94fd93 Upload via GUI (44 Dateien) 2026-08-14 08:17:05 +00:00
M_Viper e0bdcb01fb Edit README.md via Git Manager GUI 2026-08-09 08:07:12 +00:00
M_Viper c840a49f75 Edit README.md via Git Manager GUI 2026-08-09 08:07:11 +00:00
M_Viper c535d8b830 Edit README.md via Git Manager GUI 2026-08-09 08:07:07 +00:00
Git Manager GUI 248a387f01 Upload via GUI (45 Dateien) 2026-08-09 10:06:39 +02:00
8 changed files with 978 additions and 957 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
Ein Minecraft-Launcher für Windows mit getrennten Instanzen, Mod-Unterstützung und Microsoft-Anmeldung. Ein Minecraft-Launcher für Windows mit getrennten Instanzen, Mod-Unterstützung und Microsoft-Anmeldung.
Aktueller Stand: Version 1.0.8 Aktueller Stand: Version 1.0.9
</div> </div>
+3 -3
View File
@@ -1,13 +1,13 @@
{ {
"name": "aeromc", "name": "aeromc",
"version": "1.0.7", "version": "1.0.9",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "aeromc", "name": "aeromc",
"version": "1.0.7", "version": "1.0.9",
"license": "MIT", "license": "SEE LICENSE IN LICENSE",
"dependencies": { "dependencies": {
"archiver": "^7.0.1", "archiver": "^7.0.1",
"skinview3d": "^3.4.2", "skinview3d": "^3.4.2",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "aeromc", "name": "aeromc",
"productName": "AeroMC", "productName": "AeroMC",
"version": "1.0.8", "version": "1.0.9",
"description": "AeroMC Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)", "description": "AeroMC Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
"homepage": "https://m-viper.de", "homepage": "https://m-viper.de",
"launcherWebsite": "https://aeromc.viper.ipv64.net", "launcherWebsite": "https://aeromc.viper.ipv64.net",
+451 -439
View File
@@ -1,440 +1,452 @@
'use strict'; 'use strict';
const archiver = require('archiver'); const archiver = require('archiver');
const fs = require('fs'); const fs = require('fs');
const os = require('os'); const os = require('os');
const path = require('path'); const path = require('path');
const { pipeline } = require('stream/promises'); const { pipeline } = require('stream/promises');
const unzipper = require('unzipper'); const unzipper = require('unzipper');
function pad2(value) { function pad2(value) {
return String(value).padStart(2, '0'); return String(value).padStart(2, '0');
} }
function defaultBackupFileName(date = new Date()) { function defaultBackupFileName(date = new Date()) {
return 'AeroMC-Backup-' + return 'AeroMC-Backup-' +
date.getFullYear() + '-' + date.getFullYear() + '-' +
pad2(date.getMonth() + 1) + '-' + pad2(date.getMonth() + 1) + '-' +
pad2(date.getDate()) + '_' + pad2(date.getDate()) + '_' +
pad2(date.getHours()) + '-' + pad2(date.getHours()) + '-' +
pad2(date.getMinutes()) + '-' + pad2(date.getMinutes()) + '-' +
pad2(date.getSeconds()) + pad2(date.getSeconds()) +
'.zip'; '.zip';
} }
function defaultBackupDir(documentsDir) { function defaultBackupDir(documentsDir) {
return path.join(documentsDir, 'AeroMC Launcher', 'Backups'); return path.join(documentsDir, 'AeroMC Launcher', 'Backups');
} }
function resolveAutoBackupPath(documentsDir, date = new Date()) { function resolveAutoBackupPath(documentsDir, date = new Date()) {
const dir = defaultBackupDir(documentsDir); const dir = defaultBackupDir(documentsDir);
ensureDir(dir); ensureDir(dir);
const baseName = defaultBackupFileName(date); const baseName = defaultBackupFileName(date);
const ext = path.extname(baseName); const ext = path.extname(baseName);
const stem = baseName.slice(0, -ext.length); const stem = baseName.slice(0, -ext.length);
let attempt = 0; let attempt = 0;
let candidate = path.join(dir, baseName); let candidate = path.join(dir, baseName);
while (fs.existsSync(candidate)) { while (fs.existsSync(candidate)) {
attempt += 1; attempt += 1;
candidate = path.join(dir, `${stem}-${attempt}${ext}`); candidate = path.join(dir, `${stem}-${attempt}${ext}`);
} }
return candidate; return candidate;
} }
function listGlobalBackups(documentsDir) { function listGlobalBackups(documentsDir) {
const dir = defaultBackupDir(documentsDir); const dir = defaultBackupDir(documentsDir);
ensureDir(dir); ensureDir(dir);
return fs.readdirSync(dir, { withFileTypes: true }) return fs.readdirSync(dir, { withFileTypes: true })
.filter((entry) => entry.isFile() && /\.zip$/i.test(entry.name) && !/\.partial\.zip$/i.test(entry.name)) .filter((entry) => entry.isFile() && /\.zip$/i.test(entry.name) && !/\.partial\.zip$/i.test(entry.name))
.map((entry) => { .map((entry) => {
const filePath = path.join(dir, entry.name); const filePath = path.join(dir, entry.name);
const stat = fs.statSync(filePath); const stat = fs.statSync(filePath);
return { return {
name: entry.name, name: entry.name,
path: filePath, path: filePath,
size: stat.size, size: stat.size,
modifiedAt: stat.mtime.toISOString(), modifiedAt: stat.mtime.toISOString(),
}; };
}) })
.sort((a, b) => { .sort((a, b) => {
const timeDiff = new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime(); const timeDiff = new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime();
return timeDiff || a.name.localeCompare(b.name, 'de'); return timeDiff || a.name.localeCompare(b.name, 'de');
}); });
} }
function ensureDir(dir) { function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
} }
function countInstanceDirs(instancesDir) { function countInstanceDirs(instancesDir) {
if (!fs.existsSync(instancesDir)) return 0; if (!fs.existsSync(instancesDir)) return 0;
return fs.readdirSync(instancesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).length; return fs.readdirSync(instancesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).length;
} }
function readJson(file, fallback) { function readJson(file, fallback) {
try { try {
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
} catch { } catch {
return fallback; return fallback;
} }
} }
function writeJson(file, data) { function writeJson(file, data) {
ensureDir(path.dirname(file)); ensureDir(path.dirname(file));
fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8'); fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf8');
} }
function emitProgress(cb, mode, percent, detail) { function emitProgress(cb, mode, percent, detail) {
if (!cb) return; if (!cb) return;
cb({ mode, percent: Math.max(0, Math.min(100, Math.round(percent))), detail: detail || '' }); cb({ mode, percent: Math.max(0, Math.min(100, Math.round(percent))), detail: detail || '' });
} }
function normalizeZipEntryPath(entryPath) { function normalizeZipEntryPath(entryPath) {
const parts = String(entryPath || '') const parts = String(entryPath || '')
.replace(/\\/g, '/') .replace(/\\/g, '/')
.split('/') .split('/')
.filter(Boolean); .filter(Boolean);
if (!parts.length) return ''; if (!parts.length) return '';
if (parts.some((part) => part === '.' || part === '..')) { if (parts.some((part) => part === '.' || part === '..')) {
throw new Error('Das Backup-Archiv enthält ungültige Pfade.'); throw new Error('Das Backup-Archiv enthält ungültige Pfade.');
} }
return path.join(...parts); return path.join(...parts);
} }
async function extractBackupArchive(sourcePath, destinationDir, options) { async function extractBackupArchive(sourcePath, destinationDir, options) {
const opts = options || {}; const opts = options || {};
const onProgress = opts.onProgress; const onProgress = opts.onProgress;
const mode = opts.mode || 'restore'; const mode = opts.mode || 'restore';
const startPercent = opts.startPercent ?? 5; const startPercent = opts.startPercent ?? 5;
const endPercent = opts.endPercent ?? 18; const endPercent = opts.endPercent ?? 18;
const detail = opts.detail || 'Entpacke Backup'; const detail = opts.detail || 'Entpacke Backup';
const directory = await unzipper.Open.file(sourcePath); const directory = await unzipper.Open.file(sourcePath);
const files = directory.files || []; const files = directory.files || [];
const fileEntries = files.filter((entry) => entry.type !== 'Directory'); const fileEntries = files.filter((entry) => entry.type !== 'Directory');
const totalBytes = fileEntries.reduce((sum, entry) => sum + Number(entry.uncompressedSize || 0), 0); const totalBytes = fileEntries.reduce((sum, entry) => sum + Number(entry.uncompressedSize || 0), 0);
let processedBytes = 0; let processedBytes = 0;
let processedEntries = 0; let processedEntries = 0;
ensureDir(destinationDir); ensureDir(destinationDir);
if (!files.length) { if (!files.length) {
emitProgress(onProgress, mode, endPercent, detail + ' abgeschlossen'); emitProgress(onProgress, mode, endPercent, detail + ' abgeschlossen');
return; return;
} }
for (const entry of files) { for (const entry of files) {
const relativePath = normalizeZipEntryPath(entry.path); const relativePath = normalizeZipEntryPath(entry.path);
if (!relativePath) continue; if (!relativePath) continue;
const targetPath = path.join(destinationDir, relativePath); const targetPath = path.join(destinationDir, relativePath);
if (entry.type === 'Directory') { if (entry.type === 'Directory') {
ensureDir(targetPath); ensureDir(targetPath);
continue; continue;
} }
ensureDir(path.dirname(targetPath)); ensureDir(path.dirname(targetPath));
await pipeline(entry.stream(), fs.createWriteStream(targetPath)); await pipeline(entry.stream(), fs.createWriteStream(targetPath));
processedBytes += Number(entry.uncompressedSize || 0); processedBytes += Number(entry.uncompressedSize || 0);
processedEntries += 1; processedEntries += 1;
const ratio = totalBytes > 0 const ratio = totalBytes > 0
? Math.max(0, Math.min(1, processedBytes / totalBytes)) ? Math.max(0, Math.min(1, processedBytes / totalBytes))
: Math.max(0, Math.min(1, processedEntries / Math.max(1, fileEntries.length))); : Math.max(0, Math.min(1, processedEntries / Math.max(1, fileEntries.length)));
const percent = startPercent + ((endPercent - startPercent) * ratio); const percent = startPercent + ((endPercent - startPercent) * ratio);
emitProgress(onProgress, mode, percent, detail + ' ...'); emitProgress(onProgress, mode, percent, detail + ' ...');
} }
} }
function createBackupArchive(options) { function createBackupArchive(options) {
const opts = options || {}; const opts = options || {};
const instancesDir = opts.instancesDir; const instancesDir = opts.instancesDir;
const settingsFile = opts.settingsFile; const settingsFile = opts.settingsFile;
const includeSettings = opts.includeSettings !== false; const includeSettings = opts.includeSettings !== false;
const backupInfo = opts.backupInfo || {}; const backupInfo = opts.backupInfo || {};
const destinationPath = opts.destinationPath; const destinationPath = opts.destinationPath;
const onProgress = opts.onProgress; const onProgress = opts.onProgress;
const mode = opts.mode || 'create'; const mode = opts.mode || 'create';
const startPercent = opts.startPercent ?? 15; const startPercent = opts.startPercent ?? 15;
const endPercent = opts.endPercent ?? 98; const endPercent = opts.endPercent ?? 98;
const detail = opts.detail || 'Packe ZIP-Archiv'; const detail = opts.detail || 'Packe ZIP-Archiv';
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
ensureDir(path.dirname(destinationPath)); ensureDir(path.dirname(destinationPath));
const output = fs.createWriteStream(destinationPath); const output = fs.createWriteStream(destinationPath);
const archive = archiver('zip', { store: true }); const archive = archiver('zip', { store: true });
let done = false; let done = false;
const fail = (err) => { const fail = (err) => {
if (done) return; if (done) return;
done = true; done = true;
try { archive.destroy(); } catch { /* ignore */ } try { archive.destroy(); } catch { /* ignore */ }
try { output.destroy(); } catch { /* ignore */ } try { output.destroy(); } catch { /* ignore */ }
reject(err); reject(err);
}; };
output.on('close', () => { output.on('close', () => {
if (done) return; if (done) return;
done = true; done = true;
resolve({ bytes: archive.pointer() }); resolve({ bytes: archive.pointer() });
}); });
output.on('error', fail); output.on('error', fail);
archive.on('error', fail); archive.on('error', fail);
archive.on('warning', (err) => { archive.on('warning', (err) => {
if (err && err.code === 'ENOENT') return; if (err && err.code === 'ENOENT') return;
fail(err); fail(err);
}); });
archive.on('progress', (progress) => { archive.on('progress', (progress) => {
const totalBytes = progress && progress.fs ? progress.fs.totalBytes : 0; const totalBytes = progress && progress.fs ? progress.fs.totalBytes : 0;
const processedBytes = progress && progress.fs ? progress.fs.processedBytes : 0; const processedBytes = progress && progress.fs ? progress.fs.processedBytes : 0;
if (!totalBytes) return; if (!totalBytes) return;
const ratio = Math.max(0, Math.min(1, processedBytes / totalBytes)); const ratio = Math.max(0, Math.min(1, processedBytes / totalBytes));
const percent = startPercent + ((endPercent - startPercent) * ratio); const percent = startPercent + ((endPercent - startPercent) * ratio);
emitProgress(onProgress, mode, percent, detail + ' ...'); emitProgress(onProgress, mode, percent, detail + ' ...');
}); });
archive.pipe(output); archive.pipe(output);
if (fs.existsSync(instancesDir)) archive.directory(instancesDir, 'instances'); if (fs.existsSync(instancesDir)) archive.directory(instancesDir, 'instances');
else archive.append('', { name: 'instances/.keep' }); else archive.append('', { name: 'instances/.keep' });
if (includeSettings && settingsFile && fs.existsSync(settingsFile)) { if (includeSettings && settingsFile && fs.existsSync(settingsFile)) {
archive.file(settingsFile, { name: 'settings.json' }); archive.file(settingsFile, { name: 'settings.json' });
} }
archive.append(`${JSON.stringify(backupInfo, null, 2)}\n`, { name: 'backup-info.json' }); archive.append(`${JSON.stringify(backupInfo, null, 2)}\n`, { name: 'backup-info.json' });
try { try {
const finalizeResult = archive.finalize(); const finalizeResult = archive.finalize();
if (finalizeResult && typeof finalizeResult.catch === 'function') finalizeResult.catch(fail); if (finalizeResult && typeof finalizeResult.catch === 'function') finalizeResult.catch(fail);
} catch (err) { } catch (err) {
fail(err); fail(err);
} }
}); });
} }
function copyDirContentsTracked(sourceDir, targetDir, options) { function copyDirContentsTracked(sourceDir, targetDir, options) {
const opts = options || {}; const opts = options || {};
const mode = opts.mode || 'create'; const mode = opts.mode || 'create';
const onProgress = opts.onProgress; const onProgress = opts.onProgress;
const startPercent = opts.startPercent ?? 0; const startPercent = opts.startPercent ?? 0;
const endPercent = opts.endPercent ?? 100; const endPercent = opts.endPercent ?? 100;
const detailPrefix = opts.detailPrefix || 'Kopiere'; const detailPrefix = opts.detailPrefix || 'Kopiere';
ensureDir(targetDir); ensureDir(targetDir);
if (!fs.existsSync(sourceDir)) { if (!fs.existsSync(sourceDir)) {
emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen'); emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen');
return; return;
} }
const entries = fs.readdirSync(sourceDir, { withFileTypes: true }); const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
if (!entries.length) { if (!entries.length) {
emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen'); emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen');
return; return;
} }
entries.forEach((entry, index) => { entries.forEach((entry, index) => {
fs.cpSync(path.join(sourceDir, entry.name), path.join(targetDir, entry.name), { recursive: true }); fs.cpSync(path.join(sourceDir, entry.name), path.join(targetDir, entry.name), { recursive: true });
const ratio = (index + 1) / entries.length; const ratio = (index + 1) / entries.length;
const percent = startPercent + ((endPercent - startPercent) * ratio); const percent = startPercent + ((endPercent - startPercent) * ratio);
emitProgress(onProgress, mode, percent, `${detailPrefix}: ${entry.name}`); emitProgress(onProgress, mode, percent, `${detailPrefix}: ${entry.name}`);
}); });
} }
function moveDirTracked(sourceDir, targetDir, options) { function moveDirTracked(sourceDir, targetDir, options) {
const opts = options || {}; const opts = options || {};
const mode = opts.mode || 'restore'; const mode = opts.mode || 'restore';
const onProgress = opts.onProgress; const onProgress = opts.onProgress;
const startPercent = opts.startPercent ?? 0; const startPercent = opts.startPercent ?? 0;
const endPercent = opts.endPercent ?? 100; const endPercent = opts.endPercent ?? 100;
const detailPrefix = opts.detailPrefix || 'Verschiebe'; const detailPrefix = opts.detailPrefix || 'Verschiebe';
if (!fs.existsSync(sourceDir)) { if (!fs.existsSync(sourceDir)) {
emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen'); emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen');
return; return;
} }
ensureDir(path.dirname(targetDir)); ensureDir(path.dirname(targetDir));
try { try {
fs.renameSync(sourceDir, targetDir); fs.renameSync(sourceDir, targetDir);
emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen'); emitProgress(onProgress, mode, endPercent, detailPrefix + ' abgeschlossen');
return; return;
} catch (err) { } catch (err) {
if (!err || !['EXDEV', 'EPERM', 'EACCES'].includes(err.code)) throw err; if (!err || !['EXDEV', 'EPERM', 'EACCES'].includes(err.code)) throw err;
} }
copyDirContentsTracked(sourceDir, targetDir, opts); copyDirContentsTracked(sourceDir, targetDir, opts);
try { fs.rmSync(sourceDir, { recursive: true, force: true }); } catch { /* ignore */ } try { fs.rmSync(sourceDir, { recursive: true, force: true }); } catch { /* ignore */ }
} }
function resolveExtractedBackupRoot(extractDir) { function resolveExtractedBackupRoot(extractDir) {
if (fs.existsSync(path.join(extractDir, 'instances'))) return extractDir; if (fs.existsSync(path.join(extractDir, 'instances'))) return extractDir;
const entries = fs.readdirSync(extractDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()); const entries = fs.readdirSync(extractDir, { withFileTypes: true }).filter((entry) => entry.isDirectory());
if (entries.length === 1) { if (entries.length === 1) {
const nested = path.join(extractDir, entries[0].name); const nested = path.join(extractDir, entries[0].name);
if (fs.existsSync(path.join(nested, 'instances'))) return nested; if (fs.existsSync(path.join(nested, 'instances'))) return nested;
} }
return null; return null;
} }
async function createGlobalBackup(options) { async function createGlobalBackup(options) {
const instancesDir = options && options.instancesDir; const instancesDir = options && options.instancesDir;
const userDataDir = options && options.userDataDir; const userDataDir = options && options.userDataDir;
const destinationPath = options && options.destinationPath; const destinationPath = options && options.destinationPath;
const includeSettings = !options || options.includeSettings !== false; const includeSettings = !options || options.includeSettings !== false;
const onProgress = options && options.onProgress; const onProgress = options && options.onProgress;
if (!instancesDir || !userDataDir || !destinationPath) throw new Error('Backup-Parameter unvollständig.'); if (!instancesDir || !userDataDir || !destinationPath) throw new Error('Backup-Parameter unvollständig.');
const settingsFile = path.join(userDataDir, 'settings.json'); const settingsFile = path.join(userDataDir, 'settings.json');
const workingArchivePath = destinationPath.replace(/\.zip$/i, '') + '.partial.zip'; const workingArchivePath = destinationPath.replace(/\.zip$/i, '') + '.partial.zip';
const backupInfo = { const backupInfo = {
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
instanceCount: countInstanceDirs(instancesDir), instanceCount: countInstanceDirs(instancesDir),
includeSettings, includeSettings,
app: 'AeroMC', app: 'AeroMC',
version: 1, version: 1,
}; };
try { try {
emitProgress(onProgress, 'create', 5, 'Bereite Backup vor'); emitProgress(onProgress, 'create', 5, 'Bereite Backup vor');
emitProgress(onProgress, 'create', 10, 'Ermittle Backup-Inhalt'); emitProgress(onProgress, 'create', 10, 'Ermittle Backup-Inhalt');
if (fs.existsSync(destinationPath)) fs.unlinkSync(destinationPath); if (fs.existsSync(destinationPath)) fs.unlinkSync(destinationPath);
if (fs.existsSync(workingArchivePath)) fs.unlinkSync(workingArchivePath); if (fs.existsSync(workingArchivePath)) fs.unlinkSync(workingArchivePath);
emitProgress(onProgress, 'create', 90, 'Packe ZIP-Archiv'); emitProgress(onProgress, 'create', 90, 'Packe ZIP-Archiv');
await createBackupArchive({ await createBackupArchive({
instancesDir, instancesDir,
settingsFile, settingsFile,
includeSettings, includeSettings,
backupInfo, backupInfo,
destinationPath: workingArchivePath, destinationPath: workingArchivePath,
mode: 'create', mode: 'create',
onProgress, onProgress,
startPercent: 15, startPercent: 15,
endPercent: 98, endPercent: 98,
detail: 'Packe ZIP-Archiv', detail: 'Packe ZIP-Archiv',
}); });
emitProgress(onProgress, 'create', 99, 'Finalisiere Backup'); emitProgress(onProgress, 'create', 99, 'Finalisiere Backup');
fs.renameSync(workingArchivePath, destinationPath); fs.renameSync(workingArchivePath, destinationPath);
emitProgress(onProgress, 'create', 100, 'Backup abgeschlossen'); emitProgress(onProgress, 'create', 100, 'Backup abgeschlossen');
return { return {
ok: true, ok: true,
path: destinationPath, path: destinationPath,
instanceCount: countInstanceDirs(instancesDir), instanceCount: countInstanceDirs(instancesDir),
includedSettings: includeSettings && fs.existsSync(settingsFile), includedSettings: includeSettings && fs.existsSync(settingsFile),
}; };
} finally { } finally {
try { try {
if (fs.existsSync(workingArchivePath)) fs.rmSync(workingArchivePath, { force: true }); if (fs.existsSync(workingArchivePath)) fs.rmSync(workingArchivePath, { force: true });
} catch { /* ignore */ } } catch { /* ignore */ }
} }
} }
async function restoreGlobalBackup(options) { async function restoreGlobalBackup(options) {
const instancesDir = options && options.instancesDir; const instancesDir = options && options.instancesDir;
const userDataDir = options && options.userDataDir; const userDataDir = options && options.userDataDir;
const sourcePath = options && options.sourcePath; const sourcePath = options && options.sourcePath;
const includeSettings = !!(options && options.includeSettings); const includeSettings = !!(options && options.includeSettings);
const onProgress = options && options.onProgress; const onProgress = options && options.onProgress;
const instancesDirSetting = options && Object.prototype.hasOwnProperty.call(options, 'instancesDirSetting') const instancesDirSetting = options && Object.prototype.hasOwnProperty.call(options, 'instancesDirSetting')
? options.instancesDirSetting ? options.instancesDirSetting
: null; : null;
if (!instancesDir || !userDataDir || !sourcePath) throw new Error('Restore-Parameter unvollständig.'); if (!instancesDir || !userDataDir || !sourcePath) throw new Error('Restore-Parameter unvollständig.');
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'aeromc-restore-')); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'aeromc-restore-'));
const extractDir = path.join(tmp, 'extract'); const extractDir = path.join(tmp, 'extract');
const rollbackDir = path.join(tmp, 'rollback'); const rollbackDir = path.join(tmp, 'rollback');
const rollbackInstances = path.join(rollbackDir, 'instances'); const rollbackInstances = path.join(rollbackDir, 'instances');
const settingsFile = path.join(userDataDir, 'settings.json'); const settingsFile = path.join(userDataDir, 'settings.json');
const rollbackSettings = path.join(rollbackDir, 'settings.json'); const rollbackSettings = path.join(rollbackDir, 'settings.json');
let settingsPreviouslyExisted = false; let settingsPreviouslyExisted = false;
// wird erst true, sobald der aktuelle Stand tatsächlich weggesichert wurde -
try { // vorher darf der Rollback nichts anfassen, sonst löscht ein früher Fehler
ensureDir(extractDir); // (z. B. ungültiges Archiv) den unveränderten, intakten Originalbestand
emitProgress(onProgress, 'restore', 5, 'Entpacke Backup'); let originalMovedAway = false;
await extractBackupArchive(sourcePath, extractDir, {
mode: 'restore', try {
onProgress, ensureDir(extractDir);
startPercent: 5, emitProgress(onProgress, 'restore', 5, 'Entpacke Backup');
endPercent: 18, await extractBackupArchive(sourcePath, extractDir, {
detail: 'Entpacke Backup', mode: 'restore',
}); onProgress,
startPercent: 5,
const backupRoot = resolveExtractedBackupRoot(extractDir); endPercent: 18,
if (!backupRoot) throw new Error('Das Archiv enthält kein gültiges AeroMC-Backup.'); detail: 'Entpacke Backup',
emitProgress(onProgress, 'restore', 18, 'Backup geprüft'); });
const sourceInstances = path.join(backupRoot, 'instances'); const backupRoot = resolveExtractedBackupRoot(extractDir);
const sourceSettings = path.join(backupRoot, 'settings.json'); if (!backupRoot) throw new Error('Das Archiv enthält kein gültiges AeroMC-Backup.');
ensureDir(rollbackDir); emitProgress(onProgress, 'restore', 18, 'Backup geprüft');
emitProgress(onProgress, 'restore', 25, 'Sichere aktuellen Stand'); const sourceInstances = path.join(backupRoot, 'instances');
moveDirTracked(instancesDir, rollbackInstances, { const sourceSettings = path.join(backupRoot, 'settings.json');
mode: 'restore', ensureDir(rollbackDir);
onProgress,
startPercent: 28, emitProgress(onProgress, 'restore', 25, 'Sichere aktuellen Stand');
endPercent: 40, moveDirTracked(instancesDir, rollbackInstances, {
detailPrefix: 'Sichere aktuelle Instanzen', mode: 'restore',
}); onProgress,
settingsPreviouslyExisted = fs.existsSync(settingsFile); startPercent: 28,
if (settingsPreviouslyExisted) fs.copyFileSync(settingsFile, rollbackSettings); endPercent: 40,
detailPrefix: 'Sichere aktuelle Instanzen',
ensureDir(path.dirname(instancesDir)); });
moveDirTracked(sourceInstances, instancesDir, { originalMovedAway = true;
mode: 'restore', settingsPreviouslyExisted = fs.existsSync(settingsFile);
onProgress, if (settingsPreviouslyExisted) fs.copyFileSync(settingsFile, rollbackSettings);
startPercent: 45,
endPercent: 78, ensureDir(path.dirname(instancesDir));
detailPrefix: 'Stelle Instanzen wieder her', moveDirTracked(sourceInstances, instancesDir, {
}); mode: 'restore',
onProgress,
let restoredSettings = false; startPercent: 45,
if (includeSettings && fs.existsSync(sourceSettings)) { endPercent: 78,
const restored = readJson(sourceSettings, {}); detailPrefix: 'Stelle Instanzen wieder her',
restored.instancesDir = instancesDirSetting; });
writeJson(settingsFile, restored);
restoredSettings = true; let restoredSettings = false;
emitProgress(onProgress, 'restore', 90, 'Stelle Einstellungen wieder her'); if (includeSettings && fs.existsSync(sourceSettings)) {
} else { const restored = readJson(sourceSettings, {});
emitProgress(onProgress, 'restore', 90, 'Einstellungen übersprungen'); restored.instancesDir = instancesDirSetting;
} writeJson(settingsFile, restored);
restoredSettings = true;
emitProgress(onProgress, 'restore', 100, 'Wiederherstellen abgeschlossen'); emitProgress(onProgress, 'restore', 90, 'Stelle Einstellungen wieder her');
} else {
return { emitProgress(onProgress, 'restore', 90, 'Einstellungen übersprungen');
ok: true, }
path: sourcePath,
instanceCount: countInstanceDirs(instancesDir), emitProgress(onProgress, 'restore', 100, 'Wiederherstellen abgeschlossen');
restoredSettings,
}; return {
} catch (err) { ok: true,
try { path: sourcePath,
if (fs.existsSync(instancesDir)) fs.rmSync(instancesDir, { recursive: true, force: true }); instanceCount: countInstanceDirs(instancesDir),
if (fs.existsSync(rollbackInstances)) fs.cpSync(rollbackInstances, instancesDir, { recursive: true }); restoredSettings,
else ensureDir(instancesDir); };
} catch (err) {
if (includeSettings) { // Nur zurückrollen, wenn wir den ursprünglichen Bestand überhaupt schon
if (fs.existsSync(rollbackSettings)) fs.copyFileSync(rollbackSettings, settingsFile); // angefasst haben (siehe originalMovedAway oben). Schlägt die
else if (!settingsPreviouslyExisted && fs.existsSync(settingsFile)) fs.rmSync(settingsFile, { force: true }); // Wiederherstellung vorher fehl (ungültiges/beschädigtes Archiv o. Ä.),
} // ist instancesDir noch der unveränderte Originalbestand - den lassen wir
} catch { /* ignore rollback errors */ } // dann bewusst in Ruhe, statt ihn zu löschen.
throw err; if (originalMovedAway) {
} finally { try {
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ } if (fs.existsSync(instancesDir)) fs.rmSync(instancesDir, { recursive: true, force: true });
} if (fs.existsSync(rollbackInstances)) fs.cpSync(rollbackInstances, instancesDir, { recursive: true });
} else ensureDir(instancesDir);
module.exports = { if (includeSettings) {
createGlobalBackup, if (fs.existsSync(rollbackSettings)) fs.copyFileSync(rollbackSettings, settingsFile);
restoreGlobalBackup, else if (!settingsPreviouslyExisted && fs.existsSync(settingsFile)) fs.rmSync(settingsFile, { force: true });
defaultBackupFileName, }
defaultBackupDir, } catch { /* ignore rollback errors */ }
listGlobalBackups, }
resolveAutoBackupPath, throw err;
} finally {
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ }
}
}
module.exports = {
createGlobalBackup,
restoreGlobalBackup,
defaultBackupFileName,
defaultBackupDir,
listGlobalBackups,
resolveAutoBackupPath,
}; };
+195 -195
View File
@@ -1,196 +1,196 @@
'use strict'; 'use strict';
/* /*
* cfimport.js Instanzen aus dem CurseForge-Launcher übernehmen * cfimport.js Instanzen aus dem CurseForge-Launcher übernehmen
* --------------------------------------------------------------- * ---------------------------------------------------------------
* Unterstützt sowohl den Launcher-Wurzelordner als auch direkt den * Unterstützt sowohl den Launcher-Wurzelordner als auch direkt den
* Instances-Ordner. Gelesen werden nach Möglichkeit minecraftinstance.json * Instances-Ordner. Gelesen werden nach Möglichkeit minecraftinstance.json
* und die vorhandenen Spieldaten im Profilordner. * und die vorhandenen Spieldaten im Profilordner.
*/ */
const fs = require('fs'); const fs = require('fs');
const os = require('os'); const os = require('os');
const path = require('path'); const path = require('path');
const META_FILES = ['minecraftinstance.json', 'instance.json']; const META_FILES = ['minecraftinstance.json', 'instance.json'];
function readJson(file, fallback) { function readJson(file, fallback) {
try { try {
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
} catch { } catch {
return fallback; return fallback;
} }
} }
function existingMetaFile(dir) { function existingMetaFile(dir) {
return META_FILES.map((name) => path.join(dir, name)).find((file) => fs.existsSync(file)) || null; return META_FILES.map((name) => path.join(dir, name)).find((file) => fs.existsSync(file)) || null;
} }
function metaVersion(meta) { function metaVersion(meta) {
return String( return String(
(meta && ( (meta && (
meta.gameVersion || meta.minecraftVersion || meta.mcVersion || meta.gameVersion || meta.minecraftVersion || meta.mcVersion ||
(meta.installedModpack && meta.installedModpack.gameVersion) (meta.installedModpack && meta.installedModpack.gameVersion)
)) || '' )) || ''
).trim(); ).trim();
} }
function resolveInstancesDir(inputPath) { function resolveInstancesDir(inputPath) {
if (!inputPath || !fs.existsSync(inputPath)) return null; if (!inputPath || !fs.existsSync(inputPath)) return null;
const directNames = new Set(['instances', 'minecraftinstances']); const directNames = new Set(['instances', 'minecraftinstances']);
if (directNames.has(path.basename(inputPath).toLowerCase())) return inputPath; if (directNames.has(path.basename(inputPath).toLowerCase())) return inputPath;
const candidates = [ const candidates = [
path.join(inputPath, 'Instances'), path.join(inputPath, 'Instances'),
path.join(inputPath, 'instances'), path.join(inputPath, 'instances'),
path.join(inputPath, 'minecraftInstances'), path.join(inputPath, 'minecraftInstances'),
path.join(inputPath, 'Minecraft', 'Instances'), path.join(inputPath, 'Minecraft', 'Instances'),
path.join(inputPath, 'minecraft', 'Instances'), path.join(inputPath, 'minecraft', 'Instances'),
]; ];
const looksLikeWindowsInstall = const looksLikeWindowsInstall =
/curseforge windows/i.test(inputPath) || /curseforge windows/i.test(inputPath) ||
(fs.existsSync(path.join(inputPath, 'CurseForge.exe')) && fs.existsSync(path.join(inputPath, 'resources'))); (fs.existsSync(path.join(inputPath, 'CurseForge.exe')) && fs.existsSync(path.join(inputPath, 'resources')));
if (looksLikeWindowsInstall) { if (looksLikeWindowsInstall) {
candidates.push( candidates.push(
path.join(os.homedir(), 'curseforge', 'minecraft', 'Instances'), path.join(os.homedir(), 'curseforge', 'minecraft', 'Instances'),
path.join(os.homedir(), 'CurseForge', 'minecraft', 'Instances'), path.join(os.homedir(), 'CurseForge', 'minecraft', 'Instances'),
); );
} }
return candidates.find((candidate) => fs.existsSync(candidate)) || null; return candidates.find((candidate) => fs.existsSync(candidate)) || null;
} }
function normalizeLoaderVersion(loader, rawValue, mcVersion) { function normalizeLoaderVersion(loader, rawValue, mcVersion) {
let value = String(rawValue || '').trim(); let value = String(rawValue || '').trim();
if (!value) return ''; if (!value) return '';
if (loader === 'forge') { if (loader === 'forge') {
value = value.replace(/^forge-/i, ''); value = value.replace(/^forge-/i, '');
if (mcVersion && value.startsWith(mcVersion + '-')) value = value.slice(mcVersion.length + 1); if (mcVersion && value.startsWith(mcVersion + '-')) value = value.slice(mcVersion.length + 1);
return value; return value;
} }
if (loader === 'neoforge') { if (loader === 'neoforge') {
return value.replace(/^neoforge-/i, ''); return value.replace(/^neoforge-/i, '');
} }
if (loader === 'fabric') { if (loader === 'fabric') {
value = value.replace(/^fabric(?:-loader)?-/i, ''); value = value.replace(/^fabric(?:-loader)?-/i, '');
if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1)); if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1));
return value; return value;
} }
if (loader === 'quilt') { if (loader === 'quilt') {
value = value.replace(/^quilt(?:-loader)?-/i, ''); value = value.replace(/^quilt(?:-loader)?-/i, '');
if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1)); if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1));
return value; return value;
} }
return value; return value;
} }
function detectLoader(meta, mcVersion) { function detectLoader(meta, mcVersion) {
const raw = meta && ( const raw = meta && (
meta.baseModLoader || meta.modLoader || meta.modloader || meta.loader || meta.modLoaderId meta.baseModLoader || meta.modLoader || meta.modloader || meta.loader || meta.modLoaderId
); );
const value = typeof raw === 'object' const value = typeof raw === 'object'
? (raw.name || raw.id || raw.value || raw.version || '') ? (raw.name || raw.id || raw.value || raw.version || '')
: (raw || ''); : (raw || '');
const lower = String(value).toLowerCase(); const lower = String(value).toLowerCase();
const objectVersion = raw && typeof raw === 'object' const objectVersion = raw && typeof raw === 'object'
? (raw.version || raw.name || raw.id || '') ? (raw.version || raw.name || raw.id || '')
: ''; : '';
if (!lower) return { loader: 'vanilla', loaderVersion: '' }; if (!lower) return { loader: 'vanilla', loaderVersion: '' };
if (lower.includes('neoforge')) return { loader: 'neoforge', loaderVersion: normalizeLoaderVersion('neoforge', objectVersion || value, mcVersion) }; 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('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('quilt')) return { loader: 'quilt', loaderVersion: normalizeLoaderVersion('quilt', objectVersion || value, mcVersion) };
if (lower.includes('forge')) return { loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', objectVersion || value, mcVersion) }; if (lower.includes('forge')) return { loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', objectVersion || value, mcVersion) };
return { loader: 'vanilla', loaderVersion: '' }; return { loader: 'vanilla', loaderVersion: '' };
} }
function detectVersion(meta, loaderVersion) { function detectVersion(meta, loaderVersion) {
return metaVersion(meta) || (loaderVersion && loaderVersion.includes('-') ? loaderVersion.split('-')[0] : ''); return metaVersion(meta) || (loaderVersion && loaderVersion.includes('-') ? loaderVersion.split('-')[0] : '');
} }
function findGameDir(dir) { function findGameDir(dir) {
const nested = ['minecraft', '.minecraft'] const nested = ['minecraft', '.minecraft']
.map((name) => path.join(dir, name)) .map((name) => path.join(dir, name))
.find((candidate) => fs.existsSync(candidate)); .find((candidate) => fs.existsSync(candidate));
if (nested) return nested; if (nested) return nested;
const markers = ['mods', 'config', 'resourcepacks', 'shaderpacks', 'saves', 'options.txt']; const markers = ['mods', 'config', 'resourcepacks', 'shaderpacks', 'saves', 'options.txt'];
return markers.some((name) => fs.existsSync(path.join(dir, name))) ? dir : null; return markers.some((name) => fs.existsSync(path.join(dir, name))) ? dir : null;
} }
function scan(inputPath) { function scan(inputPath) {
const instancesDir = resolveInstancesDir(inputPath); const instancesDir = resolveInstancesDir(inputPath);
if (!instancesDir) return { ok: false, reason: 'not-found' }; if (!instancesDir) return { ok: false, reason: 'not-found' };
const list = []; const list = [];
for (const entry of fs.readdirSync(instancesDir, { withFileTypes: true })) { for (const entry of fs.readdirSync(instancesDir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue; if (!entry.isDirectory()) continue;
const dir = path.join(instancesDir, entry.name); const dir = path.join(instancesDir, entry.name);
const metaFile = existingMetaFile(dir); const metaFile = existingMetaFile(dir);
const meta = metaFile ? readJson(metaFile, {}) : {}; const meta = metaFile ? readJson(metaFile, {}) : {};
const gameDir = findGameDir(dir); const gameDir = findGameDir(dir);
if (!metaFile && !gameDir) continue; if (!metaFile && !gameDir) continue;
const versionHint = metaVersion(meta); const versionHint = metaVersion(meta);
const loaderInfo = detectLoader(meta, versionHint); const loaderInfo = detectLoader(meta, versionHint);
list.push({ list.push({
folder: entry.name, folder: entry.name,
dir, dir,
name: meta.name || meta.displayName || entry.name, name: meta.name || meta.displayName || entry.name,
group: '', group: '',
notes: meta.notes || meta.summary || '', notes: meta.notes || meta.summary || '',
version: versionHint || detectVersion(meta, loaderInfo.loaderVersion), version: versionHint || detectVersion(meta, loaderInfo.loaderVersion),
loader: loaderInfo.loader, loader: loaderInfo.loader,
loaderVersion: loaderInfo.loaderVersion, loaderVersion: loaderInfo.loaderVersion,
javaPath: String(meta.javaPath || meta.javaExecutable || '').trim(), javaPath: String(meta.javaPath || meta.javaExecutable || '').trim(),
minMemMb: Number(meta.minimumMemory || meta.minMemory || meta.minMemAlloc) || null, minMemMb: Number(meta.minimumMemory || meta.minMemory || meta.minMemAlloc) || null,
maxMemMb: Number(meta.maximumMemory || meta.maxMemory || meta.maxMemAlloc || meta.allocatedMemory) || null, maxMemMb: Number(meta.maximumMemory || meta.maxMemory || meta.maxMemAlloc || meta.allocatedMemory) || null,
gameDir: gameDir || dir, gameDir: gameDir || dir,
hasGameData: !!gameDir, hasGameData: !!gameDir,
}); });
} }
list.sort((a, b) => a.name.localeCompare(b.name)); list.sort((a, b) => a.name.localeCompare(b.name));
return { ok: true, instancesDir, count: list.length, instances: list }; return { ok: true, instancesDir, count: list.length, instances: list };
} }
function copyInto(sourceDir, targetDir) { function copyInto(sourceDir, targetDir) {
fs.mkdirSync(targetDir, { recursive: true }); fs.mkdirSync(targetDir, { recursive: true });
for (const name of fs.readdirSync(sourceDir)) { for (const name of fs.readdirSync(sourceDir)) {
if (META_FILES.includes(name)) continue; if (META_FILES.includes(name)) continue;
fs.cpSync(path.join(sourceDir, name), path.join(targetDir, name), { recursive: true }); fs.cpSync(path.join(sourceDir, name), path.join(targetDir, name), { recursive: true });
} }
} }
function importOne(store, entry, copyData = true) { function importOne(store, entry, copyData = true) {
const created = store.createOrReplaceImportedInstance({ const created = store.createOrReplaceImportedInstance({
name: entry.name, name: entry.name,
group: entry.group, group: entry.group,
notes: entry.notes, notes: entry.notes,
minecraft: { minecraft: {
version: entry.version, version: entry.version,
loader: entry.loader, loader: entry.loader,
loaderVersion: entry.loaderVersion, loaderVersion: entry.loaderVersion,
}, },
}); });
const patch = {}; const patch = {};
if (entry.javaPath || entry.minMemMb || entry.maxMemMb) { if (entry.javaPath || entry.minMemMb || entry.maxMemMb) {
patch.java = { patch.java = {
path: entry.javaPath || '', path: entry.javaPath || '',
minMemMb: entry.minMemMb || null, minMemMb: entry.minMemMb || null,
maxMemMb: entry.maxMemMb || null, maxMemMb: entry.maxMemMb || null,
extraArgs: '', extraArgs: '',
}; };
} }
if (Object.keys(patch).length) store.updateInstance(created.id, patch); if (Object.keys(patch).length) store.updateInstance(created.id, patch);
let copied = false; let copied = false;
if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) { if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) {
copyInto(entry.gameDir, store.gameDir(created.id)); copyInto(entry.gameDir, store.gameDir(created.id));
copied = true; copied = true;
} }
return { id: created.id, name: created.name, copied }; return { id: created.id, name: created.name, copied };
} }
module.exports = { scan, importOne, resolveInstancesDir }; module.exports = { scan, importOne, resolveInstancesDir };
+10 -1
View File
@@ -269,7 +269,16 @@ function destroyTray() {
// Windows-Autostart-Eintrag setzen/entfernen + Tray-Symbol passend ein-/ausblenden // Windows-Autostart-Eintrag setzen/entfernen + Tray-Symbol passend ein-/ausblenden
function applyAutostart(enabled) { function applyAutostart(enabled) {
try { try {
app.setLoginItemSettings({ openAtLogin: !!enabled, args: enabled ? ['--hidden'] : [] }); // Gleiches Problem wie bei den Desktop-Verknüpfungen (siehe verknuepfungsArgs
// weiter oben): im Entwicklungsmodus zeigt process.execPath nur auf die nackte
// electron.exe, die ohne Pfadangabe ihren eigenen leeren Startbildschirm zeigt
// statt AeroMC zu laden. Zusätzlich: bei der Portable-Fassung zeigt
// process.execPath auf den entpackten Temp-Ordner der wäre morgen tot,
// deshalb wie bei den Verknüpfungen über verknuepfungsZiel() auflösen.
const args = enabled
? (app.isPackaged ? ['--hidden'] : [path.join(__dirname, '..'), '--hidden'])
: [];
app.setLoginItemSettings({ openAtLogin: !!enabled, path: verknuepfungsZiel(), args });
} catch { /* z. B. in portabler Umgebung ohne Schreibrechte auf die Registry */ } } catch { /* z. B. in portabler Umgebung ohne Schreibrechte auf die Registry */ }
if (enabled) createTray(); if (enabled) createTray();
else destroyTray(); else destroyTray();
+216 -216
View File
@@ -1,217 +1,217 @@
'use strict'; 'use strict';
const fs = require('fs'); const fs = require('fs');
const os = require('os'); const os = require('os');
const path = require('path'); const path = require('path');
const PROFILE_FILES = [ const PROFILE_FILES = [
'launcher_profiles.json', 'launcher_profiles.json',
'launcher_profiles_microsoft_store.json', 'launcher_profiles_microsoft_store.json',
'launcher_profiles_microsoft_store_2.json', 'launcher_profiles_microsoft_store_2.json',
]; ];
const ROOT_COPY_DIRS = new Set([ const ROOT_COPY_DIRS = new Set([
'config', 'defaultconfigs', 'kubejs', 'mods', 'resourcepacks', 'screenshots', 'shaderpacks', 'saves', 'config', 'defaultconfigs', 'kubejs', 'mods', 'resourcepacks', 'screenshots', 'shaderpacks', 'saves',
]); ]);
const ROOT_COPY_FILE_PATTERNS = [ const ROOT_COPY_FILE_PATTERNS = [
/^options.*\.(txt|of)$/i, /^options.*\.(txt|of)$/i,
/^servers\.dat(?:_old)?$/i, /^servers\.dat(?:_old)?$/i,
/^usercache\.json$/i, /^usercache\.json$/i,
/^tl_skin_cape\.json$/i, /^tl_skin_cape\.json$/i,
/^journeymap.*\.(json|txt|cfg)$/i, /^journeymap.*\.(json|txt|cfg)$/i,
]; ];
function readJson(file, fallback) { function readJson(file, fallback) {
try { try {
return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, '')); return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^\uFEFF/, ''));
} catch { } catch {
return fallback; return fallback;
} }
} }
function resolveLauncherRoot(inputPath) { function resolveLauncherRoot(inputPath) {
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'); const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
const defaultRoot = path.join(appData, '.minecraft'); const defaultRoot = path.join(appData, '.minecraft');
const candidates = []; const candidates = [];
if (inputPath && fs.existsSync(inputPath)) candidates.push(inputPath); if (inputPath && fs.existsSync(inputPath)) candidates.push(inputPath);
candidates.push(defaultRoot); candidates.push(defaultRoot);
for (const candidate of candidates) { for (const candidate of candidates) {
for (const profileFile of PROFILE_FILES) { for (const profileFile of PROFILE_FILES) {
if (fs.existsSync(path.join(candidate, profileFile))) return candidate; if (fs.existsSync(path.join(candidate, profileFile))) return candidate;
} }
} }
return null; return null;
} }
function resolveProfileFile(rootDir) { function resolveProfileFile(rootDir) {
return PROFILE_FILES.map((name) => path.join(rootDir, name)).find((file) => fs.existsSync(file)) || null; return PROFILE_FILES.map((name) => path.join(rootDir, name)).find((file) => fs.existsSync(file)) || null;
} }
function normalizeLoaderVersion(loader, rawValue, mcVersion) { function normalizeLoaderVersion(loader, rawValue, mcVersion) {
let value = String(rawValue || '').trim(); let value = String(rawValue || '').trim();
if (!value) return ''; if (!value) return '';
if (loader === 'forge') { if (loader === 'forge') {
value = value.replace(/^forge-/i, ''); value = value.replace(/^forge-/i, '');
if (mcVersion && value.startsWith(mcVersion + '-')) value = value.slice(mcVersion.length + 1); if (mcVersion && value.startsWith(mcVersion + '-')) value = value.slice(mcVersion.length + 1);
return value; return value;
} }
if (loader === 'neoforge') return value.replace(/^neoforge-/i, ''); if (loader === 'neoforge') return value.replace(/^neoforge-/i, '');
if (loader === 'fabric') { if (loader === 'fabric') {
value = value.replace(/^fabric(?:-loader)?-/i, ''); value = value.replace(/^fabric(?:-loader)?-/i, '');
if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1)); if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1));
return value; return value;
} }
if (loader === 'quilt') { if (loader === 'quilt') {
value = value.replace(/^quilt(?:-loader)?-/i, ''); value = value.replace(/^quilt(?:-loader)?-/i, '');
if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1)); if (mcVersion && value.endsWith('-' + mcVersion)) value = value.slice(0, -1 * (mcVersion.length + 1));
return value; return value;
} }
return value; return value;
} }
function parseLastVersionId(lastVersionId) { function parseLastVersionId(lastVersionId) {
const raw = String(lastVersionId || '').trim(); const raw = String(lastVersionId || '').trim();
if (!raw) return { version: '', loader: 'vanilla', loaderVersion: '' }; if (!raw) return { version: '', loader: 'vanilla', loaderVersion: '' };
let match = raw.match(/^fabric-loader-([^-]+(?:\.[^-]+)*)-(.+)$/i); let match = raw.match(/^fabric-loader-([^-]+(?:\.[^-]+)*)-(.+)$/i);
if (match) return { version: match[2], loader: 'fabric', loaderVersion: normalizeLoaderVersion('fabric', match[1], match[2]) }; if (match) return { version: match[2], loader: 'fabric', loaderVersion: normalizeLoaderVersion('fabric', match[1], match[2]) };
match = raw.match(/^quilt-loader-([^-]+(?:\.[^-]+)*)-(.+)$/i); match = raw.match(/^quilt-loader-([^-]+(?:\.[^-]+)*)-(.+)$/i);
if (match) return { version: match[2], loader: 'quilt', loaderVersion: normalizeLoaderVersion('quilt', match[1], match[2]) }; if (match) return { version: match[2], loader: 'quilt', loaderVersion: normalizeLoaderVersion('quilt', match[1], match[2]) };
match = raw.match(/^(.+)-forge-([\w.-]+)$/i); match = raw.match(/^(.+)-forge-([\w.-]+)$/i);
if (match) return { version: match[1], loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', match[2], match[1]) }; if (match) return { version: match[1], loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', match[2], match[1]) };
match = raw.match(/^forge-(.+)-([\w.-]+)$/i); match = raw.match(/^forge-(.+)-([\w.-]+)$/i);
if (match) return { version: match[1], loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', match[2], match[1]) }; if (match) return { version: match[1], loader: 'forge', loaderVersion: normalizeLoaderVersion('forge', match[2], match[1]) };
match = raw.match(/^(.+)-neoforge-([\w.-]+)$/i); match = raw.match(/^(.+)-neoforge-([\w.-]+)$/i);
if (match) return { version: match[1], loader: 'neoforge', loaderVersion: normalizeLoaderVersion('neoforge', match[2], match[1]) }; 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: '' }; if (/^\d+(?:\.\d+)+(?:-[\w.]+)?$/i.test(raw)) return { version: raw, loader: 'vanilla', loaderVersion: '' };
return { version: raw, loader: 'vanilla', loaderVersion: '' }; return { version: raw, loader: 'vanilla', loaderVersion: '' };
} }
function parseJavaSettings(javaDir, javaArgs) { function parseJavaSettings(javaDir, javaArgs) {
const args = String(javaArgs || '').trim(); const args = String(javaArgs || '').trim();
const minMatch = args.match(/(?:^|\s)-Xms(\d+)([mMgG])/); const minMatch = args.match(/(?:^|\s)-Xms(\d+)([mMgG])/);
const maxMatch = args.match(/(?:^|\s)-Xmx(\d+)([mMgG])/); const maxMatch = args.match(/(?:^|\s)-Xmx(\d+)([mMgG])/);
const toMb = (match) => { const toMb = (match) => {
if (!match) return null; if (!match) return null;
const num = Number(match[1]); const num = Number(match[1]);
if (!Number.isFinite(num)) return null; if (!Number.isFinite(num)) return null;
return match[2].toLowerCase() === 'g' ? num * 1024 : num; return match[2].toLowerCase() === 'g' ? num * 1024 : num;
}; };
return { return {
path: String(javaDir || '').trim(), path: String(javaDir || '').trim(),
minMemMb: toMb(minMatch), minMemMb: toMb(minMatch),
maxMemMb: toMb(maxMatch), maxMemMb: toMb(maxMatch),
extraArgs: args extraArgs: args
.replace(/(?:^|\s)-Xms\d+[mMgG]/g, ' ') .replace(/(?:^|\s)-Xms\d+[mMgG]/g, ' ')
.replace(/(?:^|\s)-Xmx\d+[mMgG]/g, ' ') .replace(/(?:^|\s)-Xmx\d+[mMgG]/g, ' ')
.replace(/\s+/g, ' ') .replace(/\s+/g, ' ')
.trim(), .trim(),
}; };
} }
function isImportableProfile(profile) { function isImportableProfile(profile) {
const type = String((profile && profile.type) || '').trim().toLowerCase(); const type = String((profile && profile.type) || '').trim().toLowerCase();
if (type === 'latest-release' || type === 'latest-snapshot') return false; if (type === 'latest-release' || type === 'latest-snapshot') return false;
const name = String((profile && profile.name) || '').trim(); const name = String((profile && profile.name) || '').trim();
const lastVersionId = String((profile && profile.lastVersionId) || '').trim(); const lastVersionId = String((profile && profile.lastVersionId) || '').trim();
return !!(name || lastVersionId); return !!(name || lastVersionId);
} }
function shouldCopyRootEntry(name) { function shouldCopyRootEntry(name) {
if (ROOT_COPY_DIRS.has(name)) return true; if (ROOT_COPY_DIRS.has(name)) return true;
return ROOT_COPY_FILE_PATTERNS.some((pattern) => pattern.test(name)); return ROOT_COPY_FILE_PATTERNS.some((pattern) => pattern.test(name));
} }
function scan(inputPath) { function scan(inputPath) {
const rootDir = resolveLauncherRoot(inputPath); const rootDir = resolveLauncherRoot(inputPath);
if (!rootDir) return { ok: false, reason: 'not-found' }; if (!rootDir) return { ok: false, reason: 'not-found' };
const profileFile = resolveProfileFile(rootDir); const profileFile = resolveProfileFile(rootDir);
const json = readJson(profileFile, {}); const json = readJson(profileFile, {});
const profiles = json.profiles || {}; const profiles = json.profiles || {};
const list = []; const list = [];
const defaultRoot = path.normalize(rootDir).toLowerCase(); const defaultRoot = path.normalize(rootDir).toLowerCase();
for (const [id, profile] of Object.entries(profiles)) { for (const [id, profile] of Object.entries(profiles)) {
if (!isImportableProfile(profile)) continue; if (!isImportableProfile(profile)) continue;
const name = String(profile.name || '').trim() || String(profile.lastVersionId || '').trim() || id; const name = String(profile.name || '').trim() || String(profile.lastVersionId || '').trim() || id;
const parsed = parseLastVersionId(profile.lastVersionId); const parsed = parseLastVersionId(profile.lastVersionId);
const gameDir = path.normalize(String(profile.gameDir || rootDir)); const gameDir = path.normalize(String(profile.gameDir || rootDir));
const java = parseJavaSettings(profile.javaDir, profile.javaArgs); const java = parseJavaSettings(profile.javaDir, profile.javaArgs);
list.push({ list.push({
id, id,
name, name,
group: '', group: '',
notes: 'Importiert aus dem Minecraft Launcher', notes: 'Importiert aus dem Minecraft Launcher',
version: parsed.version, version: parsed.version,
loader: parsed.loader, loader: parsed.loader,
loaderVersion: parsed.loaderVersion, loaderVersion: parsed.loaderVersion,
javaPath: java.path, javaPath: java.path,
minMemMb: java.minMemMb, minMemMb: java.minMemMb,
maxMemMb: java.maxMemMb, maxMemMb: java.maxMemMb,
extraJavaArgs: java.extraArgs, extraJavaArgs: java.extraArgs,
gameDir, gameDir,
hasGameData: fs.existsSync(gameDir), hasGameData: fs.existsSync(gameDir),
usesDefaultGameDir: gameDir.toLowerCase() === defaultRoot, usesDefaultGameDir: gameDir.toLowerCase() === defaultRoot,
rootDir, rootDir,
}); });
} }
list.sort((a, b) => a.name.localeCompare(b.name)); list.sort((a, b) => a.name.localeCompare(b.name));
return { ok: true, rootDir, count: list.length, instances: list }; return { ok: true, rootDir, count: list.length, instances: list };
} }
function copyInto(sourceDir, targetDir, usesDefaultGameDir) { function copyInto(sourceDir, targetDir, usesDefaultGameDir) {
fs.mkdirSync(targetDir, { recursive: true }); fs.mkdirSync(targetDir, { recursive: true });
for (const name of fs.readdirSync(sourceDir)) { for (const name of fs.readdirSync(sourceDir)) {
if (/^launcher_profiles.*\.json$/i.test(name)) continue; if (/^launcher_profiles.*\.json$/i.test(name)) continue;
if (name === 'versions' || name === 'libraries' || name === 'assets' || name === 'runtime' || name === 'webcache2') continue; if (name === 'versions' || name === 'libraries' || name === 'assets' || name === 'runtime' || name === 'webcache2') continue;
if (usesDefaultGameDir && !shouldCopyRootEntry(name)) continue; if (usesDefaultGameDir && !shouldCopyRootEntry(name)) continue;
fs.cpSync(path.join(sourceDir, name), path.join(targetDir, name), { recursive: true }); fs.cpSync(path.join(sourceDir, name), path.join(targetDir, name), { recursive: true });
} }
} }
function importOne(store, entry, copyData = true) { function importOne(store, entry, copyData = true) {
const created = store.createOrReplaceImportedInstance({ const created = store.createOrReplaceImportedInstance({
name: entry.name, name: entry.name,
group: entry.group, group: entry.group,
notes: entry.notes, notes: entry.notes,
minecraft: { minecraft: {
version: entry.version, version: entry.version,
loader: entry.loader, loader: entry.loader,
loaderVersion: entry.loaderVersion, loaderVersion: entry.loaderVersion,
}, },
}); });
const patch = {}; const patch = {};
if (entry.javaPath || entry.minMemMb || entry.maxMemMb || entry.extraJavaArgs) { if (entry.javaPath || entry.minMemMb || entry.maxMemMb || entry.extraJavaArgs) {
patch.java = { patch.java = {
path: entry.javaPath || '', path: entry.javaPath || '',
minMemMb: entry.minMemMb || null, minMemMb: entry.minMemMb || null,
maxMemMb: entry.maxMemMb || null, maxMemMb: entry.maxMemMb || null,
extraArgs: entry.extraJavaArgs || '', extraArgs: entry.extraJavaArgs || '',
}; };
} }
if (Object.keys(patch).length) store.updateInstance(created.id, patch); if (Object.keys(patch).length) store.updateInstance(created.id, patch);
let copied = false; let copied = false;
if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) { if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) {
copyInto(entry.gameDir, store.gameDir(created.id), !!entry.usesDefaultGameDir); copyInto(entry.gameDir, store.gameDir(created.id), !!entry.usesDefaultGameDir);
copied = true; copied = true;
} }
return { id: created.id, name: created.name, copied }; return { id: created.id, name: created.name, copied };
} }
module.exports = { scan, importOne, parseLastVersionId, resolveLauncherRoot }; module.exports = { scan, importOne, parseLastVersionId, resolveLauncherRoot };
+101 -101
View File
@@ -1,101 +1,101 @@
'use strict'; 'use strict';
const DEFAULT_GRACE_MS = 15000; const DEFAULT_GRACE_MS = 15000;
const CLEAN_SHUTDOWN = /\[(?:Render thread|Client thread)\/INFO\](?:\s+\([^)]*\))?:\s*Stopping!\s*$/; const CLEAN_SHUTDOWN = /\[(?:Render thread|Client thread)\/INFO\](?:\s+\([^)]*\))?:\s*Stopping!\s*$/;
const FATAL_OUTPUT = [ const FATAL_OUTPUT = [
/---- Minecraft Crash Report ----/i, /---- Minecraft Crash Report ----/i,
/\b(?:Encountered an unexpected exception|Unreported exception thrown|Reported exception thrown)\b/i, /\b(?:Encountered an unexpected exception|Unreported exception thrown|Reported exception thrown)\b/i,
/\b(?:This crash report has been saved to|Saving crash report to)\b/i, /\b(?:This crash report has been saved to|Saving crash report to)\b/i,
/\bA fatal error has been detected by the Java Runtime Environment\b/i, /\bA fatal error has been detected by the Java Runtime Environment\b/i,
/Exception in thread "(?:Render thread|Client thread|main)"/i, /Exception in thread "(?:Render thread|Client thread|main)"/i,
/\[(?:Render thread|Client thread|main)\/FATAL\]/i, /\[(?:Render thread|Client thread|main)\/FATAL\]/i,
]; ];
function createProcessWatchdog(options) { function createProcessWatchdog(options) {
const graceMs = options.graceMs || DEFAULT_GRACE_MS; const graceMs = options.graceMs || DEFAULT_GRACE_MS;
const schedule = options.setTimer || setTimeout; const schedule = options.setTimer || setTimeout;
const cancel = options.clearTimer || clearTimeout; const cancel = options.clearTimer || clearTimeout;
const buffers = { stdout: '', stderr: '' }; const buffers = { stdout: '', stderr: '' };
let timer = null; let timer = null;
let armed = false; let armed = false;
let crashed = false; let crashed = false;
let ended = false; let ended = false;
let forced = false; let forced = false;
// bleibt true, sobald einmal ein regulärer Shutdown ("Stopping!") gesehen // bleibt true, sobald einmal ein regulärer Shutdown ("Stopping!") gesehen
// wurde im Gegensatz zu `armed` wird das NICHT durch markCrash() // wurde im Gegensatz zu `armed` wird das NICHT durch markCrash()
// zurückgesetzt, damit ein später vom internen Watchdog erzwungener Halt // zurückgesetzt, damit ein später vom internen Watchdog erzwungener Halt
// weiterhin als "war ein regulärer Shutdown" erkennbar bleibt // weiterhin als "war ein regulärer Shutdown" erkennbar bleibt
let sawCleanShutdown = false; let sawCleanShutdown = false;
function clear() { function clear() {
if (timer !== null) cancel(timer); if (timer !== null) cancel(timer);
timer = null; timer = null;
armed = false; armed = false;
} }
function markCrash() { function markCrash() {
crashed = true; crashed = true;
clear(); clear();
} }
function scheduleKill() { function scheduleKill() {
if (timer !== null) cancel(timer); if (timer !== null) cancel(timer);
timer = schedule(() => { timer = schedule(() => {
timer = null; timer = null;
if (ended || crashed || !armed || !options.isRunning()) return; if (ended || crashed || !armed || !options.isRunning()) return;
try { try {
forced = options.kill() === true; forced = options.kill() === true;
} catch { } catch {
forced = false; forced = false;
} }
if (forced && options.onForce) options.onForce(graceMs); if (forced && options.onForce) options.onForce(graceMs);
else if (!forced && options.isRunning() && options.onError) options.onError(); else if (!forced && options.isRunning() && options.onError) options.onError();
}, graceMs); }, graceMs);
if (timer && typeof timer.unref === 'function') timer.unref(); if (timer && typeof timer.unref === 'function') timer.unref();
} }
function inspectLine(line) { function inspectLine(line) {
const isCleanShutdown = CLEAN_SHUTDOWN.test(line); const isCleanShutdown = CLEAN_SHUTDOWN.test(line);
if (isCleanShutdown) sawCleanShutdown = true; if (isCleanShutdown) sawCleanShutdown = true;
if (FATAL_OUTPUT.some((pattern) => pattern.test(line))) { if (FATAL_OUTPUT.some((pattern) => pattern.test(line))) {
markCrash(); markCrash();
return; return;
} }
if (ended || crashed || armed || !isCleanShutdown) return; if (ended || crashed || armed || !isCleanShutdown) return;
armed = true; armed = true;
scheduleKill(); scheduleKill();
} }
function feed(chunk, stream) { function feed(chunk, stream) {
if (ended) return; if (ended) return;
const key = stream === 'stderr' ? 'stderr' : 'stdout'; const key = stream === 'stderr' ? 'stderr' : 'stdout';
const text = buffers[key] + String(chunk || ''); const text = buffers[key] + String(chunk || '');
const lines = text.split(/\r?\n/); const lines = text.split(/\r?\n/);
buffers[key] = lines.pop() || ''; buffers[key] = lines.pop() || '';
for (const line of lines) inspectLine(line); for (const line of lines) inspectLine(line);
if (armed && !crashed && !ended && lines.length) scheduleKill(); if (armed && !crashed && !ended && lines.length) scheduleKill();
} }
function end() { function end() {
ended = true; ended = true;
clear(); clear();
} }
return { return {
feed, feed,
end, end,
get armed() { return armed; }, get armed() { return armed; },
get crashed() { return crashed; }, get crashed() { return crashed; },
get forced() { return forced; }, get forced() { return forced; },
get sawCleanShutdown() { return sawCleanShutdown; }, get sawCleanShutdown() { return sawCleanShutdown; },
}; };
} }
module.exports = { module.exports = {
DEFAULT_GRACE_MS, DEFAULT_GRACE_MS,
CLEAN_SHUTDOWN, CLEAN_SHUTDOWN,
FATAL_OUTPUT, FATAL_OUTPUT,
createProcessWatchdog, createProcessWatchdog,
}; };