Upload via GUI (194 Dateien)
This commit is contained in:
+541
-23
@@ -7,6 +7,29 @@
|
||||
*/
|
||||
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell, safeStorage, Notification, Tray, Menu, nativeImage, clipboard, desktopCapturer, session, screen, globalShortcut } = require('electron');
|
||||
|
||||
// Vom Setup am Ende der Installation einmal aufgerufen: nur starten und sofort
|
||||
// wieder beenden. Windows prüft die neue EXE dabei (dauert beim ersten Start
|
||||
// mehrere Sekunden) – so passiert das während des Fortschrittsbalkens und der
|
||||
// echte Start nach "Fertigstellen" ist gleich da. Siehe installer/Installer.cs.
|
||||
if (process.argv.includes('--aeromc-vorwaermen')) process.exit(0);
|
||||
|
||||
// Sicherheit: Die fertige App nimmt keine Debug-Schalter an. Mit
|
||||
// --remote-debugging-port oder --inspect könnte jedes andere Programm den
|
||||
// Launcher fernsteuern (Konten, Instanzen, Befehle). Beim Entwickeln (npm start)
|
||||
// bleiben sie erlaubt. Die Node-Varianten (--inspect, NODE_OPTIONS,
|
||||
// ELECTRON_RUN_AS_NODE) sperren zusätzlich die Electron-Fuses, siehe
|
||||
// "electronFuses" in package.json (dort auch die Integritätsprüfung von app.asar).
|
||||
if (app.isPackaged) {
|
||||
const verboten = ['remote-debugging-port', 'remote-debugging-pipe', 'remote-debugging-address', 'inspect', 'inspect-brk', 'inspect-port', 'js-flags'];
|
||||
const gesetzt = verboten.find((s) => app.commandLine.hasSwitch(s))
|
||||
|| process.argv.slice(1).find((a) => /^--(remote-debugging|inspect)/i.test(a));
|
||||
if (gesetzt) {
|
||||
console.error('AeroMC: Debug-Schalter sind in der fertigen App nicht erlaubt (' + gesetzt + ').');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
@@ -26,6 +49,63 @@ const loaders = require('./loaders');
|
||||
const forge = require('./forge');
|
||||
const auth = require('./auth');
|
||||
const updater = require('./updater');
|
||||
const integritaet = require('./integritaet');
|
||||
|
||||
/*
|
||||
* Manipulationsschutz (siehe integritaet.js): Kanäle, über die der Launcher
|
||||
* selbst Mod-Dateien überschreibt oder Startdaten ändert. Vorher wird geprüft
|
||||
* (eine schon manipulierte Instanz darf nicht "mitversiegelt" werden), danach
|
||||
* das Siegel auf den neuen Stand gebracht. Gesperrte Instanzen lassen sich
|
||||
* darüber gar nicht mehr verändern.
|
||||
*/
|
||||
const SIEGEL_KANAELE = new Set([
|
||||
'mods:install', 'mods:addFile', 'mods:applyUpdates', 'mods:leistungsPaket',
|
||||
'instances:installPerformancePack', 'instances:update', 'modpack:update',
|
||||
]);
|
||||
const GESPERRT_TEXT = 'Diese Instanz ist gesperrt, weil sie verändert wurde. Bitte aus einer Sicherung wiederherstellen und die gesperrte Instanz löschen.';
|
||||
{
|
||||
const handleOriginal = ipcMain.handle.bind(ipcMain);
|
||||
ipcMain.handle = (kanal, fn) => handleOriginal(kanal, !SIEGEL_KANAELE.has(kanal) ? fn : async (e, id, ...rest) => {
|
||||
const vorher = await integritaet.pruefe(id);
|
||||
if (vorher.status === 'gesperrt') {
|
||||
if (vorher.neuGesperrt) meldeGesperrt(id);
|
||||
return { ok: false, message: GESPERRT_TEXT };
|
||||
}
|
||||
try {
|
||||
return await fn(e, id, ...rest);
|
||||
} finally {
|
||||
await integritaet.versiegele(id).catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// mit Gesperrt-Status für die Anzeige
|
||||
function mitSperre(inst) {
|
||||
return inst ? Object.assign({}, inst, { gesperrt: integritaet.status(inst.id) }) : inst;
|
||||
}
|
||||
|
||||
function meldeGesperrt(id) {
|
||||
broadcast('instances:changed');
|
||||
const inst = store.getInstance(id);
|
||||
try {
|
||||
new Notification({
|
||||
title: 'AeroMC – Instanz gesperrt',
|
||||
body: `"${(inst && inst.name) || id}" wurde außerhalb des Launchers verändert und ist gesperrt.`,
|
||||
icon: benachrichtigungsLogo || undefined,
|
||||
}).show();
|
||||
} catch { /* nur Hinweis */ }
|
||||
}
|
||||
|
||||
// Beim Launcher-Start alle Instanzen im Hintergrund prüfen (schnell: nur
|
||||
// geänderte Dateien werden neu gehasht). Beim Spielstart wird gründlich geprüft.
|
||||
async function pruefeAlleInstanzen() {
|
||||
for (const inst of store.listInstances()) {
|
||||
try {
|
||||
const r = await integritaet.pruefe(inst.id, { schnell: true });
|
||||
if (r.neuGesperrt) meldeGesperrt(inst.id);
|
||||
} catch (err) { console.warn('Integritätsprüfung', inst.id, err.message); }
|
||||
}
|
||||
}
|
||||
const icons = require('./icons');
|
||||
const bedrock = require('./bedrock');
|
||||
const backup = require('./backup');
|
||||
@@ -33,6 +113,8 @@ const mmcimport = require('./mmcimport');
|
||||
const kopieren = require('./kopieren');
|
||||
const moderkennung = require('./moderkennung');
|
||||
const garderobe = require('./garderobe');
|
||||
const spieldesign = require('./spieldesign');
|
||||
const modschalter = require('./modschalter');
|
||||
const erfolge = require('./erfolge');
|
||||
const cfimport = require('./cfimport');
|
||||
const mrimport = require('./mrimport');
|
||||
@@ -602,6 +684,35 @@ ipcMain.handle('recording:open', (_e, instanceName, dateiname) => {
|
||||
// aus keins mit, ohne das ginge z. B. "Einfügen" eines Teilen-Codes nur per Strg+V.
|
||||
// Gilt für Haupt- und alle Popup-Fenster.
|
||||
app.on('web-contents-created', (_e, contents) => {
|
||||
// Sicherheit: Kein Fenster darf je eine fremde Seite laden. Die Fenster haben
|
||||
// über preload.js Zugriff auf window.api – eine Webseite im selben Fenster
|
||||
// hätte damit die volle Kontrolle über den Launcher. Links nach außen gehen
|
||||
// deshalb immer in den Standardbrowser, alles andere wird verworfen.
|
||||
const eigeneSeite = (adresse) => {
|
||||
try {
|
||||
const u = new URL(adresse);
|
||||
if (u.protocol !== 'file:') return false;
|
||||
const datei = path.normalize(decodeURIComponent(u.pathname.replace(/^\/([a-z]:)/i, '$1')));
|
||||
return !path.relative(__dirname, datei).startsWith('..');
|
||||
} catch { return false; }
|
||||
};
|
||||
const nachAussen = (adresse) => {
|
||||
if (/^https?:\/\//i.test(String(adresse || ''))) shell.openExternal(adresse).catch(() => {});
|
||||
};
|
||||
contents.on('will-navigate', (ev, adresse) => {
|
||||
if (eigeneSeite(adresse)) return;
|
||||
ev.preventDefault();
|
||||
nachAussen(adresse);
|
||||
});
|
||||
contents.on('will-redirect', (ev, adresse) => {
|
||||
if (!eigeneSeite(adresse) && !String(adresse).startsWith('data:')) ev.preventDefault();
|
||||
});
|
||||
contents.setWindowOpenHandler(({ url: adresse }) => {
|
||||
nachAussen(adresse);
|
||||
return { action: 'deny' };
|
||||
});
|
||||
contents.on('will-attach-webview', (ev) => ev.preventDefault());
|
||||
|
||||
contents.on('context-menu', (_ev, params) => {
|
||||
const auswahl = String(params.selectionText || '').trim();
|
||||
if (!params.isEditable && !auswahl) return;
|
||||
@@ -956,11 +1067,16 @@ app.whenReady().then(() => {
|
||||
moderkennung.init(app.getPath('userData'));
|
||||
garderobe.init(app.getPath('userData'));
|
||||
erfolge.init(app.getPath('userData'));
|
||||
auth.init(app.getPath('userData'), {
|
||||
const tresor = {
|
||||
available: () => { try { return safeStorage.isEncryptionAvailable(); } catch { return false; } },
|
||||
encrypt: (s) => safeStorage.encryptString(s).toString('base64'),
|
||||
decrypt: (b) => safeStorage.decryptString(Buffer.from(b, 'base64')),
|
||||
});
|
||||
};
|
||||
auth.init(app.getPath('userData'), tresor);
|
||||
try { integritaet.init(app.getPath('userData'), tresor, store); } catch (err) { console.warn('Manipulationsschutz:', err.message); }
|
||||
setTimeout(() => { pruefeAlleInstanzen().catch(() => {}); }, 8000);
|
||||
// Proxy-Passwort und API-Schlüssel ebenso (siehe store.setzeTresor)
|
||||
store.setzeTresor(tresor);
|
||||
applyProxy(store.getSettings());
|
||||
discordpresence.configure(store.getSettings());
|
||||
const autostartAn = !!store.getSettings().autostart;
|
||||
@@ -1144,7 +1260,11 @@ ipcMain.handle('mods:search', async (_e, opts) => {
|
||||
// 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);
|
||||
let liste = store.listMods(id, art);
|
||||
if ((art || 'mod') === 'mod') {
|
||||
const aus = ausstehendeMods(id);
|
||||
liste = liste.map((m) => (m.filename in aus ? Object.assign({}, m, { enabled: !!aus[m.filename], ausstehend: true }) : m));
|
||||
}
|
||||
try { return await moderkennung.ergaenze(liste, store.inhaltsDir(id, art)); }
|
||||
catch { return liste; }
|
||||
}
|
||||
@@ -1806,6 +1926,12 @@ ipcMain.handle('crash:openFolder', (_e, id) => {
|
||||
|
||||
// Mod ab-/anschalten statt löschen (Datei wird nur umbenannt)
|
||||
ipcMain.handle('mods:setEnabled', (_e, id, filename, enabled, art) => {
|
||||
// Spiel läuft: Datei ist gesperrt -> vormerken, beim Beenden umsetzen
|
||||
if ((art || 'mod') === 'mod' && laufendeInstanzen.has(id)) {
|
||||
merkeModAenderung(id, filename, !!enabled);
|
||||
broadcast('mods:changed', { id });
|
||||
return { ok: true, enabled: !!enabled, ausstehend: true };
|
||||
}
|
||||
const res = store.setModEnabled(id, filename, !!enabled, art || 'mod');
|
||||
if (res.ok) return res;
|
||||
const ordner = services.inhaltsArt(art || 'mod').ordner;
|
||||
@@ -2227,15 +2353,22 @@ ipcMain.handle('cape:setOfficial', async (_e, capeId) => {
|
||||
// Rückgabe: "pfad=serveradresse" für -javaagent, oder '' wenn es nicht klappt.
|
||||
function umhangAgent() {
|
||||
try {
|
||||
// Die Kopie im Datenordner läuft mit vollen Rechten im Spiel mit. Deshalb vor
|
||||
// jedem Start Byte für Byte mit dem Original im Paket vergleichen – eine
|
||||
// ausgetauschte oder veränderte Datei wird überschrieben, nie geladen.
|
||||
const quelle = path.join(__dirname, 'assets', 'agent', 'aeromc-capes.jar');
|
||||
const groesse = fs.statSync(quelle).size;
|
||||
const inhalt = fs.readFileSync(quelle);
|
||||
const kennung = require('crypto').createHash('sha256').update(inhalt).digest('hex').slice(0, 16);
|
||||
const ordner = path.join(app.getPath('userData'), 'agent');
|
||||
const ziel = path.join(ordner, `aeromc-capes-${groesse}.jar`);
|
||||
if (!fs.existsSync(ziel) || fs.statSync(ziel).size !== groesse) {
|
||||
const ziel = path.join(ordner, `aeromc-capes-${kennung}.jar`);
|
||||
let vorhanden = null;
|
||||
try { vorhanden = fs.readFileSync(ziel); } catch { /* noch nicht da */ }
|
||||
if (!vorhanden || !vorhanden.equals(inhalt)) {
|
||||
fs.mkdirSync(ordner, { recursive: true });
|
||||
fs.writeFileSync(ziel, fs.readFileSync(quelle));
|
||||
fs.writeFileSync(ziel, inhalt);
|
||||
if (!fs.readFileSync(ziel).equals(inhalt)) throw new Error('Agent-Kopie weicht vom Original ab.');
|
||||
for (const alt of fs.readdirSync(ordner)) {
|
||||
if (alt !== path.basename(ziel) && /^aeromc-capes-\d+\.jar$/.test(alt)) { try { fs.unlinkSync(path.join(ordner, alt)); } catch { /* noch in Benutzung */ } }
|
||||
if (alt !== path.basename(ziel) && /^aeromc-capes-[0-9a-f]+\.jar$/.test(alt)) { try { fs.unlinkSync(path.join(ordner, alt)); } catch { /* noch in Benutzung */ } }
|
||||
}
|
||||
}
|
||||
return `${ziel}=${friends.BASIS_URL}`;
|
||||
@@ -2245,6 +2378,289 @@ function umhangAgent() {
|
||||
}
|
||||
}
|
||||
|
||||
// -javaagent-Argument: Umhänge und/oder AeroMC-Look im Spiel (siehe agent/Agent.java)
|
||||
function agentArgument(settings, version, modsAdresse) {
|
||||
const umhaenge = settings.aeromcCapes !== false;
|
||||
const design = settings.aeromcDesign !== false && spieldesign.unterstuetzt(version)
|
||||
? spieldesign.agentOrdner(app.getPath('userData')) : '';
|
||||
if (!umhaenge && !design && !modsAdresse) return '';
|
||||
const agent = umhangAgent();
|
||||
if (!agent) return '';
|
||||
return agent + (umhaenge ? '' : '|capes=0') + (design ? '|design=' + design : '') + (modsAdresse ? '|mods=' + modsAdresse : '');
|
||||
}
|
||||
|
||||
// ---------- Mod-Schalter im Spiel (siehe modschalter.js) ----------
|
||||
// Während Minecraft läuft, sind die Mod-Dateien gesperrt: Änderungen werden in
|
||||
// instance.json vorgemerkt und beim Beenden umgesetzt.
|
||||
const modFenster = new Map(); // instanceId -> BrowserWindow
|
||||
const neustartNachBeenden = new Set(); // instanceId -> nach dem Beenden direkt wieder starten
|
||||
const letzteVerbindungZeit = new Map(); // instanceId -> Zeitpunkt der letzten Server-Verbindung
|
||||
const sitzungsInfo = new Map(); // instanceId -> { start, kontoId } der laufenden Sitzung
|
||||
|
||||
// Wohin der Neustart führt: zuletzt verbundener Server oder zuletzt gespeicherte
|
||||
// Welt dieser Sitzung – je nachdem, was jünger ist – samt Konto der Sitzung
|
||||
function neustartZiel(id) {
|
||||
const info = sitzungsInfo.get(id) || {};
|
||||
const seit = info.start || 0;
|
||||
const ziel = { kontoId: info.kontoId || null };
|
||||
const welt = seit ? zuletztGespielteWelt(id, seit) : null;
|
||||
const serverZeit = letzteVerbindungZeit.get(id) || 0;
|
||||
const server = letzteQuickPlayServer.get(id);
|
||||
if (server && serverZeit >= seit && (!welt || serverZeit > welt.zeit)) ziel.quickPlayServer = server;
|
||||
else if (welt) ziel.quickPlayWelt = welt.name;
|
||||
return ziel;
|
||||
}
|
||||
|
||||
function ausstehendeMods(id) {
|
||||
const inst = store.getInstance(id);
|
||||
return (inst && inst.modsAusstehend) || {};
|
||||
}
|
||||
|
||||
function merkeModAenderung(id, filename, an) {
|
||||
const aus = Object.assign({}, ausstehendeMods(id));
|
||||
const mod = store.listMods(id, 'mod').find((m) => m.filename === filename);
|
||||
if (mod && mod.enabled === an) delete aus[filename]; // zurück auf den Stand der Datei
|
||||
else aus[filename] = an;
|
||||
store.updateInstance(id, { modsAusstehend: aus });
|
||||
return Object.keys(aus).length;
|
||||
}
|
||||
|
||||
function wendeAusstehendeModsAn(id) {
|
||||
const aus = ausstehendeMods(id);
|
||||
const namen = Object.keys(aus);
|
||||
if (!namen.length) return { umgesetzt: 0, offen: 0 };
|
||||
const rest = {};
|
||||
let umgesetzt = 0;
|
||||
for (const f of namen) {
|
||||
const r = store.setModEnabled(id, f, !!aus[f], 'mod');
|
||||
if (r.ok) umgesetzt++;
|
||||
else if (r.reason !== 'not-found' && r.reason !== 'both-exist' && r.reason !== 'bad-name') rest[f] = aus[f];
|
||||
}
|
||||
store.updateInstance(id, { modsAusstehend: rest });
|
||||
broadcast('mods:changed', { id });
|
||||
return { umgesetzt, offen: Object.keys(rest).length };
|
||||
}
|
||||
|
||||
// Welt, die während der Sitzung zuletzt gespeichert wurde (level.dat neuer als der Start)
|
||||
function zuletztGespielteWelt(id, seit) {
|
||||
let beste = null;
|
||||
try {
|
||||
const savesDir = path.join(store.gameDir(id), 'saves');
|
||||
for (const e of fs.readdirSync(savesDir, { withFileTypes: true })) {
|
||||
if (!e.isDirectory()) continue;
|
||||
try {
|
||||
const zeit = fs.statSync(path.join(savesDir, e.name, 'level.dat')).mtimeMs;
|
||||
if (zeit >= seit - 5000 && (!beste || zeit > beste.zeit)) beste = { name: e.name, zeit };
|
||||
} catch { /* keine Welt */ }
|
||||
}
|
||||
} catch { /* kein saves-Ordner */ }
|
||||
return beste;
|
||||
}
|
||||
|
||||
async function oeffneModFenster(sitzung) {
|
||||
const { instanceId: id, pid } = sitzung;
|
||||
const mc = await findeMinecraftFenster(pid);
|
||||
const b = mc ? mc.getBounds() : null;
|
||||
const breite = b ? Math.max(640, Math.min(980, Math.round(b.width * 0.9))) : 900;
|
||||
const hoehe = b ? Math.max(460, Math.min(680, Math.round(b.height * 0.9))) : 620;
|
||||
const lage = b ? { x: Math.round(b.x + (b.width - breite) / 2), y: Math.round(b.y + (b.height - hoehe) / 2) } : {};
|
||||
let win = modFenster.get(id);
|
||||
if (win && !win.isDestroyed()) {
|
||||
win.setBounds(Object.assign({ width: breite, height: hoehe }, lage));
|
||||
win.webContents.send('popout:reopen', { instanceId: id });
|
||||
win.show();
|
||||
win.focus();
|
||||
return;
|
||||
}
|
||||
win = new BrowserWindow(Object.assign({
|
||||
width: breite, height: hoehe, minWidth: 560, minHeight: 400,
|
||||
title: 'AeroMC – Mods', backgroundColor: fensterFarbe(), icon: windowIcon(),
|
||||
frame: false, show: false, skipTaskbar: true, alwaysOnTop: true,
|
||||
webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, sandbox: false },
|
||||
}, lage));
|
||||
win.setAlwaysOnTop(true, 'screen-saver');
|
||||
modFenster.set(id, win);
|
||||
win.loadFile(path.join(__dirname, 'index.html'), { query: { popout: 'modschalter', instanceId: id } });
|
||||
win.once('ready-to-show', () => { win.show(); win.focus(); });
|
||||
// nach dem Schließen gleich wieder ins Spiel
|
||||
const zurueck = async () => {
|
||||
const f = await findeMinecraftFenster(pid);
|
||||
if (f) { try { f.bringToTop(); } catch { /* egal */ } }
|
||||
};
|
||||
win.on('hide', zurueck);
|
||||
win.on('closed', () => { modFenster.delete(id); zurueck(); });
|
||||
}
|
||||
modschalter.beiKlick((sitzung) => { oeffneModFenster(sitzung).catch((err) => console.error('Mod-Fenster:', err)); });
|
||||
|
||||
ipcMain.handle('modschalter:daten', async (_e, id) => {
|
||||
const inst = store.getInstance(id);
|
||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
return {
|
||||
ok: true,
|
||||
name: inst.name,
|
||||
version: inst.minecraft.version,
|
||||
loader: inst.minecraft.loader,
|
||||
laeuft: laufendeInstanzen.has(id),
|
||||
ziel: laufendeInstanzen.has(id) ? neustartZiel(id) : null,
|
||||
quickPlayWelt: spieldesign.paketFormat(inst.minecraft.version) >= 15, // Welt direkt öffnen geht ab 1.20
|
||||
mods: await modsMitErkennung(id, 'mod'),
|
||||
};
|
||||
});
|
||||
// ---------- Neustart-Bildschirm (neustart.html) ----------
|
||||
// Liegt über dem Spielfenster, während Minecraft beendet, die Mods umgestellt
|
||||
// und das Spiel neu gestartet wird – bis man wieder auf dem Server bzw. in der
|
||||
// Welt ist (Minecraft hängt dann "Mehrspieler"/"Einzelspieler" an den Fenstertitel).
|
||||
const neustartBild = new Map(); // instanceId -> { win, ziel, folger, notfall, fensterSeit, verbundenSeit }
|
||||
const START_TEXTE = {
|
||||
version: 'Minecraft wird vorbereitet', client: 'Minecraft wird vorbereitet', libraries: 'Bibliotheken werden geprüft',
|
||||
assets: 'Spieldaten werden geprüft', loader: 'Mod-Loader wird vorbereitet', java: 'Java wird vorbereitet',
|
||||
};
|
||||
const START_ANTEILE = { version: [0, 6], client: [6, 14], libraries: [14, 45], assets: [45, 80], loader: [80, 90], java: [90, 96] };
|
||||
|
||||
// Fensterrechteck (physische Pixel) -> Electron-Koordinaten
|
||||
function alsDip(r) {
|
||||
try { return screen.screenToDipRect(null, r); } catch { return r; }
|
||||
}
|
||||
|
||||
async function zeigeNeustartBild(id, pid) {
|
||||
schliesseNeustartBild(id, true);
|
||||
const mc = await findeMinecraftFenster(pid);
|
||||
// Fensterzustand merken – Minecraft selbst weiß nach dem Neustart nicht mehr,
|
||||
// dass es maximiert war, und käme in der Standardgröße wieder
|
||||
let altesFenster = null;
|
||||
if (mc) {
|
||||
try {
|
||||
const roh = mc.getBounds();
|
||||
const dip = alsDip(roh);
|
||||
const wa = screen.getDisplayMatching(dip).workArea;
|
||||
const maximiert = dip.x <= wa.x && dip.y <= wa.y && dip.x + dip.width >= wa.x + wa.width && dip.y + dip.height >= wa.y + wa.height;
|
||||
altesFenster = { bounds: roh, maximiert };
|
||||
} catch { /* ohne Wiederherstellung */ }
|
||||
}
|
||||
let b = mc ? alsDip(mc.getBounds()) : null;
|
||||
if (!b || b.width < 200 || b.height < 150) {
|
||||
const a = screen.getPrimaryDisplay().workArea;
|
||||
b = { x: Math.round(a.x + (a.width - 960) / 2), y: Math.round(a.y + (a.height - 540) / 2), width: 960, height: 540 };
|
||||
}
|
||||
const win = new BrowserWindow({
|
||||
x: b.x, y: b.y, width: b.width, height: b.height,
|
||||
frame: false, resizable: false, skipTaskbar: true, alwaysOnTop: true, focusable: false, show: false,
|
||||
backgroundColor: '#0a0e1a', icon: windowIcon(),
|
||||
webPreferences: { contextIsolation: true, sandbox: true },
|
||||
});
|
||||
win.setAlwaysOnTop(true, 'screen-saver');
|
||||
win.loadFile(path.join(__dirname, 'neustart.html'));
|
||||
win.once('ready-to-show', () => { if (!win.isDestroyed()) win.showInactive(); });
|
||||
const eintrag = { win, ziel: null, folger: null, fensterSeit: 0, verbundenSeit: 0, altesFenster };
|
||||
// Sicherheitsnetz: nie länger als 3 Minuten stehen lassen
|
||||
eintrag.notfall = setTimeout(() => schliesseNeustartBild(id), 180000);
|
||||
neustartBild.set(id, eintrag);
|
||||
}
|
||||
|
||||
function neustartStatus(id, text, prozent) {
|
||||
const e = neustartBild.get(id);
|
||||
if (!e || e.win.isDestroyed()) return;
|
||||
const js = `typeof setze === 'function' && setze(${JSON.stringify({ text, prozent: typeof prozent === 'number' ? prozent : null })})`;
|
||||
if (e.win.webContents.isLoading()) e.win.webContents.once('did-finish-load', () => e.win.webContents.executeJavaScript(js).catch(() => {}));
|
||||
else e.win.webContents.executeJavaScript(js).catch(() => {});
|
||||
}
|
||||
|
||||
function schliesseNeustartBild(id, sofort) {
|
||||
const e = neustartBild.get(id);
|
||||
if (!e) return;
|
||||
neustartBild.delete(id);
|
||||
clearTimeout(e.notfall);
|
||||
clearInterval(e.folger);
|
||||
if (e.win.isDestroyed()) return;
|
||||
if (sofort) { e.win.destroy(); return; }
|
||||
e.win.webContents.executeJavaScript('typeof ausblenden === "function" && ausblenden()').catch(() => {});
|
||||
setTimeout(() => { if (!e.win.isDestroyed()) e.win.destroy(); }, 420);
|
||||
}
|
||||
|
||||
// Startfortschritt des Neustarts anzeigen (aus dem launch:progress der neuen Sitzung)
|
||||
function neustartFortschritt(id, evt) {
|
||||
if (!neustartBild.has(id)) return;
|
||||
if (START_TEXTE[evt.phase]) {
|
||||
const [von, bis] = START_ANTEILE[evt.phase];
|
||||
const anteil = evt.total ? Math.min(1, evt.current / evt.total) : 0;
|
||||
const prozent = Math.round(von + (bis - von) * anteil);
|
||||
neustartStatus(id, `${START_TEXTE[evt.phase]} … ${prozent} %`, prozent);
|
||||
} else if (evt.phase === 'launch') {
|
||||
neustartStatus(id, 'Minecraft startet …', null);
|
||||
}
|
||||
}
|
||||
|
||||
// dem neuen Spielfenster folgen und ausblenden, sobald man wieder drin ist
|
||||
function folgeNeuemSpiel(id, pid) {
|
||||
const e = neustartBild.get(id);
|
||||
if (!e) return;
|
||||
let windowManager = null;
|
||||
try { ({ windowManager } = require('node-window-manager')); } catch { /* ohne Fensterverfolgung */ }
|
||||
const ziel = e.ziel || {};
|
||||
e.folger = setInterval(() => {
|
||||
if (e.win.isDestroyed()) { clearInterval(e.folger); return; }
|
||||
let fenster = null;
|
||||
// nur das Spielfenster – Java hat daneben u. a. ein (verstecktes) Konsolenfenster
|
||||
try { fenster = windowManager && windowManager.getWindows().find((w) => w.processId === pid && /Minecraft/i.test(w.getTitle() || '') && w.isVisible()); } catch { /* weiter versuchen */ }
|
||||
if (!fenster) return;
|
||||
const jetzt = Date.now();
|
||||
if (!e.fensterSeit) {
|
||||
e.fensterSeit = jetzt;
|
||||
// Größe/Lage bzw. „maximiert“ wie vor dem Neustart – kurz darauf noch einmal,
|
||||
// falls Minecraft die Fenstergröße beim Hochfahren selbst noch setzt
|
||||
const herstellen = () => {
|
||||
const alt = e.altesFenster;
|
||||
if (!alt) return;
|
||||
try { if (alt.maximiert) fenster.maximize(); else fenster.setBounds(alt.bounds); } catch { /* egal */ }
|
||||
};
|
||||
herstellen();
|
||||
setTimeout(herstellen, 1500);
|
||||
}
|
||||
try {
|
||||
const r = alsDip(fenster.getBounds());
|
||||
if (r.width > 200 && r.height > 150) e.win.setBounds(r);
|
||||
} catch { /* Fenster gerade im Umbau */ }
|
||||
let titel = '';
|
||||
try { titel = fenster.getTitle() || ''; } catch { /* egal */ }
|
||||
const imSpiel = titel.replace(/^AeroMC - /, '').split(' - ').length >= 2;
|
||||
if (!ziel.quickPlayServer && !ziel.quickPlayWelt) {
|
||||
// kein Ziel: das Spiel zeigt ab jetzt seinen eigenen (AeroMC-)Ladebildschirm
|
||||
if (jetzt - e.fensterSeit > 1500) schliesseNeustartBild(id);
|
||||
return;
|
||||
}
|
||||
if (imSpiel) {
|
||||
neustartStatus(id, 'Willkommen zurück!', 100);
|
||||
setTimeout(() => schliesseNeustartBild(id), 500);
|
||||
clearInterval(e.folger);
|
||||
return;
|
||||
}
|
||||
if (!e.verbundenSeit) {
|
||||
neustartStatus(id, ziel.quickPlayServer ? `Verbinde mit ${ziel.quickPlayServer} …` : `Welt „${ziel.quickPlayWelt}“ wird geladen …`, null);
|
||||
}
|
||||
// ältere Versionen (1.14) schreiben nichts in den Titel -> nach dem Verbinden kurz warten
|
||||
if (e.verbundenSeit && jetzt - e.verbundenSeit > 9000) schliesseNeustartBild(id);
|
||||
if (jetzt - e.fensterSeit > 90000) schliesseNeustartBild(id);
|
||||
}, 400);
|
||||
}
|
||||
|
||||
ipcMain.handle('modschalter:neustart', async (_e, id) => {
|
||||
const pid = laufendePid.get(id);
|
||||
if (!pid) return { ok: false, message: 'Das Spiel läuft nicht.' };
|
||||
neustartNachBeenden.add(id);
|
||||
const win = modFenster.get(id);
|
||||
if (win && !win.isDestroyed()) win.destroy();
|
||||
await zeigeNeustartBild(id, pid).catch(() => {});
|
||||
neustartStatus(id, 'Minecraft wird beendet …', null);
|
||||
// ohne /F: Windows bittet das Fenster ums Schließen – Minecraft speichert und beendet sich sauber
|
||||
const { spawn } = require('child_process');
|
||||
spawn('taskkill', ['/PID', String(pid)], { windowsHide: true }).on('error', () => {});
|
||||
return { ok: true };
|
||||
});
|
||||
ipcMain.on('modschalter:schliessen', (e) => {
|
||||
const w = BrowserWindow.fromWebContents(e.sender);
|
||||
if (w && !w.isDestroyed()) w.destroy();
|
||||
});
|
||||
|
||||
// verständliche Meldung, wenn der Server das neue Plugin noch nicht hat
|
||||
function umhangFehler(res) {
|
||||
if (res && res.code === 'rest_no_route') return { ok: false, message: 'Der Server kennt noch keine Umhänge (Plugin „AeroMC Freunde“ 1.5.0 nötig).' };
|
||||
@@ -2376,7 +2792,9 @@ async function findeMinecraftFenster(pid) {
|
||||
for (let versuch = 1; versuch <= 4; versuch++) {
|
||||
let alleFenster;
|
||||
try { alleFenster = windowManager.getWindows(); } catch { alleFenster = []; }
|
||||
const treffer = alleFenster.find((w) => w.processId === pid);
|
||||
// bevorzugt das sichtbare Spielfenster (Java hat daneben u. a. ein verstecktes Konsolenfenster)
|
||||
const vomProzess = alleFenster.filter((w) => w.processId === pid);
|
||||
const treffer = vomProzess.find((w) => { try { return w.isVisible() && /Minecraft/i.test(w.getTitle() || ''); } catch { return false; } }) || vomProzess[0];
|
||||
if (treffer) return treffer;
|
||||
if (versuch < 4) await new Promise((r) => setTimeout(r, 300));
|
||||
}
|
||||
@@ -2695,6 +3113,7 @@ function teilBeschreibung(id) {
|
||||
}
|
||||
|
||||
ipcMain.handle('share:create', async (_e, id) => {
|
||||
if (integritaet.istGesperrt(id)) return { ok: false, message: GESPERRT_TEXT };
|
||||
const beschreibung = teilBeschreibung(id);
|
||||
if (!beschreibung) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
if (!beschreibung.minecraft.version) return { ok: false, message: 'Die Instanz hat noch keine Minecraft-Version.' };
|
||||
@@ -3034,9 +3453,9 @@ ipcMain.handle('auth:login', async () => {
|
||||
// IPC – Instanzen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
ipcMain.handle('instances:list', () => store.listInstances());
|
||||
ipcMain.handle('instances:list', () => store.listInstances().map(mitSperre));
|
||||
|
||||
ipcMain.handle('instances:get', (_e, id) => store.getInstance(id));
|
||||
ipcMain.handle('instances:get', (_e, id) => mitSperre(store.getInstance(id)));
|
||||
|
||||
ipcMain.handle('instances:create', (_e, data) => {
|
||||
const res = store.createInstance(data);
|
||||
@@ -3050,9 +3469,26 @@ ipcMain.handle('instances:update', (_e, id, patch) => {
|
||||
return res;
|
||||
});
|
||||
|
||||
ipcMain.handle('instances:delete', (_e, id) => store.deleteInstance(id));
|
||||
ipcMain.handle('instances:delete', (_e, id) => {
|
||||
const res = store.deleteInstance(id);
|
||||
integritaet.entferne(id);
|
||||
return res;
|
||||
});
|
||||
|
||||
// Gesperrte Instanz als neue Instanz aus der neuesten sauberen Sicherung holen
|
||||
ipcMain.handle('integritaet:wiederherstellen', async (_e, id) => {
|
||||
try {
|
||||
const sicherungen = backup.listGlobalBackups(app.getPath('documents')).map((b) => b.path);
|
||||
const res = await integritaet.stelleWiederHer(id, sicherungen);
|
||||
broadcast('instances:changed');
|
||||
return Object.assign({ ok: true }, res);
|
||||
} catch (err) {
|
||||
return { ok: false, message: err.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('instances:duplicate', async (_e, id) => {
|
||||
if (integritaet.istGesperrt(id)) throw new Error(GESPERRT_TEXT);
|
||||
const kopie = store.duplicateInstance(id, true);
|
||||
try { await kopieren.kopiere(store.gameDir(id), store.gameDir(kopie.id), null, null); }
|
||||
catch { /* Quelle evtl. leer */ }
|
||||
@@ -3145,6 +3581,7 @@ function runPwsh(command, env) {
|
||||
}
|
||||
|
||||
ipcMain.handle('instances:export', async (_e, id) => {
|
||||
if (integritaet.istGesperrt(id)) return { ok: false, message: GESPERRT_TEXT };
|
||||
const inst = store.getInstance(id);
|
||||
if (!inst) return { ok: false, message: 'Instanz nicht gefunden.' };
|
||||
const safe = (inst.name || 'instanz').replace(/[^\w.-]+/g, '_');
|
||||
@@ -3325,11 +3762,24 @@ async function spieleMrpackEin(buf, zip, index, gameDir, { schonePlayerDaten })
|
||||
const dest = path.join(gameDir, rel.replace(/\//g, path.sep));
|
||||
if (!inside(dest)) continue;
|
||||
broadcast('launch:progress', { phase: 'modpack', current: i, total: files.length, detail: 'Modpack-Dateien' });
|
||||
// Nur über HTTPS und nur mit passender Prüfsumme aus dem Paket: so landet
|
||||
// keine unterwegs ausgetauschte oder manipulierte Mod im mods-Ordner.
|
||||
const hashes = f.hashes || {};
|
||||
const algo = hashes.sha512 ? 'sha512' : (hashes.sha1 ? 'sha1' : null);
|
||||
if (!/^https:\/\//i.test(String(url)) || !algo) {
|
||||
console.warn(`[Modpack] ${rel} übersprungen (keine sichere Quelle/Prüfsumme)`);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(url, { headers: { 'User-Agent': 'AeroMC-Launcher' } });
|
||||
if (!res.ok) continue;
|
||||
const inhalt = Buffer.from(await res.arrayBuffer());
|
||||
if (require('crypto').createHash(algo).update(inhalt).digest('hex') !== String(hashes[algo]).toLowerCase()) {
|
||||
console.warn(`[Modpack] ${rel} verworfen: Prüfsumme stimmt nicht`);
|
||||
continue;
|
||||
}
|
||||
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||
fs.writeFileSync(dest, Buffer.from(await res.arrayBuffer()));
|
||||
fs.writeFileSync(dest, inhalt);
|
||||
dateien.push(rel);
|
||||
} catch { /* einzelne Datei überspringen */ }
|
||||
}
|
||||
@@ -3541,7 +3991,7 @@ async function importCurseForge(buf, zip, zugang) {
|
||||
try {
|
||||
// Das CurseForge-CDN verlangt seit Juli 2026 auch beim reinen Dateiabruf
|
||||
// einen gültigen Schlüssel. Bei der Weiterleitung setzt ihn der Server.
|
||||
await services.downloadFile(info.url, dest, info.kopfzeilen);
|
||||
await services.downloadFile(info.url, dest, info.kopfzeilen, info.hashes);
|
||||
geladen++;
|
||||
} catch {
|
||||
fehlend.push(f.fileID);
|
||||
@@ -3597,7 +4047,7 @@ ipcMain.handle('modpack:installFromModrinth', async (_e, project) => {
|
||||
};
|
||||
}
|
||||
broadcast('launch:progress', { phase: 'modpack', detail: 'Lade ' + (file.filename || file.name || 'Modpack') + ' …' });
|
||||
const buf = await services.downloadBuffer(file.url);
|
||||
const buf = await services.downloadBuffer(file.url, null, file.hashes);
|
||||
const zip = forge.readZipIndex(buf);
|
||||
if (!zip['modrinth.index.json']) {
|
||||
return { ok: false, message: 'Die geladene Datei ist kein gültiges Modrinth-Modpack (.mrpack).' };
|
||||
@@ -3648,7 +4098,7 @@ ipcMain.handle('modpack:installFromCurseForge', async (_e, project) => {
|
||||
}
|
||||
broadcast('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 buf = await services.downloadBuffer(dlUrl, zugang.downloadKopfzeilen, file.hashes);
|
||||
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.' };
|
||||
@@ -3827,15 +4277,25 @@ async function importInstanceZipFromPath(filePath) {
|
||||
notes: imported.notes || '',
|
||||
minecraft: imported.minecraft || {},
|
||||
});
|
||||
if (imported.java) store.updateInstance(created.id, { java: imported.java });
|
||||
// Aus fremden Archiven nur den Arbeitsspeicher übernehmen. Java-Pfad und
|
||||
// JVM-Argumente bleiben leer: ein präpariertes Archiv könnte sonst beim
|
||||
// nächsten Start ein beliebiges Programm ausführen lassen (eigene "java.exe",
|
||||
// -javaagent:…, -XX:OnOutOfMemoryError=…).
|
||||
if (imported.java && typeof imported.java === 'object') {
|
||||
const speicher = (v) => (Number.isFinite(Number(v)) && Number(v) >= 256 && Number(v) <= 262144 ? Math.round(Number(v)) : null);
|
||||
store.updateInstance(created.id, { java: { minMemMb: speicher(imported.java.minMemMb), maxMemMb: speicher(imported.java.maxMemMb) } });
|
||||
}
|
||||
|
||||
const mcSrc = path.join(base, 'minecraft');
|
||||
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))) {
|
||||
fs.copyFileSync(path.join(base, imported.customImage), path.join(store.instancePath(created.id), imported.customImage));
|
||||
store.updateInstance(created.id, { customImage: imported.customImage });
|
||||
// nur ein schlichter Bild-Dateiname – kein "../" o. Ä., das aus dem Ordner hinausführt
|
||||
const bildName = typeof imported.customImage === 'string' ? imported.customImage : '';
|
||||
if (/^[\w.\- ]+\.(png|jpe?g|gif|webp)$/i.test(bildName) && bildName === path.basename(bildName)
|
||||
&& fs.existsSync(path.join(base, bildName))) {
|
||||
fs.copyFileSync(path.join(base, bildName), path.join(store.instancePath(created.id), bildName));
|
||||
store.updateInstance(created.id, { customImage: bildName });
|
||||
}
|
||||
return { ok: true, instance: store.getInstance(created.id) };
|
||||
} catch (err) {
|
||||
@@ -4079,6 +4539,14 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => {
|
||||
if (!inst.minecraft.version) {
|
||||
return { ok: false, message: 'Bitte zuerst eine Minecraft-Version wählen (Bearbeiten).' };
|
||||
}
|
||||
// Manipulationsschutz: gründliche Prüfung (alle Mods neu gehasht) vor jedem Start
|
||||
{
|
||||
const r = await integritaet.pruefe(id);
|
||||
if (r.status === 'gesperrt') {
|
||||
if (r.neuGesperrt) meldeGesperrt(id);
|
||||
return { ok: false, gesperrt: true, message: GESPERRT_TEXT };
|
||||
}
|
||||
}
|
||||
const settings = store.getSettings();
|
||||
|
||||
// Speicherangaben prüfen, bevor Java mit einer unverständlichen Meldung
|
||||
@@ -4103,9 +4571,18 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => {
|
||||
// Auf Wunsch vor dem Start Mods aktualisieren (Fehler/Zeitüberschreitung -> einfach ohne Update starten)
|
||||
let modUpdates = null;
|
||||
if (settings.autoModUpdates) modUpdates = await modUpdatesVorDemStart(id).catch(() => null);
|
||||
if (modUpdates) await integritaet.versiegele(id).catch(() => {}); // eigene Änderung, geprüft wurde oben
|
||||
|
||||
// vorgemerkte Mod-Änderungen aus der letzten Sitzung (falls dort nicht mehr umgesetzt)
|
||||
if (!laufendeInstanzen.has(id)) wendeAusstehendeModsAn(id);
|
||||
// Mod-Schalter im Pausemenü (nur Instanzen mit Mod-Loader)
|
||||
const modSitzung = settings.modSchalter !== false && inst.minecraft.loader && inst.minecraft.loader !== 'vanilla'
|
||||
? await modschalter.neueSitzung(id).catch(() => null) : null;
|
||||
|
||||
const opts = {
|
||||
javaAgent: settings.aeromcCapes === false ? '' : umhangAgent(),
|
||||
javaAgent: agentArgument(settings, inst.minecraft.version, modSitzung && modSitzung.adresse),
|
||||
// Ressourcenpaket für Menü-Logo, Sprüche und Knöpfe anlegen bzw. entfernen
|
||||
vorStart: (info, protokoll) => spieldesign.vorbereiten(Object.assign({ an: settings.aeromcDesign !== false, mods: !!modSitzung, protokoll }, info)),
|
||||
versionId: inst.minecraft.version,
|
||||
loader: inst.minecraft.loader || 'vanilla',
|
||||
loaderVersion: inst.minecraft.loaderVersion || '',
|
||||
@@ -4126,6 +4603,7 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => {
|
||||
// fehlende Java-Fassung bei Bedarf selbst beschaffen (abschaltbar)
|
||||
ensureJava: settings.autoJava === false ? null : (major, onProgress) => javadl.ensureJava(major, onProgress),
|
||||
quickPlayServer: (launchOpts && launchOpts.quickPlayServer) ? String(launchOpts.quickPlayServer).trim() : '',
|
||||
quickPlayWelt: (launchOpts && launchOpts.quickPlayWelt) ? String(launchOpts.quickPlayWelt) : '',
|
||||
};
|
||||
|
||||
// eingeloggtes Microsoft-Konto verwenden (gewähltes oder aktives) -> echter Start
|
||||
@@ -4153,9 +4631,11 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => {
|
||||
try {
|
||||
let launchStart = 0;
|
||||
const res = await launcher.prepareAndLaunch(opts, (evt) => {
|
||||
if (neustartBild.has(id)) neustartFortschritt(id, evt);
|
||||
if (evt.phase === 'launch') {
|
||||
launchStart = Date.now();
|
||||
laufendeInstanzen.add(id);
|
||||
sitzungsInfo.set(id, { start: Date.now(), kontoId });
|
||||
meldeLaufende();
|
||||
if (!gezaehlt) { gezaehlt = true; kontoSpieltJetzt(kontoId); }
|
||||
discordpresence.setActivity({
|
||||
@@ -4194,6 +4674,34 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => {
|
||||
laufendePid.delete(id); // für die "Nur Minecraft"-Tonaufnahme: Prozess ist weg
|
||||
laufendeInstanzen.delete(id);
|
||||
meldeLaufende();
|
||||
if (modSitzung) modschalter.beende(modSitzung.token);
|
||||
const offenesModFenster = modFenster.get(id);
|
||||
if (offenesModFenster && !offenesModFenster.isDestroyed()) offenesModFenster.destroy();
|
||||
{
|
||||
const neuStarten = neustartNachBeenden.delete(id);
|
||||
// jetzt bestimmen, solange die Sitzungsdaten noch stimmen
|
||||
const zielBeimBeenden = neuStarten ? neustartZiel(id) : null;
|
||||
sitzungsInfo.delete(id);
|
||||
if (neuStarten) {
|
||||
const bild = neustartBild.get(id);
|
||||
if (bild) bild.ziel = zielBeimBeenden;
|
||||
neustartStatus(id, 'Mods werden übernommen …', null);
|
||||
} else if (neustartBild.has(id)) {
|
||||
schliesseNeustartBild(id); // neues Spiel nach einem Neustart wieder beendet (z. B. Absturz)
|
||||
}
|
||||
// kurz warten, bis Windows die Mod-Dateien freigegeben hat
|
||||
setTimeout(() => {
|
||||
let erg = wendeAusstehendeModsAn(id);
|
||||
if (erg.offen) { setTimeout(() => { erg = wendeAusstehendeModsAn(id); }, 2000); }
|
||||
if (erg.umgesetzt && !neuStarten) broadcast('ui:toast', { text: `${erg.umgesetzt} Mod-Änderung${erg.umgesetzt === 1 ? '' : 'en'} übernommen – gilt ab dem nächsten Start.` });
|
||||
if (neuStarten && mainWindow && !mainWindow.isDestroyed()) {
|
||||
neustartStatus(id, 'Minecraft startet neu …', null);
|
||||
mainWindow.webContents.send('modschalter:neustarten', { id, ziel: zielBeimBeenden });
|
||||
} else if (neuStarten) {
|
||||
schliesseNeustartBild(id);
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
// beim nächsten Start dieses Kontos sollen bereits online befindliche
|
||||
// Freunde wieder gemeldet werden
|
||||
if (kontoId) { kontoPid.delete(kontoId); freundeOnlineBaseline.delete(kontoId); }
|
||||
@@ -4218,6 +4726,9 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => {
|
||||
if (m) {
|
||||
const server = m[2] === '25565' ? m[1] : `${m[1]}:${m[2]}`;
|
||||
letzteQuickPlayServer.set(id, server);
|
||||
letzteVerbindungZeit.set(id, Date.now());
|
||||
const bild = neustartBild.get(id);
|
||||
if (bild && !bild.verbundenSeit) { bild.verbundenSeit = Date.now(); neustartStatus(id, `Verbinde mit ${server} …`, null); }
|
||||
// damit ein schon offenes Aufzeichnungs-Fenster den Servernamen im
|
||||
// Titel nachträgt, auch wenn es VOR dem Verbinden geöffnet wurde
|
||||
broadcast('recording:serverDetected', { id, server });
|
||||
@@ -4239,6 +4750,8 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => {
|
||||
}
|
||||
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 (modSitzung) { if (res.ok && res.pid) modschalter.setzePid(modSitzung.token, res.pid); else modschalter.beende(modSitzung.token); }
|
||||
if (neustartBild.has(id)) { if (res.ok && res.pid) folgeNeuemSpiel(id, res.pid); else schliesseNeustartBild(id); }
|
||||
if (res.ok && res.pid && kontoId) { kontoPid.set(kontoId, res.pid); kontoInstanz.set(kontoId, id); aktualisiereFreundeTastenkuerzel(); }
|
||||
return res;
|
||||
} catch (err) {
|
||||
@@ -4611,12 +5124,17 @@ ipcMain.handle('update:check', () => pruefeAufUpdates());
|
||||
ipcMain.handle('update:lastCheck', () => letzterUpdateCheck);
|
||||
|
||||
// Ein Klick: herunterladen, prüfen, Update anwenden und Launcher beenden
|
||||
ipcMain.handle('update:install', async (_e, asset) => {
|
||||
// Die Download-Adresse kommt bewusst nicht aus dem Fenster, sondern aus der
|
||||
// eigenen Update-Prüfung – sonst könnte jeder Aufruf von window.api eine
|
||||
// beliebige Datei als "Update" ausführen lassen.
|
||||
ipcMain.handle('update:install', async () => {
|
||||
try {
|
||||
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);
|
||||
});
|
||||
const res = updater.applyUpdate(file);
|
||||
const res = await updater.applyUpdate(file);
|
||||
// Portable: der Tausch klappt erst, wenn der Launcher die EXE freigibt
|
||||
setTimeout(() => app.quit(), 1200);
|
||||
return { ok: true, path: file.path, kind: file.kind, mode: res.started };
|
||||
|
||||
Reference in New Issue
Block a user