6 Commits
Author SHA1 Message Date
M_Viper 3b54217989 Upload via GUI (43 Dateien) 2026-08-15 14:20:19 +00:00
M_Viper 425b94fd93 Upload via GUI (44 Dateien) 2026-08-14 08:17:05 +00:00
M_Viper e0bdcb01fb Edit README.md via Git Manager GUI 2026-08-09 08:07:12 +00:00
M_Viper c840a49f75 Edit README.md via Git Manager GUI 2026-08-09 08:07:11 +00:00
M_Viper c535d8b830 Edit README.md via Git Manager GUI 2026-08-09 08:07:07 +00:00
Git Manager GUI 248a387f01 Upload via GUI (45 Dateien) 2026-08-09 10:06:39 +02:00
10 changed files with 1232 additions and 960 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
Ein Minecraft-Launcher für Windows mit getrennten Instanzen, Mod-Unterstützung und Microsoft-Anmeldung.
Aktueller Stand: Version 1.0.8
Aktueller Stand: Version 1.0.9
</div>
+141 -3
View File
@@ -1,14 +1,15 @@
{
"name": "aeromc",
"version": "1.0.7",
"version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "aeromc",
"version": "1.0.7",
"license": "MIT",
"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",
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "aeromc",
"productName": "AeroMC",
"version": "1.0.8",
"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",
+21 -9
View File
@@ -354,6 +354,10 @@ async function restoreGlobalBackup(options) {
const settingsFile = path.join(userDataDir, 'settings.json');
const rollbackSettings = path.join(rollbackDir, 'settings.json');
let settingsPreviouslyExisted = false;
// wird erst true, sobald der aktuelle Stand tatsächlich weggesichert wurde -
// vorher darf der Rollback nichts anfassen, sonst löscht ein früher Fehler
// (z. B. ungültiges Archiv) den unveränderten, intakten Originalbestand
let originalMovedAway = false;
try {
ensureDir(extractDir);
@@ -382,6 +386,7 @@ async function restoreGlobalBackup(options) {
endPercent: 40,
detailPrefix: 'Sichere aktuelle Instanzen',
});
originalMovedAway = true;
settingsPreviouslyExisted = fs.existsSync(settingsFile);
if (settingsPreviouslyExisted) fs.copyFileSync(settingsFile, rollbackSettings);
@@ -414,16 +419,23 @@ async function restoreGlobalBackup(options) {
restoredSettings,
};
} catch (err) {
try {
if (fs.existsSync(instancesDir)) fs.rmSync(instancesDir, { recursive: true, force: true });
if (fs.existsSync(rollbackInstances)) fs.cpSync(rollbackInstances, instancesDir, { recursive: true });
else ensureDir(instancesDir);
// Nur zurückrollen, wenn wir den ursprünglichen Bestand überhaupt schon
// angefasst haben (siehe originalMovedAway oben). Schlägt die
// Wiederherstellung vorher fehl (ungültiges/beschädigtes Archiv o. Ä.),
// ist instancesDir noch der unveränderte Originalbestand - den lassen wir
// dann bewusst in Ruhe, statt ihn zu löschen.
if (originalMovedAway) {
try {
if (fs.existsSync(instancesDir)) fs.rmSync(instancesDir, { recursive: true, force: true });
if (fs.existsSync(rollbackInstances)) fs.cpSync(rollbackInstances, instancesDir, { recursive: true });
else ensureDir(instancesDir);
if (includeSettings) {
if (fs.existsSync(rollbackSettings)) fs.copyFileSync(rollbackSettings, settingsFile);
else if (!settingsPreviouslyExisted && fs.existsSync(settingsFile)) fs.rmSync(settingsFile, { force: true });
}
} catch { /* ignore rollback errors */ }
if (includeSettings) {
if (fs.existsSync(rollbackSettings)) fs.copyFileSync(rollbackSettings, settingsFile);
else if (!settingsPreviouslyExisted && fs.existsSync(settingsFile)) fs.rmSync(settingsFile, { force: true });
}
} catch { /* ignore rollback errors */ }
}
throw err;
} finally {
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* ignore */ }
+102
View File
@@ -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 };
+22 -4
View File
@@ -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');
@@ -269,7 +270,16 @@ function destroyTray() {
// Windows-Autostart-Eintrag setzen/entfernen + Tray-Symbol passend ein-/ausblenden
function applyAutostart(enabled) {
try {
app.setLoginItemSettings({ openAtLogin: !!enabled, args: enabled ? ['--hidden'] : [] });
// Gleiches Problem wie bei den Desktop-Verknüpfungen (siehe verknuepfungsArgs
// weiter oben): im Entwicklungsmodus zeigt process.execPath nur auf die nackte
// electron.exe, die ohne Pfadangabe ihren eigenen leeren Startbildschirm zeigt
// statt AeroMC zu laden. Zusätzlich: bei der Portable-Fassung zeigt
// process.execPath auf den entpackten Temp-Ordner der wäre morgen tot,
// deshalb wie bei den Verknüpfungen über verknuepfungsZiel() auflösen.
const args = enabled
? (app.isPackaged ? ['--hidden'] : [path.join(__dirname, '..'), '--hidden'])
: [];
app.setLoginItemSettings({ openAtLogin: !!enabled, path: verknuepfungsZiel(), args });
} catch { /* z. B. in portabler Umgebung ohne Schreibrechte auf die Registry */ }
if (enabled) createTray();
else destroyTray();
@@ -288,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)
@@ -353,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)
@@ -381,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;
});
@@ -1926,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
@@ -1942,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 };
}
});
+1
View File
@@ -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)