From 3b54217989164c82424bff57b7f35703b6c7e16a Mon Sep 17 00:00:00 2001 From: M_Viper Date: Sat, 15 Aug 2026 14:20:19 +0000 Subject: [PATCH] Upload via GUI (43 Dateien) --- package-lock.json | 142 ++++++++++++++++++++++++++++++++++++++++- package.json | 3 +- src/discordpresence.js | 102 +++++++++++++++++++++++++++++ src/main.js | 15 ++++- src/store.js | 1 + 5 files changed, 257 insertions(+), 6 deletions(-) create mode 100644 src/discordpresence.js diff --git a/package-lock.json b/package-lock.json index 7d4199c..3d85385 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "aeromc", - "version": "1.0.9", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aeromc", - "version": "1.0.9", + "version": "1.1.0", "license": "SEE LICENSE IN LICENSE", "dependencies": { + "@xhayper/discord-rpc": "^1.3.4", "archiver": "^7.0.1", "skinview3d": "^3.4.2", "undici": "^6.28.0", @@ -37,6 +38,56 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.3.tgz", + "integrity": "sha512-wvOylxNYJkwKjctS/Mn5GP1w9r3/rzyH+ThD1JlAca6zEdlHs8QWBBUQJpU5Q+W6DoIj/Ljh1IPlZs7hTU+UAg==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.2.0", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.5", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.50", + "magic-bytes.js": "^1.13.0", + "tslib": "^2.6.3", + "undici": "^6.27.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", + "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, "node_modules/@electron/asar": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", @@ -494,6 +545,26 @@ "node": ">=14" } }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz", + "integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -662,6 +733,31 @@ "@types/node": "*" } }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@xhayper/discord-rpc": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@xhayper/discord-rpc/-/discord-rpc-1.3.4.tgz", + "integrity": "sha512-ff0uEXuibh9wi+l4vOj7xInLUjtlTaQBje/SCyQkeXZ0j2V0y+Zge5PQIQFRHH9TjjGaYJkTofEcQhncM2q7/w==", + "license": "ISC", + "dependencies": { + "@discordjs/rest": "^2.6.1", + "@vladfrangu/async_event_emitter": "^2.4.7", + "discord-api-types": "^0.38.47", + "ws": "^8.20.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.9.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", @@ -1799,6 +1895,15 @@ "node": "*" } }, + "node_modules/discord-api-types": { + "version": "0.38.53", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.53.tgz", + "integrity": "sha512-HL1zz/UuZ+bbJjA/X8Kbxx9gk8v9rJAbTeWRNYKmIdjwJ7EovjlHgoJTxcLpATfNJ+AonOtMdy3Y5MVIJAAd/A==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, "node_modules/dmg-builder": { "version": "24.13.3", "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-24.13.3.tgz", @@ -3363,6 +3468,12 @@ "node": ">=10" } }, + "node_modules/magic-bytes.js": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.1.tgz", + "integrity": "sha512-x5sn4UX2k5gCWlcfmoFwG4TPie8+dctESyqOBdhB5p6MsgWXdBKGmt9nXPObj/JI50TTL928lc5Yt1WntMn1bw==", + "license": "MIT" + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", @@ -4318,6 +4429,12 @@ "utf8-byte-length": "^1.0.1" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -4517,6 +4634,27 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/package.json b/package.json index 45422cf..d98a457 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "aeromc", "productName": "AeroMC", - "version": "1.0.9", + "version": "1.1.0", "description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)", "homepage": "https://m-viper.de", "launcherWebsite": "https://aeromc.viper.ipv64.net", @@ -40,6 +40,7 @@ "electron-builder": "^24.0.0" }, "dependencies": { + "@xhayper/discord-rpc": "^1.3.4", "archiver": "^7.0.1", "skinview3d": "^3.4.2", "undici": "^6.28.0", diff --git a/src/discordpresence.js b/src/discordpresence.js new file mode 100644 index 0000000..abaead2 --- /dev/null +++ b/src/discordpresence.js @@ -0,0 +1,102 @@ +'use strict'; + +/* + * discordpresence.js – Discord Rich Presence ("Spielt Minecraft über AeroMC") + * ----------------------------------------------------------------------------- + * Verbindet sich rein lokal über die IPC-Pipe des laufenden Discord-Clients. + * SET_ACTIVITY braucht dafür bewusst KEINE Authentifizierung/OAuth (anders als + * der Rest der RPC-API) – daher hier absichtlich client.request(...) direkt + * statt client.user.setActivity(...), das erst nach einem Login funktioniert. + * + * Immer aktiv, ohne eigene Einstellung – braucht also keine Zustimmung/kein + * Häkchen. Läuft trotzdem komplett unauffällig: ist Discord nicht installiert + * oder nicht gestartet, bleibt es einfach inaktiv, ohne den Launcher oder den + * Spielstart zu stören – jeder Fehler hier wird bewusst verschluckt. + */ + +const { Client } = require('@xhayper/discord-rpc'); + +// Von M_Viper unter discord.com/developers/applications angelegte AeroMC-App. +// Öffentliche Anwendungs-ID, kein Geheimnis (genau wie DEFAULT_CLIENT_ID bei +// der Microsoft-Anmeldung in auth.js) – gilt für alle AeroMC-Nutzer gleich. +const DEFAULT_CLIENT_ID = '1538173087730630686'; + +const LAUNCHER_WEBSITE = 'https://aeromc.viper.ipv64.net/'; + +let clientId = DEFAULT_CLIENT_ID; +let client = null; +let connecting = null; + +function resolveClientId() { + return (clientId && String(clientId).trim()) || DEFAULT_CLIENT_ID; +} + +async function connect() { + if (client && client.isConnected) return client; + if (connecting) return connecting; + + const id = resolveClientId(); + const c = new Client({ clientId: id }); + c.on('disconnected', () => { if (client === c) client = null; }); + connecting = c.connect() + .then(() => { client = c; connecting = null; return c; }) + .catch((err) => { connecting = null; throw err; }); + return connecting; +} + +/* + * activity: { details, state, startTimestamp } + * details/state -> die beiden Zeilen unter dem Profilbild in Discord + * startTimestamp -> Discord zeigt die Spielzeit dann selbst laufend an + */ +async function setActivity(activity) { + try { + const c = await connect(); + await c.request('SET_ACTIVITY', { + pid: process.pid, + activity: { + details: activity.details, + state: activity.state, + timestamps: activity.startTimestamp ? { start: activity.startTimestamp } : undefined, + assets: { large_image: 'aeromc_logo', large_text: 'AeroMC Launcher' }, + // Discord zeigt Buttons Betrachtern des Profils an (nicht sich selbst) - + // maximal 2 sind erlaubt, hier reicht einer. + buttons: [{ label: 'AeroMC herunterladen', url: LAUNCHER_WEBSITE }], + instance: false, + }, + }); + } catch { + // Discord nicht installiert/nicht gestartet o. Ä. - bewusst lautlos + } +} + +async function clearActivity() { + try { + if (!client || !client.isConnected) return; + await client.request('SET_ACTIVITY', { pid: process.pid }); + } catch { /* egal */ } +} + +async function shutdown() { + const c = client; + client = null; + connecting = null; + if (!c) return; + try { await c.destroy(); } catch { /* egal */ } +} + +/* + * Verbindet direkt beim Start. settings.discordClientId erlaubt weiterhin + * (rein optional, per Hand in settings.json) eine eigene App-ID zu setzen - + * ohne eigenes Einstellungsfeld, ohne dass es dafür einen Schalter braucht. + */ +function configure(settings) { + const nextId = (settings && settings.discordClientId) || DEFAULT_CLIENT_ID; + const idChanged = nextId !== clientId; + clientId = nextId; + + if (idChanged) { shutdown().then(() => connect().catch(() => {})); } + else if (!client) { connect().catch(() => {}); } +} + +module.exports = { configure, setActivity, clearActivity, shutdown, DEFAULT_CLIENT_ID }; diff --git a/src/main.js b/src/main.js index c873b5a..e19ac98 100644 --- a/src/main.js +++ b/src/main.js @@ -32,6 +32,7 @@ const crashreports = require('./crashreports'); const skins = require('./skins'); const servers = require('./servers'); const mcping = require('./mcping'); +const discordpresence = require('./discordpresence'); // undici als globales fetch verwenden -> ermöglicht Proxy via setGlobalDispatcher const undici = require('undici'); @@ -297,6 +298,7 @@ app.whenReady().then(() => { decrypt: (b) => safeStorage.decryptString(Buffer.from(b, 'base64')), }); applyProxy(store.getSettings()); + discordpresence.configure(store.getSettings()); const autostartAn = !!store.getSettings().autostart; // Autostart-Eintrag/Tray mit der Einstellung synchron halten (falls z. B. // von Hand an der Registry gedreht wurde oder nach einem Update) @@ -362,7 +364,7 @@ app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); -app.on('before-quit', () => { appIsQuitting = true; }); +app.on('before-quit', () => { appIsQuitting = true; discordpresence.shutdown(); }); // --------------------------------------------------------------------------- // Java-Erkennung (für Anzeige in den Einstellungen) @@ -390,6 +392,7 @@ ipcMain.handle('settings:save', (_e, patch) => { const s = store.saveSettings(patch); applyProxy(s); if (patch && Object.prototype.hasOwnProperty.call(patch, 'autostart')) applyAutostart(s.autostart); + if (patch && Object.prototype.hasOwnProperty.call(patch, 'discordClientId')) discordpresence.configure(s); return s; }); @@ -1935,9 +1938,15 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => { if (evt.phase === 'launch') { launchStart = Date.now(); if (!gezaehlt) { gezaehlt = true; kontoSpieltJetzt(kontoId); } + discordpresence.setActivity({ + details: 'Spielt Minecraft', + state: `${inst.name} · ${inst.minecraft.version}`, + startTimestamp: launchStart, + }); } if (evt.phase === 'exit') { if (gezaehlt) { gezaehlt = false; kontoSpieltNichtMehr(kontoId); } + discordpresence.clearActivity(); if (launchStart) { const secs = Math.max(0, Math.round((Date.now() - launchStart) / 1000)); launchStart = 0; // Spielzeit nur einmal gutschreiben @@ -1951,11 +1960,11 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => { }); // Start fehlgeschlagen, obwohl schon gezählt -> Zählung zurücknehmen, // sonst bliebe das Konto für immer als "spielt" stehen - if (!res.ok && gezaehlt) { gezaehlt = false; kontoSpieltNichtMehr(kontoId); } + if (!res.ok && gezaehlt) { gezaehlt = false; kontoSpieltNichtMehr(kontoId); discordpresence.clearActivity(); } if (res.ok) store.updateInstance(id, { lastPlayed: new Date().toISOString() }); return res; } catch (err) { - if (gezaehlt) { gezaehlt = false; kontoSpieltNichtMehr(kontoId); } + if (gezaehlt) { gezaehlt = false; kontoSpieltNichtMehr(kontoId); discordpresence.clearActivity(); } return { ok: false, message: 'Start fehlgeschlagen: ' + err.message }; } }); diff --git a/src/store.js b/src/store.js index 1ea26f8..e1fb3f1 100644 --- a/src/store.js +++ b/src/store.js @@ -77,6 +77,7 @@ const DEFAULT_SETTINGS = { bedrockEnabled: true, // Bedrock-Bereich anzeigen (nur wenn auch installiert) desktopNotifications: true, // Windows-Meldung bei fertigem Hintergrund-Download/-Backup autostart: false, // mit Windows starten, im Infobereich statt als Fenster + discordClientId: '', // optional, nur per Hand in settings.json; leer => eingebaute AeroMC-App-ID bgSkin: '', // '' = aus, 'active' = aktives Konto, sonst Konto-UUID bgSkinOpacity: 55, // Deckkraft des Hintergrund-Skins in % bgSkinPose: 'walking', // Pose des 3D-Renders (falls der Pose-Dienst erreichbar ist)