Upload via GUI (39 Dateien)
This commit is contained in:
+211
-37
@@ -274,6 +274,8 @@ ipcMain.handle('java:detect', (_e, javaPath) => detectJava(javaPath));
|
||||
|
||||
// automatische Java-Suche über das System
|
||||
ipcMain.handle('java:scan', () => services.scanJava(false)); // Knopf sucht immer frisch
|
||||
// aus dem Zwischenspeicher – für Dialoge, die nur die Liste anzeigen wollen
|
||||
ipcMain.handle('java:list', () => services.scanJava(true));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// IPC – Minecraft-Versionen (Mojang)
|
||||
@@ -287,18 +289,23 @@ ipcMain.handle('mc:versions', () => services.getMcVersions(app.getPath('userData
|
||||
|
||||
ipcMain.handle('mods:search', (_e, opts) => services.modrinthSearch(opts));
|
||||
|
||||
ipcMain.handle('mods:list', (_e, id) => store.listMods(id));
|
||||
ipcMain.handle('mods:list', (_e, id, art) => store.listMods(id, art || 'mod'));
|
||||
|
||||
ipcMain.handle('mods:install', async (_e, id, project) => {
|
||||
ipcMain.handle('mods:install', async (_e, id, project, art) => {
|
||||
const inst = store.getInstance(id);
|
||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
const typ = services.inhaltsArt(art || 'mod');
|
||||
const gameVersion = inst.minecraft.version;
|
||||
const loader = inst.minecraft.loader;
|
||||
if (!gameVersion) return { ok: false, message: 'Instanz hat keine Minecraft-Version gesetzt.' };
|
||||
if (!loader || loader === 'vanilla') return { ok: false, message: 'Mods brauchen einen Mod-Loader (Fabric/Forge/NeoForge/Quilt).' };
|
||||
// Nur Mods brauchen einen Loader – Ressourcenpakete und Shader laufen auch
|
||||
// in einer Vanilla-Instanz.
|
||||
if (typ.loaderNoetig && (!loader || loader === 'vanilla')) {
|
||||
return { ok: false, message: 'Mods brauchen einen Mod-Loader (Fabric/Forge/NeoForge/Quilt).' };
|
||||
}
|
||||
|
||||
const modsDir = store.modsDir(id);
|
||||
const done = new Set(store.listMods(id).map((m) => m.projectId));
|
||||
const modsDir = store.inhaltsDir(id, art || 'mod');
|
||||
const done = new Set(store.listMods(id, art || 'mod').map((m) => m.projectId));
|
||||
const added = [];
|
||||
let firstError = null;
|
||||
|
||||
@@ -308,13 +315,13 @@ ipcMain.handle('mods:install', async (_e, id, project) => {
|
||||
done.add(proj.projectId);
|
||||
try {
|
||||
const res = await services.modrinthInstall({
|
||||
projectId: proj.projectId, title: proj.title, gameVersion, loader, modsDir,
|
||||
projectId: proj.projectId, title: proj.title, gameVersion, loader, modsDir, art: art || 'mod',
|
||||
});
|
||||
if (!res.ok) {
|
||||
if (!isDep && !firstError) firstError = res.message;
|
||||
return;
|
||||
}
|
||||
store.addMod(id, res.entry);
|
||||
store.addMod(id, res.entry, art || 'mod');
|
||||
added.push(res.entry);
|
||||
for (const dep of res.dependencies || []) {
|
||||
if (dep.dependency_type === 'required' && dep.project_id && !done.has(dep.project_id)) {
|
||||
@@ -338,7 +345,7 @@ ipcMain.handle('loaders:versions', async (_e, loader, mcVersion) => {
|
||||
catch { return []; }
|
||||
});
|
||||
|
||||
ipcMain.handle('mods:remove', (_e, id, filename) => store.removeMod(id, filename));
|
||||
ipcMain.handle('mods:remove', (_e, id, filename, art) => store.removeMod(id, filename, art || 'mod'));
|
||||
|
||||
// Vorhandener Arbeitsspeicher – Grundlage für die Prüfung der RAM-Angaben
|
||||
function gesamtSpeicherMb() {
|
||||
@@ -421,28 +428,29 @@ ipcMain.handle('crash:openFolder', (_e, id) => {
|
||||
});
|
||||
|
||||
// Mod ab-/anschalten statt löschen (Datei wird nur umbenannt)
|
||||
ipcMain.handle('mods:setEnabled', (_e, id, filename, enabled) => {
|
||||
const res = store.setModEnabled(id, filename, !!enabled);
|
||||
ipcMain.handle('mods:setEnabled', (_e, id, filename, enabled, art) => {
|
||||
const res = store.setModEnabled(id, filename, !!enabled, art || 'mod');
|
||||
if (res.ok) return res;
|
||||
const ordner = services.inhaltsArt(art || 'mod').ordner;
|
||||
const gruende = {
|
||||
'not-found': 'Die Datei liegt nicht mehr im mods-Ordner.',
|
||||
'both-exist': 'Es liegen beide Fassungen im mods-Ordner (an und abgeschaltet). Bitte eine davon von Hand entfernen.',
|
||||
'not-found': `Die Datei liegt nicht mehr im ${ordner}-Ordner.`,
|
||||
'both-exist': `Es liegen beide Fassungen im ${ordner}-Ordner (an und abgeschaltet). Bitte eine davon von Hand entfernen.`,
|
||||
'bad-name': 'Ungültiger Dateiname.',
|
||||
};
|
||||
return { ok: false, message: gruende[res.reason] || res.reason };
|
||||
});
|
||||
|
||||
// sucht für die installierten Mods nach neueren Dateien (lädt noch nichts)
|
||||
ipcMain.handle('mods:checkUpdates', async (_e, id) => {
|
||||
ipcMain.handle('mods:checkUpdates', async (_e, id, art) => {
|
||||
const inst = store.getInstance(id);
|
||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
const gameVersion = inst.minecraft.version;
|
||||
const loader = inst.minecraft.loader;
|
||||
if (!gameVersion) return { ok: false, message: 'Instanz hat keine Minecraft-Version gesetzt.' };
|
||||
try {
|
||||
const res = await services.modrinthFindUpdates(store.listMods(id), gameVersion, loader, (p) => {
|
||||
const res = await services.modrinthFindUpdates(store.listMods(id, art || 'mod'), gameVersion, loader, (p) => {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('mods:updateProgress', p);
|
||||
});
|
||||
}, art || 'mod');
|
||||
return { ok: true, ...res };
|
||||
} catch (err) {
|
||||
return { ok: false, message: 'Update-Suche fehlgeschlagen: ' + err.message };
|
||||
@@ -450,28 +458,29 @@ ipcMain.handle('mods:checkUpdates', async (_e, id) => {
|
||||
});
|
||||
|
||||
// lädt die gewählten Aktualisierungen und ersetzt die alten Dateien
|
||||
ipcMain.handle('mods:applyUpdates', async (_e, id, projectIds) => {
|
||||
ipcMain.handle('mods:applyUpdates', async (_e, id, projectIds, art) => {
|
||||
const inst = store.getInstance(id);
|
||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
const gameVersion = inst.minecraft.version;
|
||||
const loader = inst.minecraft.loader;
|
||||
const modsDir = store.modsDir(id);
|
||||
const sorte = art || 'mod';
|
||||
const modsDir = store.inhaltsDir(id, sorte);
|
||||
const gewuenscht = new Set(projectIds || []);
|
||||
const erneuert = [];
|
||||
const fehler = [];
|
||||
|
||||
for (const mod of store.listMods(id)) {
|
||||
for (const mod of store.listMods(id, sorte)) {
|
||||
if (!mod.projectId || !gewuenscht.has(mod.projectId)) continue;
|
||||
try {
|
||||
const res = await services.modrinthInstall({
|
||||
projectId: mod.projectId, title: mod.title, gameVersion, loader, modsDir,
|
||||
projectId: mod.projectId, title: mod.title, gameVersion, loader, modsDir, art: sorte,
|
||||
});
|
||||
if (!res.ok) { fehler.push({ title: mod.title, message: res.message }); continue; }
|
||||
store.addMod(id, res.entry);
|
||||
store.addMod(id, res.entry, sorte);
|
||||
// alte Datei erst weg, wenn die neue liegt – und nie, wenn sie gleich heißt
|
||||
if (mod.filename && mod.filename !== res.entry.filename) store.removeMod(id, mod.filename);
|
||||
// war die Mod abgeschaltet, bleibt sie es auch nach dem Update
|
||||
if (mod.enabled === false) store.setModEnabled(id, res.entry.filename, false);
|
||||
if (mod.filename && mod.filename !== res.entry.filename) store.removeMod(id, mod.filename, sorte);
|
||||
// war der Eintrag abgeschaltet, bleibt er es auch nach dem Update
|
||||
if (mod.enabled === false) store.setModEnabled(id, res.entry.filename, false, sorte);
|
||||
erneuert.push({ title: mod.title, von: mod.versionNumber || '?', auf: res.entry.versionNumber });
|
||||
} catch (err) {
|
||||
fehler.push({ title: mod.title, message: err.message });
|
||||
@@ -698,38 +707,203 @@ async function importMrpack(buf, zip) {
|
||||
const gameDir = store.gameDir(created.id);
|
||||
const inside = (dest) => dest.startsWith(gameDir + path.sep);
|
||||
|
||||
const files = index.files || [];
|
||||
const eingespielt = await spieleMrpackEin(buf, zip, index, gameDir, { schonePlayerDaten: false });
|
||||
|
||||
// Herkunft merken, damit sich das Paket später aktualisieren lässt
|
||||
const herkunft = await modpackHerkunft(buf, index);
|
||||
store.updateInstance(created.id, { modpack: Object.assign(herkunft, { dateien: eingespielt.dateien }) });
|
||||
|
||||
return { ok: true, instance: store.getInstance(created.id), fileCount: eingespielt.dateien.length };
|
||||
}
|
||||
|
||||
/*
|
||||
* Spielt ein .mrpack in ein Spielverzeichnis ein.
|
||||
* schonePlayerDaten: bei einer Aktualisierung bleiben Welten, Bildschirmfotos
|
||||
* und die Optionen unangetastet – sonst wären die Einstellungen des Spielers weg.
|
||||
* -> { dateien: [relative Pfade] }
|
||||
*/
|
||||
const SPIELERDATEN = [/^saves\//i, /^screenshots\//i, /^logs\//i, /^options\.txt$/i, /^servers\.dat$/i];
|
||||
|
||||
async function spieleMrpackEin(buf, zip, index, gameDir, { schonePlayerDaten }) {
|
||||
const inside = (dest) => dest.startsWith(gameDir + path.sep);
|
||||
const geschuetzt = (rel) => schonePlayerDaten && SPIELERDATEN.some((r) => r.test(rel));
|
||||
const dateien = [];
|
||||
|
||||
const files = (index.files || []).filter((f) => !(f.env && f.env.client === 'unsupported'));
|
||||
let i = 0;
|
||||
for (const f of files) {
|
||||
i++;
|
||||
if (f.env && f.env.client === 'unsupported') continue;
|
||||
const url = f.downloads && f.downloads[0];
|
||||
if (!url || !f.path) continue;
|
||||
const dest = path.join(gameDir, f.path.replace(/\//g, path.sep));
|
||||
const rel = String(f.path).replace(/\\/g, '/');
|
||||
if (geschuetzt(rel)) continue;
|
||||
const dest = path.join(gameDir, rel.replace(/\//g, path.sep));
|
||||
if (!inside(dest)) continue;
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('launch:progress', { phase: 'modpack', current: i, total: files.length, detail: 'Modpack-Dateien' });
|
||||
}
|
||||
const res = await fetch(url, { headers: { 'User-Agent': 'AeroMC-Launcher/0.11' } });
|
||||
if (!res.ok) continue;
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
|
||||
try {
|
||||
const res = await fetch(url, { headers: { 'User-Agent': 'AeroMC-Launcher' } });
|
||||
if (!res.ok) continue;
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
|
||||
dateien.push(rel);
|
||||
} catch { /* einzelne Datei überspringen */ }
|
||||
}
|
||||
|
||||
// overrides einspielen
|
||||
for (const prefix of ['overrides/', 'client-overrides/']) {
|
||||
for (const name of Object.keys(zip)) {
|
||||
if (name.startsWith(prefix) && !name.endsWith('/')) {
|
||||
const dest = path.join(gameDir, name.slice(prefix.length).replace(/\//g, path.sep));
|
||||
if (!inside(dest)) continue;
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.writeFileSync(dest, forge.readZipEntry(buf, zip[name]));
|
||||
}
|
||||
if (!name.startsWith(prefix) || name.endsWith('/')) continue;
|
||||
const rel = name.slice(prefix.length);
|
||||
if (geschuetzt(rel)) continue;
|
||||
const dest = path.join(gameDir, rel.replace(/\//g, path.sep));
|
||||
if (!inside(dest)) continue;
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.writeFileSync(dest, forge.readZipEntry(buf, zip[name]));
|
||||
dateien.push(rel);
|
||||
}
|
||||
}
|
||||
return { ok: true, instance: store.getInstance(created.id), fileCount: files.length };
|
||||
return { dateien };
|
||||
}
|
||||
|
||||
/*
|
||||
* Woher stammt das Paket? Modrinth kennt jede veröffentlichte Datei an ihrer
|
||||
* Prüfsumme – darüber finden wir Projekt und Fassung, ohne den Nutzer zu fragen.
|
||||
*/
|
||||
async function modpackHerkunft(buf, index) {
|
||||
const basis = {
|
||||
quelle: 'modrinth',
|
||||
name: index.name || '',
|
||||
versionNummer: index.versionId || '',
|
||||
projectId: '',
|
||||
versionId: '',
|
||||
};
|
||||
try {
|
||||
const sha1 = require('crypto').createHash('sha1').update(buf).digest('hex');
|
||||
const res = await fetch(`https://api.modrinth.com/v2/version_file/${sha1}?algorithm=sha1`,
|
||||
{ headers: { 'User-Agent': 'AeroMC-Launcher', Accept: 'application/json' } });
|
||||
if (res.ok) {
|
||||
const v = await res.json();
|
||||
basis.projectId = v.project_id || '';
|
||||
basis.versionId = v.id || '';
|
||||
basis.versionNummer = v.version_number || basis.versionNummer;
|
||||
}
|
||||
} catch { /* offline oder unbekanntes Paket – dann eben ohne Herkunft */ }
|
||||
return basis;
|
||||
}
|
||||
|
||||
// ---- Modpack aktualisieren (Modrinth) ----
|
||||
|
||||
/*
|
||||
* Sucht eine neuere Fassung des installierten Modpacks.
|
||||
* -> { ok, verfuegbar, aktuelleNummer, neueNummer, ... }
|
||||
*/
|
||||
ipcMain.handle('modpack:check', async (_e, id) => {
|
||||
const inst = store.getInstance(id);
|
||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
const mp = inst.modpack;
|
||||
if (!mp || !mp.projectId) {
|
||||
return { ok: false, message: 'Für diese Instanz ist kein Modrinth-Modpack hinterlegt. '
|
||||
+ 'Aktualisieren geht nur bei Instanzen, die aus einem .mrpack entstanden sind.' };
|
||||
}
|
||||
try {
|
||||
const res = await fetch(`https://api.modrinth.com/v2/project/${mp.projectId}/version`,
|
||||
{ headers: { 'User-Agent': 'AeroMC-Launcher', Accept: 'application/json' } });
|
||||
if (!res.ok) return { ok: false, message: 'Modrinth antwortet mit HTTP ' + res.status };
|
||||
const versionen = await res.json();
|
||||
if (!Array.isArray(versionen) || !versionen.length) return { ok: true, verfuegbar: false };
|
||||
|
||||
const neueste = versionen[0];
|
||||
const verfuegbar = neueste.id !== mp.versionId;
|
||||
return {
|
||||
ok: true,
|
||||
verfuegbar,
|
||||
name: mp.name,
|
||||
aktuelleNummer: mp.versionNummer || '?',
|
||||
neueNummer: neueste.version_number,
|
||||
neueVersionId: neueste.id,
|
||||
hinweise: neueste.changelog || '',
|
||||
spielVersion: (neueste.game_versions || [])[0] || '',
|
||||
};
|
||||
} catch (err) {
|
||||
return { ok: false, message: 'Suche fehlgeschlagen: ' + err.message };
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
* Spielt eine neuere Fassung ein. Dateien der alten Fassung, die es nicht mehr
|
||||
* gibt, werden entfernt; Welten, Bildschirmfotos und Optionen bleiben.
|
||||
*/
|
||||
ipcMain.handle('modpack:update', async (_e, id, versionId) => {
|
||||
const inst = store.getInstance(id);
|
||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
const mp = inst.modpack;
|
||||
if (!mp || !mp.projectId) return { ok: false, message: 'Kein Modpack hinterlegt.' };
|
||||
|
||||
try {
|
||||
const vres = await fetch(`https://api.modrinth.com/v2/version/${versionId}`,
|
||||
{ headers: { 'User-Agent': 'AeroMC-Launcher', Accept: 'application/json' } });
|
||||
if (!vres.ok) return { ok: false, message: 'Fassung nicht abrufbar (HTTP ' + vres.status + ').' };
|
||||
const version = await vres.json();
|
||||
const datei = (version.files || []).find((f) => f.primary) || (version.files || [])[0];
|
||||
if (!datei || !datei.url) return { ok: false, message: 'Diese Fassung enthält keine Paketdatei.' };
|
||||
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('launch:progress', { id, phase: 'modpack', detail: 'Neue Fassung wird geladen …' });
|
||||
}
|
||||
const dl = await fetch(datei.url, { headers: { 'User-Agent': 'AeroMC-Launcher' } });
|
||||
if (!dl.ok) return { ok: false, message: 'Download fehlgeschlagen (HTTP ' + dl.status + ').' };
|
||||
const buf = Buffer.from(await dl.arrayBuffer());
|
||||
|
||||
const zip = forge.readZipIndex(buf);
|
||||
if (!zip['modrinth.index.json']) return { ok: false, message: 'Die geladene Datei ist kein .mrpack.' };
|
||||
const index = JSON.parse(forge.readZipEntry(buf, zip['modrinth.index.json']).toString('utf8').replace(/^/, ''));
|
||||
|
||||
const gameDir = store.gameDir(id);
|
||||
const neu = await spieleMrpackEin(buf, zip, index, gameDir, { schonePlayerDaten: true });
|
||||
|
||||
// Was die alte Fassung mitbrachte und die neue nicht mehr kennt, muss weg –
|
||||
// sonst bleiben alte Mods liegen und der Loader stolpert darüber.
|
||||
const behalten = new Set(neu.dateien.map((d) => d.toLowerCase()));
|
||||
const entfernt = [];
|
||||
for (const alt of mp.dateien || []) {
|
||||
if (behalten.has(String(alt).toLowerCase())) continue;
|
||||
if (SPIELERDATEN.some((r) => r.test(alt))) continue;
|
||||
const p = path.join(gameDir, String(alt).replace(/\//g, path.sep));
|
||||
if (!p.startsWith(gameDir + path.sep)) continue;
|
||||
try { if (fs.existsSync(p)) { fs.rmSync(p, { force: true }); entfernt.push(alt); } } catch { /* egal */ }
|
||||
}
|
||||
|
||||
// Minecraft-Version und Loader können sich mit der neuen Fassung ändern
|
||||
const deps = index.dependencies || {};
|
||||
let loader = 'vanilla'; let loaderVersion = '';
|
||||
if (deps['fabric-loader']) { loader = 'fabric'; loaderVersion = deps['fabric-loader']; }
|
||||
else if (deps['quilt-loader']) { loader = 'quilt'; loaderVersion = deps['quilt-loader']; }
|
||||
else if (deps.forge) { loader = 'forge'; loaderVersion = deps.forge; }
|
||||
else if (deps.neoforge) { loader = 'neoforge'; loaderVersion = deps.neoforge; }
|
||||
|
||||
store.updateInstance(id, {
|
||||
minecraft: { version: deps.minecraft || inst.minecraft.version, loader, loaderVersion },
|
||||
modpack: Object.assign({}, mp, {
|
||||
versionId: version.id,
|
||||
versionNummer: version.version_number,
|
||||
dateien: neu.dateien,
|
||||
}),
|
||||
});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
neueNummer: version.version_number,
|
||||
eingespielt: neu.dateien.length,
|
||||
entfernt: entfernt.length,
|
||||
spielVersion: deps.minecraft || '',
|
||||
};
|
||||
} catch (err) {
|
||||
return { ok: false, message: 'Aktualisierung fehlgeschlagen: ' + err.message };
|
||||
}
|
||||
});
|
||||
|
||||
// ---- Modpack-Import (CurseForge .zip mit manifest.json) ----
|
||||
|
||||
// "forge-47.2.0" / "fabric-0.15.7" -> { loader, loaderVersion }
|
||||
|
||||
Reference in New Issue
Block a user