Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
425b94fd93 | ||
|
|
e0bdcb01fb | ||
|
|
c840a49f75 | ||
|
|
c535d8b830 | ||
|
|
248a387f01 | ||
|
|
87fded4cb6 |
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# beim Bauen erzeugt, enthält Adresse und Token der CurseForge-Weiterleitung
|
||||||
|
src/build-config.json
|
||||||
|
|
||||||
|
# Zugangsdaten des Weiterleitungs-Servers
|
||||||
|
server/curseforge-proxy/.env
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Generated
+3
-3
@@ -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",
|
||||||
|
|||||||
+3
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"productName": "AeroMC",
|
"productName": "AeroMC",
|
||||||
"version": "1.0.7",
|
"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",
|
||||||
@@ -23,7 +23,8 @@
|
|||||||
"compression": "store",
|
"compression": "store",
|
||||||
"files": [
|
"files": [
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
"package.json"
|
"package.json",
|
||||||
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"win": {
|
"win": {
|
||||||
"target": "nsis",
|
"target": "nsis",
|
||||||
|
|||||||
+451
-439
@@ -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
@@ -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 };
|
||||||
+44
-2
@@ -130,6 +130,7 @@
|
|||||||
<option value="forge">Forge</option>
|
<option value="forge">Forge</option>
|
||||||
<option value="neoforge">NeoForge</option>
|
<option value="neoforge">NeoForge</option>
|
||||||
<option value="quilt">Quilt</option>
|
<option value="quilt">Quilt</option>
|
||||||
|
<option value="optifine">OptiFine</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
@@ -139,6 +140,23 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="f-optifine-tools" class="field-row hidden" style="align-items:flex-start">
|
||||||
|
<div class="field" style="flex:1">
|
||||||
|
<span>OptiFine einrichten</span>
|
||||||
|
<p class="hint" style="margin:2px 0 8px">
|
||||||
|
OptiFine bietet kein automatisches Download-API (Downloads laufen bei OptiFine bewusst über eine Werbeseite).
|
||||||
|
Lade den Installer zuerst selbst von <b>optifine.net</b> herunter, wähle ihn dann hier aus – AeroMC bereitet
|
||||||
|
die passende Minecraft-Version vor und startet den echten OptiFine-Installer. Wähle darin als Zielordner den
|
||||||
|
automatisch in die Zwischenablage kopierten Pfad, klicke „Install" und bestätige danach hier.
|
||||||
|
</p>
|
||||||
|
<div class="path-row">
|
||||||
|
<button type="button" id="f-optifine-install" class="btn">Installer wählen und starten…</button>
|
||||||
|
<button type="button" id="f-optifine-confirm" class="btn btn-primary hidden">Fertig – Version übernehmen</button>
|
||||||
|
</div>
|
||||||
|
<small id="f-optifine-status" class="hint"></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Notizen <small class="opt">(optional)</small></span>
|
<span>Notizen <small class="opt">(optional)</small></span>
|
||||||
<textarea id="f-notes" rows="2" placeholder="Modliste, Server-IP, To-dos …"></textarea>
|
<textarea id="f-notes" rows="2" placeholder="Modliste, Server-IP, To-dos …"></textarea>
|
||||||
@@ -642,6 +660,7 @@
|
|||||||
<option value="mod">Mods</option>
|
<option value="mod">Mods</option>
|
||||||
<option value="resourcepack">Ressourcenpakete</option>
|
<option value="resourcepack">Ressourcenpakete</option>
|
||||||
<option value="shader">Shader</option>
|
<option value="shader">Shader</option>
|
||||||
|
<option value="datapack">Datenpakete</option>
|
||||||
</select>
|
</select>
|
||||||
<input id="m-query" type="text" placeholder="Mods durchsuchen … (z. B. Sodium, JEI, Create)" />
|
<input id="m-query" type="text" placeholder="Mods durchsuchen … (z. B. Sodium, JEI, Create)" />
|
||||||
<button id="m-search-btn" class="btn btn-primary">Suchen</button>
|
<button id="m-search-btn" class="btn btn-primary">Suchen</button>
|
||||||
@@ -966,6 +985,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===================== Dialog: Lizenz ===================== -->
|
||||||
|
<div id="modal-license" class="modal hidden">
|
||||||
|
<div class="modal-card modal-lg">
|
||||||
|
<div class="modal-head">
|
||||||
|
<h3>Lizenz</h3>
|
||||||
|
<button class="modal-close" data-close>✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<pre id="license-text" class="crash-voll">Lade …</pre>
|
||||||
|
</div>
|
||||||
|
<div class="modal-foot">
|
||||||
|
<button class="btn" data-close>Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ===================== Dialog: Update ===================== -->
|
<!-- ===================== Dialog: Update ===================== -->
|
||||||
<div id="modal-update" class="modal hidden">
|
<div id="modal-update" class="modal hidden">
|
||||||
<div class="modal-card modal-sm">
|
<div class="modal-card modal-sm">
|
||||||
@@ -1019,11 +1054,18 @@
|
|||||||
|
|
||||||
<div id="import-pane-modpack" class="import-pane hidden">
|
<div id="import-pane-modpack" class="import-pane hidden">
|
||||||
<p class="import-copy">Modpack aus dem Katalog installieren oder als Datei importieren.</p>
|
<p class="import-copy">Modpack aus dem Katalog installieren oder als Datei importieren.</p>
|
||||||
<p class="hint">Suche läuft über Modrinth. Datei-Import unterstützt zusätzlich CurseForge-ZIP (manifest.json) – dafür ist ein API-Key oder eine Weiterleitung in den Netzwerk-Einstellungen nötig.</p>
|
<p class="hint">Katalog-Suche läuft über Modrinth oder CurseForge. Für CurseForge wird ein eigener API-Key in den Netzwerk-Einstellungen gebraucht (die Weiterleitung reicht nur für den Datei-Import). Datei-Import unterstützt zusätzlich CurseForge-ZIP (manifest.json).</p>
|
||||||
|
|
||||||
<div class="field-row" style="margin-top:12px;align-items:flex-end">
|
<div class="field-row" style="margin-top:12px;align-items:flex-end">
|
||||||
|
<label class="field" style="width:150px">
|
||||||
|
<span>Quelle</span>
|
||||||
|
<select id="mp-source">
|
||||||
|
<option value="modrinth">Modrinth</option>
|
||||||
|
<option value="curseforge">CurseForge</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
<label class="field" style="flex:1">
|
<label class="field" style="flex:1">
|
||||||
<span>Suche (Modrinth)</span>
|
<span>Suche</span>
|
||||||
<input id="mp-query" type="text" placeholder="z. B. Fabulously Optimized, Create …" autocomplete="off" />
|
<input id="mp-query" type="text" placeholder="z. B. Fabulously Optimized, Create …" autocomplete="off" />
|
||||||
</label>
|
</label>
|
||||||
<label class="field" style="width:140px">
|
<label class="field" style="width:140px">
|
||||||
|
|||||||
@@ -464,6 +464,37 @@ async function prepareAndLaunch(opts, onProgress) {
|
|||||||
classpath.unshift(path.join(libDir, rel.replace(/\//g, path.sep)));
|
classpath.unshift(path.join(libDir, rel.replace(/\//g, path.sep)));
|
||||||
}
|
}
|
||||||
effectiveVj = mergeLoader(vj, fvj);
|
effectiveVj = mergeLoader(vj, fvj);
|
||||||
|
|
||||||
|
} else if (opts.loader === 'optifine') {
|
||||||
|
/*
|
||||||
|
* OptiFine hat keine öffentliche Installations-API (Downloads laufen bewusst
|
||||||
|
* über eine Werbeseite). AeroMC lädt hier nichts selbst herunter – der Nutzer
|
||||||
|
* hat den offiziellen Installer bereits einmal sichtbar laufen lassen (siehe
|
||||||
|
* optifine:setup/optifine:confirm in main.js), der dabei ein normales
|
||||||
|
* Versions-Profil unter <sharedDir>/versions/<loaderVersion>/ abgelegt hat,
|
||||||
|
* genau wie Fabric/Forge es tun. Wir lesen es nur noch ein und mergen es.
|
||||||
|
*/
|
||||||
|
const ofId = opts.loaderVersion;
|
||||||
|
if (!ofId) {
|
||||||
|
return { ok: false, message: 'Für diese Instanz ist keine OptiFine-Version hinterlegt. Bitte im Bearbeiten-Dialog einrichten.' };
|
||||||
|
}
|
||||||
|
const ofJsonPath = path.join(opts.sharedDir, 'versions', ofId, ofId + '.json');
|
||||||
|
if (!fs.existsSync(ofJsonPath)) {
|
||||||
|
return { ok: false, message: `OptiFine-Profil "${ofId}" wurde nicht gefunden. Bitte die Einrichtung im Bearbeiten-Dialog wiederholen.` };
|
||||||
|
}
|
||||||
|
let ofvj;
|
||||||
|
try { ofvj = JSON.parse(fs.readFileSync(ofJsonPath, 'utf8')); }
|
||||||
|
catch (err) { return { ok: false, message: 'OptiFine-Profil ist beschädigt: ' + err.message }; }
|
||||||
|
for (const lib of ofvj.libraries || []) {
|
||||||
|
const art = lib.downloads && lib.downloads.artifact;
|
||||||
|
const rel = (art && art.path) || forge.mavenPath(lib.name);
|
||||||
|
const dest = path.join(libDir, rel.replace(/\//g, path.sep));
|
||||||
|
if (!fs.existsSync(dest) && art && art.url) {
|
||||||
|
await downloadIfNeeded(art.url, dest, art.sha1 || null);
|
||||||
|
}
|
||||||
|
classpath.unshift(dest);
|
||||||
|
}
|
||||||
|
effectiveVj = mergeLoader(vj, ofvj);
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalClasspath = dedupeClasspath(classpath, opts.sharedDir);
|
const finalClasspath = dedupeClasspath(classpath, opts.sharedDir);
|
||||||
|
|||||||
+179
-4
@@ -6,11 +6,11 @@
|
|||||||
* (Vanilla-Start, Microsoft-Login und Mod-Loader folgen in späteren Phasen.)
|
* (Vanilla-Start, Microsoft-Login und Mod-Loader folgen in späteren Phasen.)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { app, BrowserWindow, ipcMain, dialog, shell, safeStorage, Notification, Tray, Menu, nativeImage } = require('electron');
|
const { app, BrowserWindow, ipcMain, dialog, shell, safeStorage, Notification, Tray, Menu, nativeImage, clipboard } = require('electron');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const { execFile } = require('child_process');
|
const { execFile, spawn } = require('child_process');
|
||||||
const pkg = require('../package.json');
|
const pkg = require('../package.json');
|
||||||
const store = require('./store');
|
const store = require('./store');
|
||||||
const services = require('./services');
|
const services = require('./services');
|
||||||
@@ -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();
|
||||||
@@ -556,6 +565,111 @@ ipcMain.handle('loaders:versions', async (_e, loader, mcVersion) => {
|
|||||||
catch { return []; }
|
catch { return []; }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// OptiFine – kein offizielles Download-API (Downloads laufen bei OptiFine
|
||||||
|
// bewusst über eine Werbeseite, das umgehen wir nicht). Stattdessen: Nutzer
|
||||||
|
// lädt den Installer selbst von optifine.net, AeroMC bereitet Vanilla vor,
|
||||||
|
// startet den echten Installer sichtbar mit dem passenden Zielordner in der
|
||||||
|
// Zwischenablage und übernimmt danach die neu entstandene Profil-Version.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const optifineSetupState = new Map(); // instanceId -> { sharedDir, before: Set<string> }
|
||||||
|
|
||||||
|
function scanOptifineVersions(sharedDir, mcVersion) {
|
||||||
|
const versionsDir = path.join(sharedDir, 'versions');
|
||||||
|
if (!fs.existsSync(versionsDir)) return [];
|
||||||
|
const prefix = String(mcVersion || '');
|
||||||
|
return fs.readdirSync(versionsDir, { withFileTypes: true })
|
||||||
|
.filter((e) => e.isDirectory())
|
||||||
|
.map((e) => e.name)
|
||||||
|
.filter((name) => /optifine/i.test(name) && (!prefix || name.startsWith(prefix)))
|
||||||
|
.filter((name) => fs.existsSync(path.join(versionsDir, name, name + '.json')))
|
||||||
|
.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('optifine:find', (_e, mcVersion) => {
|
||||||
|
const sharedDir = path.join(app.getPath('userData'), 'shared');
|
||||||
|
return scanOptifineVersions(sharedDir, mcVersion);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('optifine:setup', async (_e, id) => {
|
||||||
|
const inst = store.getInstance(id);
|
||||||
|
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||||
|
if (!inst.minecraft.version) return { ok: false, message: 'Bitte zuerst eine Minecraft-Version wählen.' };
|
||||||
|
|
||||||
|
const picked = await dialog.showOpenDialog(mainWindow, {
|
||||||
|
title: 'OptiFine-Installer wählen (zuvor von optifine.net heruntergeladen)',
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: [{ name: 'OptiFine-Installer', extensions: ['jar'] }],
|
||||||
|
});
|
||||||
|
if (picked.canceled || !picked.filePaths.length) return { ok: false, canceled: true };
|
||||||
|
const installerPath = picked.filePaths[0];
|
||||||
|
|
||||||
|
const settings = store.getSettings();
|
||||||
|
const sharedDir = path.join(app.getPath('userData'), 'shared');
|
||||||
|
const opts = {
|
||||||
|
versionId: inst.minecraft.version,
|
||||||
|
loader: 'vanilla',
|
||||||
|
loaderVersion: '',
|
||||||
|
gameDir: store.gameDir(id),
|
||||||
|
sharedDir,
|
||||||
|
userDataDir: app.getPath('userData'),
|
||||||
|
javaPath: inst.java.path || settings.javaPath || '',
|
||||||
|
minMemMb: inst.java.minMemMb || settings.defaultMinMemMb,
|
||||||
|
maxMemMb: inst.java.maxMemMb || settings.defaultMaxMemMb,
|
||||||
|
extraArgs: '',
|
||||||
|
auth: null,
|
||||||
|
ensureJava: settings.autoJava === false ? null : (major, onProgress) => javadl.ensureJava(major, onProgress),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vanilla-Dateien sicherstellen (kein echter Start, da kein Konto übergeben wird)
|
||||||
|
let prep;
|
||||||
|
try {
|
||||||
|
prep = await launcher.prepareAndLaunch(opts, (evt) => {
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('launch:progress', Object.assign({ id }, evt));
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Vorbereitung fehlgeschlagen: ' + err.message };
|
||||||
|
}
|
||||||
|
if (!prep.needAuth && !prep.ok) return { ok: false, message: prep.message || 'Vorbereitung fehlgeschlagen.' };
|
||||||
|
const javaExe = (prep.java && prep.java.path) || opts.javaPath || 'java';
|
||||||
|
|
||||||
|
optifineSetupState.set(id, {
|
||||||
|
sharedDir,
|
||||||
|
before: new Set(scanOptifineVersions(sharedDir, inst.minecraft.version)),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const child = spawn(javaExe, ['-jar', installerPath], { detached: true, stdio: 'ignore' });
|
||||||
|
child.unref();
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Installer konnte nicht gestartet werden: ' + err.message };
|
||||||
|
}
|
||||||
|
try { clipboard.writeText(sharedDir); } catch { /* egal */ }
|
||||||
|
|
||||||
|
return { ok: true, sharedDir };
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('optifine:confirm', (_e, id) => {
|
||||||
|
const inst = store.getInstance(id);
|
||||||
|
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||||
|
const state = optifineSetupState.get(id);
|
||||||
|
const sharedDir = (state && state.sharedDir) || path.join(app.getPath('userData'), 'shared');
|
||||||
|
const nachher = scanOptifineVersions(sharedDir, inst.minecraft.version);
|
||||||
|
const vorher = (state && state.before) || new Set();
|
||||||
|
const neu = nachher.filter((v) => !vorher.has(v));
|
||||||
|
const treffer = neu[0] || nachher[0];
|
||||||
|
optifineSetupState.delete(id);
|
||||||
|
if (!treffer) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message: 'Keine OptiFine-Version für ' + (inst.minecraft.version || '?') + ' gefunden. '
|
||||||
|
+ 'Wurde die Installation im OptiFine-Fenster abgeschlossen und dabei der kopierte Ordner ausgewählt?',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ok: true, versionId: treffer };
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('mods:remove', (_e, id, filename, art) => store.removeMod(id, filename, art || 'mod'));
|
ipcMain.handle('mods:remove', (_e, id, filename, art) => store.removeMod(id, filename, art || 'mod'));
|
||||||
// prüft, ob andere installierte Mods diesen hier als Pflicht-Abhängigkeit brauchen
|
// prüft, ob andere installierte Mods diesen hier als Pflicht-Abhängigkeit brauchen
|
||||||
ipcMain.handle('mods:dependents', (_e, id, filename, art) => store.findDependents(id, filename, art || 'mod'));
|
ipcMain.handle('mods:dependents', (_e, id, filename, art) => store.findDependents(id, filename, art || 'mod'));
|
||||||
@@ -1253,8 +1367,13 @@ async function importCurseForge(buf, zip, zugang) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle('modpack:search', async (_e, opts) => {
|
ipcMain.handle('modpack:search', async (_e, opts) => {
|
||||||
|
const o = opts || {};
|
||||||
try {
|
try {
|
||||||
const q = Object.assign({}, opts || {}, { art: 'modpack' });
|
if (o.source === 'curseforge') {
|
||||||
|
const zugang = services.curseforgeZugang(store.getSettings());
|
||||||
|
return await services.curseforgeSearch(zugang, Object.assign({}, o, { art: 'modpack' }));
|
||||||
|
}
|
||||||
|
const q = Object.assign({}, o, { art: 'modpack' });
|
||||||
return await services.modrinthSearch(q);
|
return await services.modrinthSearch(q);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { total: 0, hits: [], error: err.message };
|
return { total: 0, hits: [], error: err.message };
|
||||||
@@ -1313,6 +1432,52 @@ ipcMain.handle('modpack:installFromModrinth', async (_e, project) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('modpack:installFromCurseForge', async (_e, project) => {
|
||||||
|
try {
|
||||||
|
const zugang = services.curseforgeZugang(store.getSettings());
|
||||||
|
if (!zugang) {
|
||||||
|
return { ok: false, needsKey: true,
|
||||||
|
message: 'CurseForge-Modpacks brauchen einen Zugang. Trage in den Netzwerk-Einstellungen entweder deinen eigenen API-Key ein (kostenlos über console.curseforge.com) oder die Adresse einer CurseForge-Weiterleitung.' };
|
||||||
|
}
|
||||||
|
const rawId = String((project && (project.cfId || project.projectId || project.id)) || '').replace(/^cf-/, '');
|
||||||
|
const modId = Number(rawId);
|
||||||
|
if (!modId) return { ok: false, message: 'Kein Modpack gewählt.' };
|
||||||
|
const gameVersion = (project && project.gameVersion) || '';
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('launch:progress', {
|
||||||
|
phase: 'modpack', detail: 'Modpack-Version wird ermittelt …',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const file = await services.curseforgeResolveModpackFile(zugang, modId, gameVersion);
|
||||||
|
if (!file || !file.url) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
message: gameVersion
|
||||||
|
? ('Keine Modpack-Version für Minecraft ' + gameVersion + ' gefunden.')
|
||||||
|
: 'Keine passende Modpack-Datei gefunden.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('launch:progress', {
|
||||||
|
phase: 'modpack', detail: 'Lade ' + file.filename + ' …',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const dlUrl = zugang.downloadAdresse ? zugang.downloadAdresse(file.url) : file.url;
|
||||||
|
const buf = await services.downloadBuffer(dlUrl, zugang.downloadKopfzeilen);
|
||||||
|
const zip = forge.readZipIndex(buf);
|
||||||
|
if (!zip['manifest.json']) {
|
||||||
|
return { ok: false, message: 'Die geladene Datei enthält kein manifest.json – kein gültiges CurseForge-Modpack.' };
|
||||||
|
}
|
||||||
|
const result = await importCurseForge(buf, zip, zugang);
|
||||||
|
if (result && result.ok && result.instance) {
|
||||||
|
notify('Modpack installiert', `${result.instance.name} ist einsatzbereit.`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Modpack-Installation fehlgeschlagen: ' + err.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Kern des Modpack-Datei-Imports, unabhängig davon ob der Pfad aus einem
|
* Kern des Modpack-Datei-Imports, unabhängig davon ob der Pfad aus einem
|
||||||
* Dateidialog oder per Drag&Drop aufs Fenster kommt.
|
* Dateidialog oder per Drag&Drop aufs Fenster kommt.
|
||||||
@@ -2081,3 +2246,13 @@ ipcMain.handle('app:info', () => ({
|
|||||||
homepage: pkg.homepage || '',
|
homepage: pkg.homepage || '',
|
||||||
launcherWebsite: pkg.launcherWebsite || '',
|
launcherWebsite: pkg.launcherWebsite || '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Lizenztext für die Info-Seite in den Einstellungen. Die LICENSE-Datei liegt
|
||||||
|
// im App-Root und wird über "files" im electron-builder-Config mitgepackt.
|
||||||
|
ipcMain.handle('app:license', () => {
|
||||||
|
try {
|
||||||
|
return fs.readFileSync(path.join(app.getAppPath(), 'LICENSE'), 'utf8');
|
||||||
|
} catch {
|
||||||
|
return 'Lizenztext konnte nicht geladen werden.';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
+216
-216
@@ -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 };
|
||||||
@@ -19,6 +19,9 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
// Minecraft-Versionen (Mojang)
|
// Minecraft-Versionen (Mojang)
|
||||||
mcVersions: () => ipcRenderer.invoke('mc:versions'),
|
mcVersions: () => ipcRenderer.invoke('mc:versions'),
|
||||||
loaderVersions: (loader, mcVersion) => ipcRenderer.invoke('loaders:versions', loader, mcVersion),
|
loaderVersions: (loader, mcVersion) => ipcRenderer.invoke('loaders:versions', loader, mcVersion),
|
||||||
|
optifineFind: (mcVersion) => ipcRenderer.invoke('optifine:find', mcVersion),
|
||||||
|
optifineSetup: (id) => ipcRenderer.invoke('optifine:setup', id),
|
||||||
|
optifineConfirm: (id) => ipcRenderer.invoke('optifine:confirm', id),
|
||||||
|
|
||||||
// Mods (Modrinth)
|
// Mods (Modrinth)
|
||||||
modSearch: (opts) => ipcRenderer.invoke('mods:search', opts),
|
modSearch: (opts) => ipcRenderer.invoke('mods:search', opts),
|
||||||
@@ -104,6 +107,7 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
filesDrop: (filePath, targetId, targetArt) => ipcRenderer.invoke('files:drop', filePath, targetId, targetArt),
|
filesDrop: (filePath, targetId, targetArt) => ipcRenderer.invoke('files:drop', filePath, targetId, targetArt),
|
||||||
modpackSearch: (opts) => ipcRenderer.invoke('modpack:search', opts),
|
modpackSearch: (opts) => ipcRenderer.invoke('modpack:search', opts),
|
||||||
modpackInstallFromModrinth: (project) => ipcRenderer.invoke('modpack:installFromModrinth', project),
|
modpackInstallFromModrinth: (project) => ipcRenderer.invoke('modpack:installFromModrinth', project),
|
||||||
|
modpackInstallFromCurseForge: (project) => ipcRenderer.invoke('modpack:installFromCurseForge', project),
|
||||||
modpackCheck: (id) => ipcRenderer.invoke('modpack:check', id),
|
modpackCheck: (id) => ipcRenderer.invoke('modpack:check', id),
|
||||||
modpackUpdate: (id, versionId) => ipcRenderer.invoke('modpack:update', id, versionId),
|
modpackUpdate: (id, versionId) => ipcRenderer.invoke('modpack:update', id, versionId),
|
||||||
onLaunchProgress: (cb) => {
|
onLaunchProgress: (cb) => {
|
||||||
@@ -236,4 +240,5 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
pickFolder: (title) => ipcRenderer.invoke('dialog:pickFolder', title),
|
pickFolder: (title) => ipcRenderer.invoke('dialog:pickFolder', title),
|
||||||
pickJava: () => ipcRenderer.invoke('dialog:pickJava'),
|
pickJava: () => ipcRenderer.invoke('dialog:pickJava'),
|
||||||
appInfo: () => ipcRenderer.invoke('app:info'),
|
appInfo: () => ipcRenderer.invoke('app:info'),
|
||||||
|
appLicense: () => ipcRenderer.invoke('app:license'),
|
||||||
});
|
});
|
||||||
|
|||||||
+101
-101
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
+132
-15
@@ -467,7 +467,7 @@ function iconOf(key, size = 44) {
|
|||||||
|
|
||||||
const LOADER_LABEL = {
|
const LOADER_LABEL = {
|
||||||
vanilla: 'Vanilla', fabric: 'Fabric', forge: 'Forge',
|
vanilla: 'Vanilla', fabric: 'Fabric', forge: 'Forge',
|
||||||
neoforge: 'NeoForge', quilt: 'Quilt',
|
neoforge: 'NeoForge', quilt: 'Quilt', optifine: 'OptiFine',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bewährte Garbage-Collector-Flags (bekannt als "Aikar's Flags"), reduzieren
|
// Bewährte Garbage-Collector-Flags (bekannt als "Aikar's Flags"), reduzieren
|
||||||
@@ -645,17 +645,28 @@ async function searchModpacks() {
|
|||||||
const query = el('mp-query').value.trim();
|
const query = el('mp-query').value.trim();
|
||||||
const gameVersion = el('mp-mc').value.trim();
|
const gameVersion = el('mp-mc').value.trim();
|
||||||
const sort = el('mp-sort').value || 'relevance';
|
const sort = el('mp-sort').value || 'relevance';
|
||||||
|
const source = (el('mp-source') && el('mp-source').value) || 'modrinth';
|
||||||
const box = el('mp-results');
|
const box = el('mp-results');
|
||||||
const st = el('mp-status');
|
const st = el('mp-status');
|
||||||
st.textContent = 'Suche …';
|
st.textContent = 'Suche …';
|
||||||
box.innerHTML = '<div class="mods-empty">Suche läuft …</div>';
|
box.innerHTML = '<div class="mods-empty">Suche läuft …</div>';
|
||||||
try {
|
try {
|
||||||
const res = await window.api.modpackSearch({
|
const res = await window.api.modpackSearch({
|
||||||
query, gameVersion, sort, limit: 30, offset: 0, art: 'modpack',
|
query, gameVersion, sort, limit: 30, offset: 0, art: 'modpack', source,
|
||||||
});
|
});
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
st.textContent = 'Fehler: ' + res.error;
|
st.textContent = 'Fehler: ' + res.error;
|
||||||
box.innerHTML = '<div class="mods-empty">Suche fehlgeschlagen.</div>';
|
box.innerHTML = '<div class="mods-empty">Suche fehlgeschlagen.</div>';
|
||||||
|
if (res.needKey) {
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.className = 'mods-empty';
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'btn btn-mini';
|
||||||
|
btn.textContent = 'Zu den Netzwerk-Einstellungen';
|
||||||
|
btn.addEventListener('click', () => { hide('modal-import'); openSettings('pane-network'); });
|
||||||
|
wrap.appendChild(btn);
|
||||||
|
box.appendChild(wrap);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hits = res.hits || [];
|
const hits = res.hits || [];
|
||||||
@@ -703,11 +714,15 @@ async function installModpackFromSearch(hit, btn) {
|
|||||||
if (btn) { btn.disabled = true; btn.textContent = '…'; }
|
if (btn) { btn.disabled = true; btn.textContent = '…'; }
|
||||||
status('Installiere Modpack: ' + (hit.title || hit.slug || '') + ' …');
|
status('Installiere Modpack: ' + (hit.title || hit.slug || '') + ' …');
|
||||||
try {
|
try {
|
||||||
const res = await window.api.modpackInstallFromModrinth({
|
const res = hit.source === 'curseforge'
|
||||||
projectId: hit.projectId,
|
? await window.api.modpackInstallFromCurseForge({
|
||||||
title: hit.title || hit.slug,
|
cfId: hit.cfId, projectId: hit.projectId, title: hit.title || hit.slug, gameVersion,
|
||||||
gameVersion,
|
})
|
||||||
});
|
: await window.api.modpackInstallFromModrinth({
|
||||||
|
projectId: hit.projectId,
|
||||||
|
title: hit.title || hit.slug,
|
||||||
|
gameVersion,
|
||||||
|
});
|
||||||
status('Bereit'); el('status-right').textContent = '';
|
status('Bereit'); el('status-right').textContent = '';
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
await reload();
|
await reload();
|
||||||
@@ -1786,7 +1801,44 @@ async function updateLoaderVersions(preselect) {
|
|||||||
const hint = el('f-loader-hint');
|
const hint = el('f-loader-hint');
|
||||||
sel.innerHTML = '<option value="">neueste (empfohlen)</option>';
|
sel.innerHTML = '<option value="">neueste (empfohlen)</option>';
|
||||||
|
|
||||||
|
const ofTools = el('f-optifine-tools');
|
||||||
|
if (ofTools) ofTools.classList.toggle('hidden', loader !== 'optifine');
|
||||||
|
|
||||||
if (loader === 'vanilla') { sel.disabled = true; hint.textContent = 'Vanilla hat keinen Loader.'; return; }
|
if (loader === 'vanilla') { sel.disabled = true; hint.textContent = 'Vanilla hat keinen Loader.'; return; }
|
||||||
|
|
||||||
|
if (loader === 'optifine') {
|
||||||
|
// Kein Loader-API bei OptiFine – die Liste zeigt bereits eingerichtete
|
||||||
|
// Fassungen (siehe f-optifine-tools weiter unten), keine automatisch
|
||||||
|
// ladbaren Versionen.
|
||||||
|
sel.innerHTML = '<option value="">– keine gewählt –</option>';
|
||||||
|
sel.disabled = false;
|
||||||
|
if (el('f-optifine-status')) el('f-optifine-status').textContent = '';
|
||||||
|
if (el('f-optifine-confirm')) el('f-optifine-confirm').classList.add('hidden');
|
||||||
|
if (!editingId) {
|
||||||
|
hint.textContent = 'Erst speichern, dann lässt sich OptiFine im Bearbeiten-Dialog einrichten.';
|
||||||
|
if (el('f-optifine-install')) el('f-optifine-install').disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (el('f-optifine-install')) el('f-optifine-install').disabled = false;
|
||||||
|
if (!mc) { hint.textContent = 'Erst Minecraft-Version wählen.'; return; }
|
||||||
|
hint.textContent = 'Suche bereits eingerichtete OptiFine-Fassungen …';
|
||||||
|
try {
|
||||||
|
const found = await window.api.optifineFind(mc);
|
||||||
|
for (const v of found) {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = v; o.textContent = v;
|
||||||
|
if (v === preselect) o.selected = true;
|
||||||
|
sel.appendChild(o);
|
||||||
|
}
|
||||||
|
hint.textContent = found.length
|
||||||
|
? `${found.length} eingerichtete OptiFine-Fassung(en) für ${mc}.`
|
||||||
|
: 'Noch keine OptiFine-Installation für diese Version – über den Knopf unten einrichten.';
|
||||||
|
} catch {
|
||||||
|
hint.textContent = 'Suche fehlgeschlagen.';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
sel.disabled = false;
|
sel.disabled = false;
|
||||||
if (!mc) { hint.textContent = 'Erst Minecraft-Version wählen.'; return; }
|
if (!mc) { hint.textContent = 'Erst Minecraft-Version wählen.'; return; }
|
||||||
|
|
||||||
@@ -2709,14 +2761,23 @@ function renderContentList(boxId, items, sub, emoji) {
|
|||||||
|
|
||||||
// Welche Inhaltsart wird gerade gezeigt: mod | resourcepack | shader
|
// Welche Inhaltsart wird gerade gezeigt: mod | resourcepack | shader
|
||||||
let modArt = 'mod';
|
let modArt = 'mod';
|
||||||
const ART_LABEL = { mod: 'Mods', resourcepack: 'Ressourcenpakete', shader: 'Shader' };
|
const ART_LABEL = {
|
||||||
|
mod: 'Mods', resourcepack: 'Ressourcenpakete', shader: 'Shader', datapack: 'Datenpakete',
|
||||||
|
};
|
||||||
const ART_SUCHE = {
|
const ART_SUCHE = {
|
||||||
mod: 'Mods durchsuchen … (z. B. Sodium, JEI, Create)',
|
mod: 'Mods durchsuchen … (z. B. Sodium, JEI, Create)',
|
||||||
resourcepack: 'Ressourcenpakete durchsuchen … (z. B. Faithful, Bare Bones)',
|
resourcepack: 'Ressourcenpakete durchsuchen … (z. B. Faithful, Bare Bones)',
|
||||||
shader: 'Shader durchsuchen … (z. B. Complementary, BSL)',
|
shader: 'Shader durchsuchen … (z. B. Complementary, BSL)',
|
||||||
|
datapack: 'Datenpakete durchsuchen … (z. B. Terralith, Vanilla Tweaks)',
|
||||||
};
|
};
|
||||||
// Nur Mods brauchen einen Loader – Ressourcenpakete und Shader laufen auch in Vanilla
|
// Nur Mods brauchen einen Loader – Ressourcenpakete, Shader und Datenpakete laufen auch in Vanilla
|
||||||
const ART_BRAUCHT_LOADER = { mod: true, resourcepack: false, shader: false };
|
const ART_BRAUCHT_LOADER = {
|
||||||
|
mod: true, resourcepack: false, shader: false, datapack: false,
|
||||||
|
};
|
||||||
|
// CurseForge hat für Datenpakete keinen verlässlich zugeordneten API-Klassencode in
|
||||||
|
// diesem Launcher – dort deshalb nur Modrinth als Quelle anbieten, statt falsche
|
||||||
|
// Treffer (z. B. Mods) unter "Datenpakete" anzuzeigen.
|
||||||
|
const ART_NUR_MODRINTH = { datapack: true };
|
||||||
|
|
||||||
async function openMods() {
|
async function openMods() {
|
||||||
const inst = await window.api.getInstance(currentDetailId);
|
const inst = await window.api.getInstance(currentDetailId);
|
||||||
@@ -2754,6 +2815,10 @@ async function zeigeModAnsicht(inst) {
|
|||||||
['m-query', 'm-search-btn', 'm-sort', 'm-category'].forEach((id) => { el(id).disabled = blocked; });
|
['m-query', 'm-search-btn', 'm-sort', 'm-category'].forEach((id) => { el(id).disabled = blocked; });
|
||||||
// Kategorien gelten nur für Mods
|
// Kategorien gelten nur für Mods
|
||||||
el('m-category').disabled = blocked || modArt !== 'mod';
|
el('m-category').disabled = blocked || modArt !== 'mod';
|
||||||
|
// Manche Quellen decken nicht jede Inhaltsart ab (z. B. CurseForge <-> Datenpakete)
|
||||||
|
const cfOption = el('m-source').querySelector('option[value="curseforge"]');
|
||||||
|
if (cfOption) cfOption.disabled = !!ART_NUR_MODRINTH[modArt];
|
||||||
|
if (ART_NUR_MODRINTH[modArt] && el('m-source').value === 'curseforge') el('m-source').value = 'modrinth';
|
||||||
el('m-query').value = '';
|
el('m-query').value = '';
|
||||||
el('m-more').classList.add('hidden');
|
el('m-more').classList.add('hidden');
|
||||||
// Ergebnisse der vorigen Ansicht nicht mitschleppen
|
// Ergebnisse der vorigen Ansicht nicht mitschleppen
|
||||||
@@ -2813,9 +2878,13 @@ async function refreshInstalled() {
|
|||||||
if (up) kennzeichen.push('<span class="mod-new">Update</span>');
|
if (up) kennzeichen.push('<span class="mod-new">Update</span>');
|
||||||
if (aus) kennzeichen.push('<span class="mod-off">aus</span>');
|
if (aus) kennzeichen.push('<span class="mod-off">aus</span>');
|
||||||
if (m.fehlt) kennzeichen.push('<span class="mod-off">Datei fehlt</span>');
|
if (m.fehlt) kennzeichen.push('<span class="mod-off">Datei fehlt</span>');
|
||||||
|
// OptiFine lässt sich nicht über Modrinth/CurseForge installieren (kein API-Zugriff auf
|
||||||
|
// deren Downloads) – von Hand hinzugefügt taucht es sonst wie ein beliebiger Mod auf.
|
||||||
|
const istOptiFine = modArt === 'mod' && /^optifine[_-]/i.test(m.filename || '');
|
||||||
|
if (istOptiFine) kennzeichen.push('<span class="mod-off" style="opacity:.8">OptiFine</span>');
|
||||||
|
|
||||||
item.innerHTML =
|
item.innerHTML =
|
||||||
`<div class="mod-icon">${aus ? '🚫' : '🧩'}</div>
|
`<div class="mod-icon">${aus ? '🚫' : istOptiFine ? '⚡' : '🧩'}</div>
|
||||||
<div class="mod-info">
|
<div class="mod-info">
|
||||||
<div class="mod-name">${escapeHtml(m.title)}${kennzeichen.length ? ' ' + kennzeichen.join(' ') : ''}</div>
|
<div class="mod-name">${escapeHtml(m.title)}${kennzeichen.length ? ' ' + kennzeichen.join(' ') : ''}</div>
|
||||||
<div class="mod-meta">${up
|
<div class="mod-meta">${up
|
||||||
@@ -3123,17 +3192,24 @@ async function openSettings(pane) {
|
|||||||
['Autor', info.author || 'Nicht hinterlegt'],
|
['Autor', info.author || 'Nicht hinterlegt'],
|
||||||
['Webseite', info.homepage || 'Nicht hinterlegt', info.homepage || ''],
|
['Webseite', info.homepage || 'Nicht hinterlegt', info.homepage || ''],
|
||||||
['Launcher-Webseite', info.launcherWebsite || 'Nicht hinterlegt', info.launcherWebsite || ''],
|
['Launcher-Webseite', info.launcherWebsite || 'Nicht hinterlegt', info.launcherWebsite || ''],
|
||||||
].map(([label, value, url]) => {
|
['Lizenz', 'Nur privater, nicht-kommerzieller Gebrauch – Details anzeigen', null, 'license'],
|
||||||
|
].map(([label, value, url, action]) => {
|
||||||
const renderedValue = url
|
const renderedValue = url
|
||||||
? `<a href="#" class="info-meta-link" data-url="${escapeHtml(url)}">${escapeHtml(value)}</a>`
|
? `<a href="#" class="info-meta-link" data-url="${escapeHtml(url)}">${escapeHtml(value)}</a>`
|
||||||
: escapeHtml(value || '–');
|
: action
|
||||||
|
? `<a href="#" class="info-meta-link" data-action="${escapeHtml(action)}">${escapeHtml(value)}</a>`
|
||||||
|
: escapeHtml(value || '–');
|
||||||
return `<div class="settings-kv-row"><span class="settings-kv-label">${escapeHtml(label)}</span><span class="settings-kv-value">${renderedValue}</span></div>`;
|
return `<div class="settings-kv-row"><span class="settings-kv-label">${escapeHtml(label)}</span><span class="settings-kv-value">${renderedValue}</span></div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
for (const link of el('s-appinfo').querySelectorAll('.info-meta-link')) {
|
for (const link of el('s-appinfo').querySelectorAll('.info-meta-link')) {
|
||||||
link.addEventListener('click', (evt) => {
|
link.addEventListener('click', async (evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const url = link.dataset.url;
|
const url = link.dataset.url;
|
||||||
if (url) window.api.openExternal(url);
|
if (url) { window.api.openExternal(url); return; }
|
||||||
|
if (link.dataset.action === 'license') {
|
||||||
|
show('modal-license');
|
||||||
|
el('license-text').textContent = await window.api.appLicense();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
show('modal-settings');
|
show('modal-settings');
|
||||||
@@ -3859,6 +3935,47 @@ function wire() {
|
|||||||
el('f-version-type').addEventListener('change', () => fillVersionSelect(el('f-version').value));
|
el('f-version-type').addEventListener('change', () => fillVersionSelect(el('f-version').value));
|
||||||
el('f-loader').addEventListener('change', () => updateLoaderVersions());
|
el('f-loader').addEventListener('change', () => updateLoaderVersions());
|
||||||
el('f-version').addEventListener('change', () => updateLoaderVersions());
|
el('f-version').addEventListener('change', () => updateLoaderVersions());
|
||||||
|
|
||||||
|
el('f-optifine-install').addEventListener('click', async () => {
|
||||||
|
if (!editingId) return;
|
||||||
|
const btn = el('f-optifine-install');
|
||||||
|
const status = el('f-optifine-status');
|
||||||
|
btn.disabled = true;
|
||||||
|
status.textContent = 'Minecraft wird vorbereitet …';
|
||||||
|
try {
|
||||||
|
const res = await window.api.optifineSetup(editingId);
|
||||||
|
if (res.canceled) { status.textContent = ''; return; }
|
||||||
|
if (!res.ok) { status.textContent = '✖ ' + (res.message || 'Fehlgeschlagen.'); toast(res.message || 'OptiFine-Installer konnte nicht gestartet werden.', true); return; }
|
||||||
|
status.textContent = `Installer gestartet – Zielordner wurde in die Zwischenablage kopiert: ${res.sharedDir} – `
|
||||||
|
+ 'im OptiFine-Fenster als Minecraft-Ordner einfügen, „Install" klicken, danach hier bestätigen.';
|
||||||
|
el('f-optifine-confirm').classList.remove('hidden');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
el('f-optifine-confirm').addEventListener('click', async () => {
|
||||||
|
if (!editingId) return;
|
||||||
|
const btn = el('f-optifine-confirm');
|
||||||
|
const status = el('f-optifine-status');
|
||||||
|
btn.disabled = true;
|
||||||
|
try {
|
||||||
|
const res = await window.api.optifineConfirm(editingId);
|
||||||
|
if (!res.ok) { status.textContent = '✖ ' + (res.message || 'Nicht gefunden.'); toast(res.message || 'OptiFine-Version wurde nicht gefunden.', true); return; }
|
||||||
|
toast('OptiFine ' + res.versionId + ' übernommen.');
|
||||||
|
status.textContent = '✔ Übernommen: ' + res.versionId;
|
||||||
|
const sel = el('f-loader-version');
|
||||||
|
if (![...sel.options].some((o) => o.value === res.versionId)) {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = res.versionId; o.textContent = res.versionId;
|
||||||
|
sel.appendChild(o);
|
||||||
|
}
|
||||||
|
sel.value = res.versionId;
|
||||||
|
btn.classList.add('hidden');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
el('f-pick-image').addEventListener('click', async () => {
|
el('f-pick-image').addEventListener('click', async () => {
|
||||||
const p = await window.api.pickImage();
|
const p = await window.api.pickImage();
|
||||||
if (!p) return;
|
if (!p) return;
|
||||||
|
|||||||
+17
-2
@@ -236,6 +236,7 @@ const INHALTSARTEN = {
|
|||||||
mod: { projectType: 'mod', ordner: 'mods', label: 'Mods', loaderNoetig: true },
|
mod: { projectType: 'mod', ordner: 'mods', label: 'Mods', loaderNoetig: true },
|
||||||
resourcepack: { projectType: 'resourcepack', ordner: 'resourcepacks', label: 'Ressourcenpakete', loaderNoetig: false },
|
resourcepack: { projectType: 'resourcepack', ordner: 'resourcepacks', label: 'Ressourcenpakete', loaderNoetig: false },
|
||||||
shader: { projectType: 'shader', ordner: 'shaderpacks', label: 'Shader', loaderNoetig: false },
|
shader: { projectType: 'shader', ordner: 'shaderpacks', label: 'Shader', loaderNoetig: false },
|
||||||
|
datapack: { projectType: 'datapack', ordner: 'datapacks', label: 'Datenpakete', loaderNoetig: false },
|
||||||
modpack: { projectType: 'modpack', ordner: null, label: 'Modpacks', loaderNoetig: false },
|
modpack: { projectType: 'modpack', ordner: null, label: 'Modpacks', loaderNoetig: false },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -515,7 +516,9 @@ function forgeCdnUrl(fileId, fileName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CF_LOADER = { forge: 1, fabric: 4, quilt: 5, neoforge: 6 };
|
const CF_LOADER = { forge: 1, fabric: 4, quilt: 5, neoforge: 6 };
|
||||||
const CF_CLASS = { mod: 6, resourcepack: 12, shader: 6552 };
|
// classId je Inhaltsart bei gameId 432 (Minecraft). modpack=4471 ist die auf
|
||||||
|
// CurseForge selbst verwendete Kategorie-ID (siehe curseforge.com/minecraft/modpacks).
|
||||||
|
const CF_CLASS = { mod: 6, resourcepack: 12, shader: 6552, modpack: 4471 };
|
||||||
|
|
||||||
function cfLoaderType(loader) {
|
function cfLoaderType(loader) {
|
||||||
return CF_LOADER[String(loader || '').toLowerCase()] || 0;
|
return CF_LOADER[String(loader || '').toLowerCase()] || 0;
|
||||||
@@ -598,7 +601,12 @@ async function curseforgeSearch(zugang, {
|
|||||||
+ 'Die Weiterleitung reicht für Downloads, nicht für den Katalog.',
|
+ 'Die Weiterleitung reicht für Downloads, nicht für den Katalog.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const classId = CF_CLASS[art] || CF_CLASS.mod;
|
if (!CF_CLASS[art]) {
|
||||||
|
// Kein zugeordneter CurseForge-Klassencode für diese Inhaltsart -> lieber gar
|
||||||
|
// keine Treffer als falsch einsortierte (z. B. Mods unter "Datenpakete").
|
||||||
|
return { total: 0, hits: [], error: 'CurseForge unterstützt diese Inhaltsart hier nicht – bitte Modrinth nutzen.' };
|
||||||
|
}
|
||||||
|
const classId = CF_CLASS[art];
|
||||||
const url = new URL(CURSEFORGE + '/mods/search');
|
const url = new URL(CURSEFORGE + '/mods/search');
|
||||||
url.searchParams.set('gameId', '432');
|
url.searchParams.set('gameId', '432');
|
||||||
url.searchParams.set('classId', String(classId));
|
url.searchParams.set('classId', String(classId));
|
||||||
@@ -713,6 +721,12 @@ async function curseforgePickFile(zugang, modId, gameVersion, loader, art) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Beste Modpack-Datei eines CurseForge-Projekts auflösen (für den Katalog-Import,
|
||||||
|
// analog zu modrinthResolveModpack).
|
||||||
|
async function curseforgeResolveModpackFile(zugang, modId, gameVersion) {
|
||||||
|
return curseforgePickFile(zugang, modId, gameVersion, '', 'modpack');
|
||||||
|
}
|
||||||
|
|
||||||
async function curseforgeInstall({ zugang, projectId, title, gameVersion, loader, modsDir, art = 'mod' }) {
|
async function curseforgeInstall({ zugang, projectId, title, gameVersion, loader, modsDir, art = 'mod' }) {
|
||||||
if (!zugang) return { ok: false, message: 'Kein CurseForge-Zugang eingerichtet.' };
|
if (!zugang) return { ok: false, message: 'Kein CurseForge-Zugang eingerichtet.' };
|
||||||
const rawId = String(projectId || '').replace(/^cf-/, '');
|
const rawId = String(projectId || '').replace(/^cf-/, '');
|
||||||
@@ -814,4 +828,5 @@ module.exports = {
|
|||||||
modrinthSearch, modrinthResolveFile, modrinthResolveModpack, modrinthInstall, modrinthProjectTitle,
|
modrinthSearch, modrinthResolveFile, modrinthResolveModpack, modrinthInstall, modrinthProjectTitle,
|
||||||
modrinthFindUpdates, downloadFile, downloadBuffer, inhaltsArt, INHALTSARTEN,
|
modrinthFindUpdates, downloadFile, downloadBuffer, inhaltsArt, INHALTSARTEN,
|
||||||
curseforgeResolveFiles, curseforgeSearch, curseforgeInstall, curseforgeTestKey, forgeCdnUrl, curseforgeZugang,
|
curseforgeResolveFiles, curseforgeSearch, curseforgeInstall, curseforgeTestKey, forgeCdnUrl, curseforgeZugang,
|
||||||
|
curseforgeResolveModpackFile,
|
||||||
};
|
};
|
||||||
|
|||||||
+6
-2
@@ -134,8 +134,12 @@ function gameDir(id) {
|
|||||||
* "mod" behält die bisherigen Namen, damit vorhandene Instanzen unverändert
|
* "mod" behält die bisherigen Namen, damit vorhandene Instanzen unverändert
|
||||||
* weiterlaufen.
|
* weiterlaufen.
|
||||||
*/
|
*/
|
||||||
const INHALT_ORDNER = { mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks' };
|
const INHALT_ORDNER = {
|
||||||
const INHALT_INDEX = { mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json' };
|
mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks', datapack: 'datapacks',
|
||||||
|
};
|
||||||
|
const INHALT_INDEX = {
|
||||||
|
mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json', datapack: 'datapacks.json',
|
||||||
|
};
|
||||||
|
|
||||||
function inhaltsDir(id, art = 'mod') {
|
function inhaltsDir(id, art = 'mod') {
|
||||||
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
|
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
|
||||||
|
|||||||
+11
-2
@@ -230,8 +230,17 @@ async function downloadAndInstall(asset, onProgress) {
|
|||||||
|
|
||||||
const dir = updateDir();
|
const dir = updateDir();
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
const dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_'));
|
let dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_'));
|
||||||
fs.rmSync(dest, { force: true });
|
try {
|
||||||
|
fs.rmSync(dest, { force: true });
|
||||||
|
} catch {
|
||||||
|
// Alte Datei ist gesperrt (Virenscanner prüft sie gerade, oder ein vorheriger
|
||||||
|
// Installer-Versuch läuft noch) - dann eben unter neuem Namen laden, statt mit
|
||||||
|
// EPERM abzubrechen. Der Rest wird bei nächster Gelegenheit von
|
||||||
|
// cleanupUpdateCache() aufgeräumt (ab 10 Minuten Alter).
|
||||||
|
const parsed = path.parse(dest);
|
||||||
|
dest = path.join(parsed.dir, `${parsed.name}-${Date.now()}${parsed.ext}`);
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(asset.url, { headers: { 'User-Agent': UA } });
|
const res = await fetch(asset.url, { headers: { 'User-Agent': UA } });
|
||||||
if (!res.ok) throw new Error('Download fehlgeschlagen: HTTP ' + res.status);
|
if (!res.ok) throw new Error('Download fehlgeschlagen: HTTP ' + res.status);
|
||||||
|
|||||||
Reference in New Issue
Block a user