diff --git a/src/main.js b/src/main.js index a79896d..29f761d 100644 --- a/src/main.js +++ b/src/main.js @@ -222,6 +222,45 @@ let letzterUpdateCheck = { checking: true }; // Hauptfenster hat seine Empfänger registriert – erst dann den Update-Hinweis schicken const hauptfensterBereit = new Promise((resolve) => ipcMain.once('renderer:ready', resolve)); +/* + * Update und laufendes Spiel + * -------------------------- + * Ein Update beendet den Launcher (und der Installer ersetzt dessen Dateien). Das darf nie + * passieren, solange Minecraft läuft: Das Update wird erst installiert, wenn kein Spiel mehr + * läuft. Damit es trotzdem nicht untergeht, bekommt der Spieler im Spiel eine Einblendung + * (wie bei den Freunde-Hinweisen), und nach dem letzten Spielende öffnet sich der Update-Dialog. + */ +const spielLaeuft = () => laufendeInstanzen.size > 0; +const SPIEL_LAEUFT_TEXT = 'Minecraft läuft noch – das Update wird erst installiert, wenn kein Spiel mehr läuft. Bitte zuerst das Spiel beenden.'; +let updateWartetAufSpielende = false; // Hinweis kam im Spiel -> nach Spielende den Dialog zeigen +let angekuendigteVersion = null; // Dialog schon gezeigt, nicht bei jeder Prüfung wieder +const updateImSpielGemeldet = new Set(); // pid des Spiels, das den Hinweis schon bekam + +// Einblendung auf dem Minecraft-Fenster, einmal je Spielstart +async function meldeUpdateImSpiel() { + const info = pendingUpdateInfo; + if (!info || !info.available || !spielLaeuft()) return; + for (const pid of [...laufendePid.values()]) { + if (updateImSpielGemeldet.has(pid)) continue; + // false, solange das Spielfenster noch nicht da ist – dann beim nächsten Durchlauf erneut + const gezeigt = await zeigeIngameOverlay('AeroMC-Update', [ + `AeroMC ${info.latestVersion || 'Update'} ist verfügbar.`, + 'Bitte nach dem Spiel installieren.', + ], pid, 10000).catch(() => false); + // nur vermerken, wenn dieses Spiel noch läuft – sonst bliebe nach seinem Ende ein falscher Zustand zurück + if (gezeigt && spielLaeuft() && [...laufendePid.values()].includes(pid)) { updateImSpielGemeldet.add(pid); updateWartetAufSpielende = true; } + } +} + +// Dialog zeigen – aber nie, während ein Spiel läuft (er würde dem Spiel den Fokus nehmen) +function kuendigeUpdateAn(info) { + if (!info || !info.available) return; + if (spielLaeuft()) { updateWartetAufSpielende = true; meldeUpdateImSpiel().catch(() => {}); return; } + if (info.latestVersion && info.latestVersion === angekuendigteVersion) return; + angekuendigteVersion = info.latestVersion || true; + broadcast('update:available', info); +} + async function pruefeAufUpdates() { letzterUpdateCheck = { checking: true }; broadcast('update:checked', letzterUpdateCheck); @@ -357,7 +396,15 @@ ipcMain.handle('recording:getServer', (_e, id) => letzteQuickPlayServer.get(id) 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]); } +function meldeLaufende() { + broadcast('game:laufend', [...laufendeInstanzen]); + // letztes Spiel beendet und ein Update wartet -> jetzt den Update-Dialog zeigen + if (!spielLaeuft() && updateWartetAufSpielende) { + updateWartetAufSpielende = false; + updateImSpielGemeldet.clear(); + kuendigeUpdateAn(pendingUpdateInfo); + } +} 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 @@ -1169,8 +1216,13 @@ app.whenReady().then(() => { pruefeAufUpdates().then(async (info) => { if (!info.available) return; await hauptfensterBereit; - broadcast('update:available', info); + kuendigeUpdateAn(info); }).catch(() => {}); + // Der Launcher bleibt oft tagelang offen: alle 3 Stunden erneut prüfen. Läuft ein Spiel, + // gibt es nur die Einblendung im Spiel, den Dialog erst nach Spielende. + setInterval(() => { pruefeAufUpdates().then(kuendigeUpdateAn).catch(() => {}); }, 3 * 60 * 60 * 1000); + // Hinweis im Spiel, sobald das Spielfenster da ist (auch für ein später gestartetes Spiel) + setInterval(() => { meldeUpdateImSpiel().catch(() => {}); }, 30000); // Anmelde-Dienste automatisch überwachen (sofort + alle 2 Minuten) setTimeout(() => checkAuthServices(), 2500); @@ -1230,7 +1282,7 @@ ipcMain.handle('settings:save', (_e, patch) => { // Beta-Teilnahme umgeschaltet -> gleich neu nach Updates suchen if (patch && Object.prototype.hasOwnProperty.call(patch, 'betaTeilnahme') && !!patch.betaTeilnahme !== betaStand) { betaStand = !!patch.betaTeilnahme; - pruefeAufUpdates().then((info) => { if (info.available) broadcast('update:available', info); }).catch(() => {}); + pruefeAufUpdates().then((info) => { if (info.available) { angekuendigteVersion = null; kuendigeUpdateAn(info); } }).catch(() => {}); } if (patch && Object.prototype.hasOwnProperty.call(patch, 'friendsEnabled')) { aktualisiereFreundeTastenkuerzel(); @@ -5575,11 +5627,15 @@ ipcMain.handle('update:lastCheck', () => letzterUpdateCheck); // beliebige Datei als "Update" ausführen lassen. ipcMain.handle('update:install', async () => { try { + // Nie während Minecraft läuft: Der Launcher beendet sich für das Update + if (spielLaeuft()) { updateWartetAufSpielende = true; return { ok: false, spielLaeuft: true, message: SPIEL_LAEUFT_TEXT }; } const asset = pendingUpdateInfo && pendingUpdateInfo.available ? pendingUpdateInfo.asset : null; if (!asset) throw new Error('Kein geprüftes Update vorhanden – bitte erneut nach Updates suchen.'); const file = await updater.downloadAndInstall(asset, (p) => { broadcast('update:progress', p); }); + // Während des Downloads kann ein Spiel gestartet worden sein – die Datei bleibt liegen + if (spielLaeuft()) { updateWartetAufSpielende = true; return { ok: false, spielLaeuft: true, message: SPIEL_LAEUFT_TEXT }; } const res = await updater.applyUpdate(file); // Portable: der Tausch klappt erst, wenn der Launcher die EXE freigibt setTimeout(() => app.quit(), 1200); diff --git a/src/renderer.js b/src/renderer.js index 0dcf373..9ad5cdf 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -1711,7 +1711,24 @@ function showUpdateDialog(info) { : (channel ? `Für den Kanal ${channel} liegt in diesem Release keine passende Datei bereit. Bitte im Release nachsehen und von Hand aktualisieren.` : 'Die Paketart dieses Launchers lässt sich nicht sicher bestimmen. Bitte das Update von Hand aus dem Release laden.'); + updHintNormal = el('upd-hint').textContent; show('modal-update'); + aktualisiereUpdateKnopf(); +} + +// Solange Minecraft läuft, wird nicht installiert: Der Launcher beendet sich für das Update +let updHintNormal = ''; +let updInstalliertGerade = false; +async function aktualisiereUpdateKnopf() { + if (!pendingUpdate || !pendingUpdate.asset || updInstalliertGerade) return; + let laeuft = false; + try { laeuft = ((await window.api.laufendeInstanzen()) || []).length > 0; } catch { /* dann nicht sperren */ } + const btn = el('upd-install'); + btn.disabled = laeuft; + btn.textContent = laeuft ? 'Minecraft läuft noch' : 'Installieren'; + el('upd-hint').textContent = laeuft + ? 'Das Update wird erst installiert, wenn kein Minecraft mehr läuft. Beende das Spiel – danach kannst du hier auf „Installieren“ klicken.' + : updHintNormal; } // Links in den Update-Hinweisen im Browser öffnen, nie im Launcher-Fenster @@ -1731,10 +1748,19 @@ async function startUpdate() { const btn = el('upd-install'); btn.disabled = true; btn.textContent = 'Wird installiert …'; + updInstalliertGerade = true; el('upd-progress').classList.remove('hidden'); el('upd-status').textContent = 'Wird heruntergeladen …'; const res = await window.api.updateInstall(); + if (!res.ok) updInstalliertGerade = false; + if (!res.ok && res.spielLaeuft) { + // Spiel lief schon oder ist während des Downloads gestartet worden + el('upd-progress').classList.add('hidden'); + el('upd-status').textContent = ''; + aktualisiereUpdateKnopf(); + return; + } if (res.ok) { el('upd-status').textContent = res.kind === 'portable' ? 'Launcher schließt sich, wird ersetzt und startet automatisch neu.' @@ -7849,6 +7875,8 @@ window.addEventListener('DOMContentLoaded', async () => { } el('upd-notes').addEventListener('click', updateLinkKlick); window.api.onLaufendeInstanzen(setzeLaufende); + // Update-Dialog offen: Knopf freigeben, sobald das letzte Spiel beendet ist (bzw. sperren, wenn eins startet) + window.api.onLaufendeInstanzen(() => { if (!el('modal-update').classList.contains('hidden')) aktualisiereUpdateKnopf(); }); window.api.laufendeInstanzen().then(setzeLaufende).catch(() => {}); el('grid').addEventListener('mousemove', kippeZurMaus); baueLichtpunkte();