Upload via GUI (146 Dateien)

This commit is contained in:
2026-10-05 17:20:31 +00:00
parent 93c74f94a1
commit 6ef51edede
13 changed files with 1734 additions and 416 deletions
+277 -10
View File
@@ -31,6 +31,9 @@ const bedrock = require('./bedrock');
const backup = require('./backup');
const mmcimport = require('./mmcimport');
const kopieren = require('./kopieren');
const moderkennung = require('./moderkennung');
const garderobe = require('./garderobe');
const erfolge = require('./erfolge');
const cfimport = require('./cfimport');
const mrimport = require('./mrimport');
const mcimport = require('./mcimport');
@@ -121,9 +124,9 @@ const POPOUT_SPEC = {
login: { title: 'AeroMC – Konten', width: 460, height: 460, minWidth: 420, minHeight: 380 },
import: { title: 'AeroMC – Import', width: 860, height: 680, minWidth: 640, minHeight: 480 },
instance: { title: 'AeroMC – Instanz', width: 640, height: 760, minWidth: 520, minHeight: 500 },
update: { title: 'AeroMC – Update', width: 460, height: 460, minWidth: 420, minHeight: 380 },
update: { title: 'AeroMC – Update', width: 600, height: 640, minWidth: 440, minHeight: 440 },
record: { title: 'AeroMC – Spielaufzeichnung', width: 960, height: 800, minWidth: 640, minHeight: 560 },
friends: { title: 'AeroMC – Freunde', width: 420, height: 560, minWidth: 360, minHeight: 420 },
friends: { title: 'AeroMC – Freunde', width: 780, height: 620, minWidth: 360, minHeight: 480 },
stats: { title: 'AeroMC – Spielzeit-Statistik', width: 640, height: 560, minWidth: 520, minHeight: 420 },
};
const POPOUT_DEFAULT = { title: 'AeroMC', width: 860, height: 680, minWidth: 640, minHeight: 480 };
@@ -262,6 +265,10 @@ const VERBINDUNGS_ZEILE = /Connecting to ([^,]+),\s*(\d+)/i;
ipcMain.handle('recording:getServer', (_e, id) => letzteQuickPlayServer.get(id) || null);
const laufendePid = new Map(); // instanceId -> pid des zuletzt gestarteten Minecraft-Prozesses
// Instanzen, deren Spiel gerade läuft (für den pulsierenden Rand der Karte)
const laufendeInstanzen = new Set();
function meldeLaufende() { broadcast('game:laufend', [...laufendeInstanzen]); }
ipcMain.handle('game:laufend', () => [...laufendeInstanzen]);
const kontoPid = new Map(); // kontoId -> pid, für den Ingame-Toast: der muss auf DEM Fenster
// des prüfenden Kontos erscheinen, nicht auf irgendeinem laufenden Minecraft-Fenster
// (sonst sieht z. B. M_Viper eine Meldung über sich selbst, weil zufällig sein
@@ -946,6 +953,9 @@ app.whenReady().then(() => {
// selbst geholte Laufzeiten: ablegen und bei der Java-Suche mitberücksichtigen
services.setManagedJavaDir(javadl.setJavaDir(path.join(app.getPath('userData'), 'java')));
// Konten verschlüsselt speichern (Windows-DPAPI, an das Benutzerkonto gebunden)
moderkennung.init(app.getPath('userData'));
garderobe.init(app.getPath('userData'));
erfolge.init(app.getPath('userData'));
auth.init(app.getPath('userData'), {
available: () => { try { return safeStorage.isEncryptionAvailable(); } catch { return false; } },
encrypt: (s) => safeStorage.encryptString(s).toString('base64'),
@@ -1131,7 +1141,14 @@ ipcMain.handle('mods:search', async (_e, opts) => {
}
});
ipcMain.handle('mods:list', (_e, id, art) => store.listMods(id, art || 'mod'));
// Liste der installierten Inhalte – von Hand kopierte/importierte Mods werden
// per Prüfsumme bei Modrinth erkannt (Katalog zeigt dann „✓ Installiert“)
async function modsMitErkennung(id, art) {
const liste = store.listMods(id, art);
try { return await moderkennung.ergaenze(liste, store.inhaltsDir(id, art)); }
catch { return liste; }
}
ipcMain.handle('mods:list', (_e, id, art) => modsMitErkennung(id, art || 'mod'));
ipcMain.handle('mods:install', (_e, id, project, art) => installiereInhalt(id, project, art));
@@ -1151,7 +1168,7 @@ async function installiereInhalt(id, project, art) {
}
const modsDir = store.inhaltsDir(id, art || 'mod');
const done = new Set(store.listMods(id, art || 'mod').map((m) => m.projectId));
const done = new Set((await modsMitErkennung(id, art || 'mod')).map((m) => m.projectId));
const added = [];
let firstError = null;
const istCf = !!(project && (project.source === 'curseforge' || String(project.projectId || '').startsWith('cf-')));
@@ -1210,6 +1227,7 @@ async function installiereInhalt(id, project, art) {
await installOne(project, false);
if (!added.length) return { ok: false, message: firstError || 'Installation fehlgeschlagen.' };
pruefeErfolge();
return { ok: true, entry: added[0], added, depCount: added.length - 1 };
}
@@ -1568,6 +1586,16 @@ ipcMain.handle('crash:fix', async (_e, id, aktion) => {
broadcast('instances:changed');
return { ok: true, text: 'Beim nächsten Start wählt AeroMC das passende Java und lädt es bei Bedarf herunter.' };
}
case 'forge-aktualisieren': {
// neueste Forge-Version für dieselbe Minecraft-Version eintragen
const liste = await loaders.listLoaderVersions(inst.minecraft.loader || 'forge', inst.minecraft.version);
const neueste = liste && liste[0] && liste[0].version;
if (!neueste) return { ok: false, message: 'Keine neuere Forge-Version gefunden.' };
if (neueste === inst.minecraft.loaderVersion) return { ok: false, message: `Forge ${neueste} ist bereits die neueste Version für Minecraft ${inst.minecraft.version}.` };
store.updateInstance(id, { minecraft: { loaderVersion: neueste } });
broadcast('instances:changed');
return { ok: true, text: `Forge auf ${neueste} aktualisiert (vorher ${inst.minecraft.loaderVersion || 'unbekannt'}). Beim nächsten Start wird es eingerichtet.` };
}
case 'mods-ordner': {
const dir = store.inhaltsDir(id, 'mod');
fs.mkdirSync(dir, { recursive: true });
@@ -1582,6 +1610,115 @@ ipcMain.handle('crash:fix', async (_e, id, aktion) => {
}
});
// ---------- Erfolge ----------
// Werte aus dem, was der Launcher weiß; mehrere Anlässe kurz hintereinander
// werden zu einer Prüfung zusammengefasst.
let erfolgMerker = {};
let erfolgFreunde = 0;
let erfolgTimer = null;
function pruefeErfolge(merker) {
Object.assign(erfolgMerker, merker || {});
clearTimeout(erfolgTimer);
erfolgTimer = setTimeout(() => {
try {
const alle = store.listInstances();
const gespielt = alle.filter((i) => (i.launchCount || 0) > 0);
let mods = 0;
for (const i of alle) {
if (!i.minecraft || !i.minecraft.loader || i.minecraft.loader === 'vanilla') continue;
try { mods += store.listMods(i.id, 'mod').filter((m) => !m.fehlt).length; } catch { /* egal */ }
}
const werte = {
starts: alle.reduce((s, i) => s + (i.launchCount || 0), 0),
sekunden: alle.reduce((s, i) => s + (i.totalPlaySeconds || 0), 0),
instanzen: alle.length,
versionen: new Set(gespielt.map((i) => i.minecraft && i.minecraft.version).filter(Boolean)).size,
mods,
modpacks: alle.filter((i) => i.modpack && (i.modpack.projectId || i.modpack.name)).length,
freunde: erfolgFreunde,
};
const merkerJetzt = erfolgMerker;
erfolgMerker = {};
const { neu, ersteAuswertung } = erfolge.pruefe(werte, merkerJetzt);
if (!neu.length) return;
broadcast('erfolge:neu', { liste: neu, ersteAuswertung });
if (neu.length === 1) notify('Erfolg erzielt!', `${neu[0].titel} – ${neu[0].text}`);
else notify(`${neu.length} Erfolge erzielt!`, neu.map((e) => e.titel).join(', '));
} catch { /* Erfolge sind Zierde */ }
}, 1500);
}
ipcMain.handle('erfolge:liste', () => erfolge.liste());
// Hauptfenster bereit -> einmal prüfen (u. a. für bestehende Nutzer nach dem Update)
hauptfensterBereit.then(() => setTimeout(async () => {
// Freunde einmal zählen (sonst erst beim Öffnen des Freunde-Fensters)
try {
const konto = store.getSettings().friendsEnabled !== false ? aktivesFreundeKonto() : null;
if (konto) {
const res = await friends.getFriends(konto.uuid);
if (res.ok && Array.isArray(res.data)) erfolgFreunde = res.data.length;
}
} catch { /* offline */ }
pruefeErfolge();
}, 4000));
// ---------- Leistungs-Paket: bewährte Performance-Mods mit einem Klick ----------
// Je Platz die erste Mod, die es für Version + Loader gibt (z. B. Sodium, sonst
// Embeddium). Kurznamen werden erst in Modrinth-IDs umgewandelt – installierte
// Mods (auch von Hand kopierte, siehe moderkennung) tragen diese IDs.
const LEISTUNGS_PAKET = [
{ zweck: 'Grafik', mods: ['sodium', 'embeddium', 'rubidium'], grafik: true },
{ zweck: 'Spiellogik', mods: ['lithium', 'radium'] },
{ zweck: 'Arbeitsspeicher', mods: ['ferrite-core'] },
{ zweck: 'Unsichtbares nicht zeichnen', mods: ['entityculling'] },
{ zweck: 'Oberfläche/Text', mods: ['immediatelyfast'] },
{ zweck: 'Ladezeit', mods: ['modernfix'] },
{ zweck: 'Weniger Last im Hintergrund', mods: ['dynamic-fps'] },
];
ipcMain.handle('mods:leistungsPaket', async (_e, id) => {
const inst = store.getInstance(id);
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
const loader = inst.minecraft.loader;
if (!loader || loader === 'vanilla' || loader === 'optifine') {
return { ok: false, message: 'Das Leistungs-Paket braucht einen Mod-Loader (Fabric, Quilt, Forge oder NeoForge).' };
}
// Kurznamen -> Modrinth-IDs (ein Abruf für alle)
const slugs = [...new Set(LEISTUNGS_PAKET.flatMap((p) => p.mods))];
let ids = {};
try {
const res = await fetch(`https://api.modrinth.com/v2/projects?ids=${encodeURIComponent(JSON.stringify(slugs))}`, { headers: { 'User-Agent': 'AeroMC-Launcher' } });
for (const p of await res.json()) ids[p.slug] = { id: p.id, title: p.title };
} catch { return { ok: false, message: 'Modrinth ist gerade nicht erreichbar.' }; }
const vorhanden = await modsMitErkennung(id, 'mod');
const hatOptifine = vorhanden.some((m) => m.enabled !== false && /optifine|optifabric/i.test(String(m.filename) + ' ' + String(m.title)));
const installiert = new Set(vorhanden.map((m) => m.projectId).filter(Boolean));
const ergebnis = { neu: [], schonDa: [], nichtVerfuegbar: [], uebersprungen: [] };
let nr = 0;
for (const platz of LEISTUNGS_PAKET) {
nr++;
if (platz.grafik && hatOptifine) { ergebnis.uebersprungen.push(`${platz.zweck} (OptiFine ist installiert)`); continue; }
const kandidaten = platz.mods.map((s) => ids[s]).filter(Boolean);
const da = kandidaten.find((k) => installiert.has(k.id));
if (da) { ergebnis.schonDa.push(da.title); continue; }
let geschafft = false;
for (const k of kandidaten) {
broadcast('mods:paketFortschritt', { nr, von: LEISTUNGS_PAKET.length, titel: k.title });
const r = await installiereInhalt(id, { projectId: k.id, title: k.title }, 'mod').catch((err) => ({ ok: false, message: err.message }));
if (r.ok) {
ergebnis.neu.push(k.title);
for (const a of r.added || []) if (a.projectId) installiert.add(a.projectId);
geschafft = true;
break;
}
}
if (!geschafft) ergebnis.nichtVerfuegbar.push(kandidaten.map((k) => k.title).join(' / ') || platz.mods[0]);
}
if (ergebnis.neu.length) broadcast('instances:changed');
if (ergebnis.neu.length || ergebnis.schonDa.length) pruefeErfolge({ leistung: true });
return Object.assign({ ok: true }, ergebnis);
});
// nach einer Lösung gleich erneut starten (Start läuft im Hauptfenster)
ipcMain.handle('crash:relaunch', (_e, id) => {
if (!store.getInstance(id)) return false;
@@ -1856,6 +1993,7 @@ ipcMain.handle('skin:apply', async (_e, dateiPfad, variante) => {
const token = await aktivesToken();
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
try {
await sichereAktuellenSkin(token);
const res = await skins.setzeSkin(token, dateiPfad, variante);
if (res && res.ok) { letzterSkinWechsel = Date.now(); armformCache.clear(); broadcast('skin:changed'); }
return res;
@@ -1867,6 +2005,7 @@ ipcMain.handle('skin:reset', async () => {
const token = await aktivesToken();
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
try {
await sichereAktuellenSkin(token);
const res = await skins.setzeSkinZurueck(token);
if (res && res.ok) { letzterSkinWechsel = Date.now(); armformCache.clear(); broadcast('skin:changed'); }
return res;
@@ -1874,6 +2013,106 @@ ipcMain.handle('skin:reset', async () => {
catch (err) { return { ok: false, message: 'Zurücksetzen fehlgeschlagen: ' + err.message }; }
});
// ---------- Passt die Armform zum Skin? ----------
// Steht das Konto auf "schmal", die Skin-Datei ist aber für breite Arme gemalt
// (oder umgekehrt), sieht man im Spiel gequetschte Arme bzw. dunkle Streifen.
async function aktuelleSkinDatei(token) {
const p = await profilMitSpielSkin(token);
if (!p.ok || !p.skinUrl) return null;
const res = await fetch(p.skinUrl);
if (!res.ok) return null;
const daten = Buffer.from(await res.arrayBuffer());
const bild = nativeImage.createFromBuffer(daten);
const { width, height } = bild.getSize();
return { daten, konto: p.variante === 'slim' ? 'slim' : 'classic', datei: skins.armformDerTextur(bild.toBitmap(), width, height) };
}
ipcMain.handle('skin:armformPruefen', async () => {
const token = await aktivesToken();
if (!token) return { ok: false };
try {
const s = await aktuelleSkinDatei(token);
if (!s) return { ok: false };
return { ok: true, konto: s.konto, datei: s.datei, passt: !s.datei || s.datei === s.konto };
} catch { return { ok: false }; }
});
ipcMain.handle('skin:armformKorrigieren', async () => {
const token = await aktivesToken();
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
const tmp = path.join(app.getPath('temp'), 'aeromc-skin-armform.png');
try {
const s = await aktuelleSkinDatei(token);
if (!s || !s.datei) return { ok: false, message: 'Die Armform der Skin-Datei ließ sich nicht eindeutig bestimmen.' };
if (s.datei === s.konto) return { ok: true, variante: s.konto, unveraendert: true };
fs.writeFileSync(tmp, s.daten);
// derselbe Skin, nur mit der passenden Armform neu hochgeladen
const res = await skins.setzeSkin(token, tmp, s.datei);
if (res && res.ok) { letzterSkinWechsel = Date.now(); armformCache.clear(); broadcast('skin:changed', { quelle: 'armform' }); }
return res && res.ok ? { ok: true, variante: s.datei } : res;
} catch (err) {
return { ok: false, message: 'Umstellen fehlgeschlagen: ' + err.message };
} finally {
try { fs.unlinkSync(tmp); } catch { /* egal */ }
}
});
// ---------- Garderobe (gespeicherte Skins und Umhänge) ----------
// Bisherigen Skin vor einem Wechsel sichern – Mojang merkt sich keine alten Skins.
// Klappt das nicht (offline …), wird trotzdem gewechselt.
async function sichereAktuellenSkin(token, name, automatisch = true) {
try {
const s = await aktuelleSkinDatei(token);
if (!s) return null;
// Armform aus der Datei, wenn eindeutig – sonst die des Kontos (so kommt beim
// Wieder-Anziehen nicht ein falsch eingestellter, gequetschter Zustand zurück)
return garderobe.hinzufuegen({ art: 'skin', daten: s.daten, variante: s.datei || s.konto, automatisch,
name: name || 'Gesichert am ' + new Date().toLocaleDateString('de-DE') });
} catch { return null; }
}
ipcMain.handle('garderobe:liste', () => garderobe.liste());
ipcMain.handle('garderobe:skinSpeichern', async (_e, name) => {
const token = await aktivesToken();
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
const r = await sichereAktuellenSkin(token, name, false);
if (!r) return { ok: false, message: 'Dein Konto nutzt den Standard-Skin oder er ist gerade nicht abrufbar.' };
return { ok: true, neu: r.neu, name: r.eintrag.name };
});
ipcMain.handle('garderobe:umhangSpeichern', async (_e, name) => {
const konto = aktivesFreundeKonto();
if (!konto) return { ok: false, message: 'Kein angemeldetes Konto.' };
const r = await umhangFuer(konto.uuid);
if (!r || !r.dataUri || r.quelle !== 'aeromc') return { ok: false, message: 'Du trägst gerade keinen eigenen AeroMC-Umhang.' };
const daten = Buffer.from(r.dataUri.split(',')[1], 'base64');
const e = garderobe.hinzufuegen({ art: 'umhang', daten, name: name || 'Umhang', automatisch: false });
return { ok: true, neu: e.neu, name: e.eintrag.name };
});
ipcMain.handle('garderobe:anziehen', async (_e, id) => {
const eintrag = garderobe.holen(id);
if (!eintrag) return { ok: false, message: 'Eintrag nicht gefunden.' };
if (eintrag.art === 'umhang') {
const konto = aktivesFreundeKonto();
if (!konto) return { ok: false, message: 'Kein angemeldetes Konto.' };
const base64 = fs.readFileSync(eintrag.pfad).toString('base64');
const res = await friends.uploadCape(konto.uuid, base64);
if (res.ok) { umhangCache.clear(); broadcast('skin:changed', { quelle: 'umhang' }); }
return res.ok ? { ok: true } : umhangFehler(res);
}
const token = await aktivesToken();
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
await sichereAktuellenSkin(token);
const res = await skins.setzeSkin(token, eintrag.pfad, eintrag.variante);
if (res && res.ok) { letzterSkinWechsel = Date.now(); armformCache.clear(); broadcast('skin:changed', { quelle: 'garderobe' }); }
return res && res.ok ? { ok: true } : (res || { ok: false, message: 'Skin konnte nicht gesetzt werden.' });
});
ipcMain.handle('garderobe:umbenennen', (_e, id, name) => ({ ok: garderobe.umbenennen(id, name) }));
ipcMain.handle('garderobe:loeschen', (_e, id) => ({ ok: garderobe.loeschen(id) }));
// ---------- Skin-Editor ----------
// Der Editor malt in eine Canvas – fremde Bildadressen (textures.minecraft.net)
// würden sie "verunreinigen" und das Auslesen der Pixel verbieten. Deshalb holt
@@ -2017,7 +2256,7 @@ ipcMain.handle('cape:upload', async (_e, base64) => {
if (!konto) return { ok: false, message: 'Kein angemeldetes Konto.' };
if (!umhangPngAusBase64(base64)) return { ok: false, message: 'Ungültiger Umhang (PNG mit 64×32 bis 1024×512 Pixeln nötig).' };
const res = await friends.uploadCape(konto.uuid, base64);
if (res.ok) { umhangCache.clear(); broadcast('skin:changed', { quelle: 'umhang' }); }
if (res.ok) { umhangCache.clear(); broadcast('skin:changed', { quelle: 'umhang' }); pruefeErfolge({ umhang: true }); }
return res.ok ? { ok: true, cape: res.data && res.data.cape } : umhangFehler(res);
});
@@ -2100,8 +2339,9 @@ ipcMain.handle('skin:applyData', async (_e, base64, variante) => {
const tmp = path.join(app.getPath('temp'), 'aeromc-skin-editor.png');
try {
fs.writeFileSync(tmp, daten);
await sichereAktuellenSkin(token);
const res = await skins.setzeSkin(token, tmp, variante);
if (res && res.ok) { letzterSkinWechsel = Date.now(); armformCache.clear(); broadcast('skin:changed', { quelle: 'editor' }); }
if (res && res.ok) { letzterSkinWechsel = Date.now(); armformCache.clear(); broadcast('skin:changed', { quelle: 'editor' }); pruefeErfolge({ 'skin-editor': true }); }
return res;
} catch (err) {
return { ok: false, message: 'Skin konnte nicht gesetzt werden: ' + err.message };
@@ -2717,6 +2957,7 @@ ipcMain.handle('friends:list', async () => {
const konto = aktivesFreundeKonto();
if (!konto) return { ok: false, message: 'Kein Konto angemeldet.' };
const res = await friends.getFriends(konto.uuid);
if (res.ok && Array.isArray(res.data) && res.data.length > erfolgFreunde) { erfolgFreunde = res.data.length; pruefeErfolge(); }
return res.ok ? { ok: true, list: await mitNamen(res.data) } : res;
});
@@ -3537,6 +3778,22 @@ ipcMain.handle('servers:shortcut', (_e, id, ip, name, wunschName) => {
* Instanz), unabhängig davon ob der Pfad aus einem Dateidialog oder per
* Drag&Drop aufs Fenster kommt.
*/
// Inhalt eines (eigenen, temporären) Ordners in den Zielordner übernehmen:
// verschieben statt kopieren – auf demselben Laufwerk ist das sofort erledigt,
// sonst (anderes Laufwerk) wird im Hintergrund kopiert. Nichts davon blockiert.
async function verschiebeInhalt(quelle, ziel, nur) {
await fs.promises.mkdir(ziel, { recursive: true });
for (const name of nur || await fs.promises.readdir(quelle)) {
const von = path.join(quelle, name);
const nach = path.join(ziel, name);
try {
await fs.promises.rename(von, nach);
} catch {
await kopieren.kopiere(von, nach, null, null);
}
}
}
async function importInstanceZipFromPath(filePath) {
const tmp = path.join(app.getPath('temp'), 'vn-import-' + Date.now());
try {
@@ -3556,8 +3813,10 @@ async function importInstanceZipFromPath(filePath) {
if (!fs.existsSync(path.join(base, 'instance.json'))) {
const mmc = mmcimport.scanSingle(tmp);
if (mmc.ok) {
const done = mmcimport.importOne(store, mmc.instance, true);
return { ok: true, instance: store.getInstance(done.id), quelle: 'multimc', copied: done.copied };
const done = mmcimport.importOne(store, mmc.instance, false);
const quellen = mmcimport.kopierQuellen(mmc.instance);
if (quellen.length) await verschiebeInhalt(mmc.instance.gameDir, store.gameDir(done.id), quellen.map((q) => q.name));
return { ok: true, instance: store.getInstance(done.id), quelle: 'multimc', copied: quellen.length > 0 };
}
throw new Error('Keine gültige Instanz im Archiv (weder instance.json noch instance.cfg).');
}
@@ -3571,7 +3830,7 @@ async function importInstanceZipFromPath(filePath) {
if (imported.java) store.updateInstance(created.id, { java: imported.java });
const mcSrc = path.join(base, 'minecraft');
if (fs.existsSync(mcSrc)) fs.cpSync(mcSrc, store.gameDir(created.id), { recursive: true });
if (fs.existsSync(mcSrc)) await verschiebeInhalt(mcSrc, store.gameDir(created.id));
const modsSrc = path.join(base, 'mods.json');
if (fs.existsSync(modsSrc)) fs.copyFileSync(modsSrc, path.join(store.instancePath(created.id), 'mods.json'));
if (imported.customImage && fs.existsSync(path.join(base, imported.customImage))) {
@@ -3582,7 +3841,8 @@ async function importInstanceZipFromPath(filePath) {
} catch (err) {
return { ok: false, message: 'Import fehlgeschlagen: ' + err.message };
} finally {
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* egal */ }
// Reste im Hintergrund löschen (bei großen Archiven sonst minutenlang blockiert)
fs.promises.rm(tmp, { recursive: true, force: true }).catch(() => { /* egal */ });
}
}
@@ -3895,6 +4155,8 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => {
const res = await launcher.prepareAndLaunch(opts, (evt) => {
if (evt.phase === 'launch') {
launchStart = Date.now();
laufendeInstanzen.add(id);
meldeLaufende();
if (!gezaehlt) { gezaehlt = true; kontoSpieltJetzt(kontoId); }
discordpresence.setActivity({
details: 'Spielt Minecraft',
@@ -3930,6 +4192,8 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => {
herzschlagJetzt = null;
if (kontoId && kontoInstanz.get(kontoId) === id) kontoInstanz.delete(kontoId);
laufendePid.delete(id); // für die "Nur Minecraft"-Tonaufnahme: Prozess ist weg
laufendeInstanzen.delete(id);
meldeLaufende();
// beim nächsten Start dieses Kontos sollen bereits online befindliche
// Freunde wieder gemeldet werden
if (kontoId) { kontoPid.delete(kontoId); freundeOnlineBaseline.delete(kontoId); }
@@ -3944,6 +4208,7 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => {
}
const cur = store.getInstance(id);
if (cur) store.updateInstance(id, { totalPlaySeconds: (cur.totalPlaySeconds || 0) + secs });
pruefeErfolge();
}
}
if (evt.phase === 'log') {
@@ -3970,12 +4235,14 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => {
if (res.ok) {
const aktuell = store.getInstance(id);
store.updateInstance(id, { lastPlayed: new Date().toISOString(), launchCount: ((aktuell && aktuell.launchCount) || 0) + 1 });
pruefeErfolge();
}
if (res.ok && modUpdates && modUpdates.erneuert && modUpdates.erneuert.length) res.modUpdates = modUpdates.erneuert.length;
if (res.ok && res.pid) laufendePid.set(id, res.pid);
if (res.ok && res.pid && kontoId) { kontoPid.set(kontoId, res.pid); kontoInstanz.set(kontoId, id); aktualisiereFreundeTastenkuerzel(); }
return res;
} catch (err) {
if (laufendeInstanzen.delete(id)) meldeLaufende();
if (gezaehlt) { gezaehlt = false; kontoSpieltNichtMehr(kontoId); discordpresence.clearActivity(); }
if (freundeHerzschlag) { clearInterval(freundeHerzschlag); freundeHerzschlag = null; }
if (herzschlagJetzt) { laufendeHerzschlaege.delete(herzschlagJetzt); herzschlagJetzt = null; }