Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee0e6dec32 | ||
|
|
3b54217989 | ||
|
|
425b94fd93 | ||
|
|
e0bdcb01fb | ||
|
|
c840a49f75 | ||
|
|
c535d8b830 | ||
|
|
248a387f01 | ||
|
|
87fded4cb6 | ||
|
|
33e533009e | ||
|
|
17f8726075 | ||
|
|
deec93e5fa | ||
|
|
fc57dfa186 | ||
|
|
bafad25059 |
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
# beim Bauen erzeugt, enthält Adresse und Token der CurseForge-Weiterleitung
|
||||
src/build-config.json
|
||||
|
||||
# Zugangsdaten des Weiterleitungs-Servers
|
||||
server/curseforge-proxy/.env
|
||||
@@ -0,0 +1,38 @@
|
||||
AeroMC – Lizenz- und Nutzungsbedingungen
|
||||
|
||||
Copyright (c) M_Viper. Alle Rechte vorbehalten.
|
||||
|
||||
Diese Software („AeroMC") sowie sämtlicher zugehöriger Quellcode, Ressourcen und
|
||||
Dokumentation sind urheberrechtlich geschützt. Mit der Nutzung dieser Software
|
||||
erkennst du die folgenden Bedingungen an.
|
||||
|
||||
1. Erlaubte Nutzung
|
||||
Die Nutzung von AeroMC ist ausschließlich für den privaten, nicht-kommerziellen
|
||||
Gebrauch gestattet.
|
||||
|
||||
2. Untersagt ist insbesondere
|
||||
- Die Weitergabe dieses Launchers oder von Teilen davon an Dritte
|
||||
- Das Verändern, Dekompilieren, Zurückentwickeln (Reverse Engineering) oder
|
||||
Bearbeiten des Codes
|
||||
- Die Veröffentlichung – auch in veränderter oder abgeleiteter Form –
|
||||
an anderer Stelle als den offiziellen Kanälen des Autors
|
||||
- Der Verkauf sowie jede sonstige kommerzielle Nutzung
|
||||
|
||||
3. Keine Gewährleistung
|
||||
Die Software wird ohne jegliche Gewährleistung bereitgestellt, weder
|
||||
ausdrücklich noch stillschweigend, einschließlich, aber nicht beschränkt auf
|
||||
die Gewährleistung der Marktgängigkeit, der Eignung für einen bestimmten
|
||||
Zweck und der Nichtverletzung von Rechten Dritter. Der Autor haftet nicht für
|
||||
Schäden, die aus der Nutzung oder Unmöglichkeit der Nutzung der Software
|
||||
entstehen.
|
||||
|
||||
4. Rechtsfolgen bei Verstoß
|
||||
Wer gegen diese Bedingungen verstößt, macht sich strafbar und kann rechtlich
|
||||
belangt werden. Alle Rechte, die hier nicht ausdrücklich eingeräumt werden,
|
||||
bleiben beim Autor vorbehalten.
|
||||
|
||||
5. Markenhinweis
|
||||
AeroMC ist kein offizielles Produkt von Mojang oder Microsoft. Minecraft ist
|
||||
eine Marke von Mojang AB.
|
||||
|
||||
Kontakt: https://m-viper.de
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
Ein Minecraft-Launcher für Windows mit getrennten Instanzen, Mod-Unterstützung und Microsoft-Anmeldung.
|
||||
|
||||
Aktueller Stand: Version 1.0.6
|
||||
Aktueller Stand: Version 1.0.9
|
||||
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen
|
||||
- Warnt beim Entfernen eines Mods, falls ein anderer installierter Mod ihn noch als Pflicht-Abhängigkeit braucht
|
||||
- Globales Backup inkl. automatischer Zeitplan-Sicherungen, dazu Schnellsicherung einzelner Welten
|
||||
- Welten, Ressourcenpakete, Datenpakete, Screenshots, Serverliste und Logdateien verwalten
|
||||
- Serverliste mit Live-Status (Spieleranzahl, MOTD, Version) und Übernahme einzelner Einträge in andere Instanzen
|
||||
- Serverliste mit Live-Status (Spieleranzahl, MOTD, Version), Übernahme einzelner Einträge in andere Instanzen sowie Desktop-Verknüpfung pro Server (startet die Instanz und verbindet direkt, ab MC 1.20)
|
||||
- Online- und Offline-Start
|
||||
- Eingebaute Konsole/Log-Ansicht inkl. Absturzberichte pro Instanz
|
||||
- Windows-Benachrichtigung, wenn ein großer Download oder ein Backup im Hintergrund fertig wird
|
||||
@@ -45,6 +45,7 @@ Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen
|
||||
- Optionaler Wrapper-Befehl und HTTP-Proxy für Downloads
|
||||
- Bedrock-Bereich für vorhandene Bedrock-Installation unter Windows, inkl. Installation zusätzlicher Add-ons
|
||||
- Update-Suche läuft automatisch beim Start, dazu manuelle Prüfung in den Einstellungen
|
||||
- Optionaler Autostart mit Windows, dann im Infobereich statt als Fenster
|
||||
- Design Hell/Dunkel und Sortierung der Instanzen
|
||||
|
||||
## Installation
|
||||
@@ -88,7 +89,7 @@ Jede Instanz hat:
|
||||
- Instanz und Notizen bearbeiten
|
||||
- Mods, Ressourcenpakete und Shader
|
||||
- Welten (inkl. Schnellsicherung einzelner Welten), Datenpakete und Bildschirmfotos
|
||||
- Server verwalten (Live-Status, Übernahme eines Eintrags in andere Instanzen)
|
||||
- Server verwalten (Live-Status, Übernahme eines Eintrags in andere Instanzen, Desktop-Verknüpfung pro Server)
|
||||
- Logdateien und Absturzberichte ansehen
|
||||
- Minecraft-, Konfigurations- und Instanz-Ordner öffnen
|
||||
- Desktop-Verknüpfung erstellen
|
||||
@@ -174,6 +175,7 @@ Speicherort: `Dokumente\AeroMC Launcher\Backups`.
|
||||
- Instanz-Verzeichnis
|
||||
- Bedrock-Bereich anzeigen
|
||||
- Windows-Benachrichtigung bei fertigem Hintergrund-Download/-Backup (nur wenn das Fenster nicht im Vordergrund ist)
|
||||
- Autostart: mit Windows starten, im Infobereich statt als Fenster – solange aktiv, versteckt „Schließen" (X) den Launcher nur, statt ihn zu beenden (über das Symbol im Infobereich wieder öffnen oder wirklich beenden)
|
||||
|
||||
### Darstellung
|
||||
- Block-Symbole räumlich (3D)
|
||||
@@ -222,7 +224,7 @@ Nutzerdaten liegen lokal unter `%APPDATA%\aeromc`.
|
||||
## Links
|
||||
|
||||
- Webseite: https://m-viper.de
|
||||
- Gitseite: https://git.viper.ipv64.net
|
||||
- Launcher-Webseite: https://aeromc.viper.ipv64.net
|
||||
- Autor: M_Viper
|
||||
|
||||
## Rechtliches
|
||||
|
||||
Generated
+222
-3
@@ -1,15 +1,18 @@
|
||||
{
|
||||
"name": "aeromc",
|
||||
"version": "1.0.5",
|
||||
"version": "1.1.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "aeromc",
|
||||
"version": "1.0.5",
|
||||
"license": "MIT",
|
||||
"version": "1.1.2",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@xhayper/discord-rpc": "^1.3.4",
|
||||
"archiver": "^7.0.1",
|
||||
"loopback-capture": "^3.0.2",
|
||||
"node-window-manager": "^2.2.4",
|
||||
"skinview3d": "^3.4.2",
|
||||
"undici": "^6.28.0",
|
||||
"unzipper": "^0.12.5"
|
||||
@@ -37,6 +40,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 +547,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 +735,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",
|
||||
@@ -1128,6 +1226,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
@@ -1799,6 +1906,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",
|
||||
@@ -2493,6 +2609,22 @@
|
||||
"bare-events": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-file-icon": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/extract-file-icon/-/extract-file-icon-0.3.2.tgz",
|
||||
"integrity": "sha512-We5/HaMDlioOx3c9zDjit8RtKWEvT94sJF8jTujaKVOyWL8Kkqu4QGnwDr5fID6XsbKvWT0JJNuWuV2Tmr7yiw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "1.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/extract-file-icon/node_modules/node-addon-api": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.1.tgz",
|
||||
"integrity": "sha512-2+DuKodWvwRTrCfKOeR24KIc5unKjOh8mz17NCzVnHWfjAdDqbfbjqh7gUT+BkXBRQM52+xCHciKWonJ3CbJMQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extract-zip": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
|
||||
@@ -2561,6 +2693,12 @@
|
||||
"integrity": "sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/filelist": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz",
|
||||
@@ -3340,6 +3478,25 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/loopback-capture": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/loopback-capture/-/loopback-capture-3.0.2.tgz",
|
||||
"integrity": "sha512-65qyF9L7e+InnbEZP3Ic4C4J+SszoxEq84XpIZWJYH3/IgUpTKqBzJ4wCF+ShL3bvqV7I2wOEgQv9VfpaO2AMw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"os": [
|
||||
"linux",
|
||||
"win32"
|
||||
],
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||
@@ -3363,6 +3520,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",
|
||||
@@ -3525,12 +3688,41 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||
"integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-window-manager": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/node-window-manager/-/node-window-manager-2.2.4.tgz",
|
||||
"integrity": "sha512-Dgq9a7qx6+Gi+BifWM79leteNz76QUtwSsdiqEe7SjvoJsc8TYQsRl1IA5uxqz5iSLeHS1iA/8YI/snoq0DblA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"extract-file-icon": "^0.3.2",
|
||||
"node-addon-api": "2.0.0",
|
||||
"node-gyp-build": "^4.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-window-manager/node_modules/node-addon-api": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.0.tgz",
|
||||
"integrity": "sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
@@ -4318,6 +4510,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 +4715,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",
|
||||
|
||||
+13
-7
@@ -1,16 +1,14 @@
|
||||
{
|
||||
"name": "aeromc",
|
||||
"productName": "AeroMC",
|
||||
"version": "1.0.5",
|
||||
"version": "1.1.2",
|
||||
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
|
||||
"homepage": "https://m-viper.de",
|
||||
"launcherWebsite": "https://aeromc.viper.ipv64.net",
|
||||
"main": "src/main.js",
|
||||
"author": "M_Viper",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.viper.ipv64.net"
|
||||
},
|
||||
"license": "MIT",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"prebuild": "node scripts/write-build-config.js",
|
||||
@@ -25,7 +23,12 @@
|
||||
"compression": "store",
|
||||
"files": [
|
||||
"src/**/*",
|
||||
"package.json"
|
||||
"package.json",
|
||||
"LICENSE"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"node_modules/loopback-capture/**/*",
|
||||
"node_modules/node-window-manager/**/*"
|
||||
],
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
@@ -41,7 +44,10 @@
|
||||
"electron-builder": "^24.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xhayper/discord-rpc": "^1.3.4",
|
||||
"archiver": "^7.0.1",
|
||||
"loopback-capture": "^3.0.2",
|
||||
"node-window-manager": "^2.2.4",
|
||||
"skinview3d": "^3.4.2",
|
||||
"undici": "^6.28.0",
|
||||
"unzipper": "^0.12.5"
|
||||
|
||||
+21
-9
@@ -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 */ }
|
||||
|
||||
+73
-11
@@ -14,7 +14,12 @@ const path = require('path');
|
||||
const { execFile, spawn } = require('child_process');
|
||||
|
||||
const PKG_FAMILY = 'Microsoft.MinecraftUWP_8wekyb3d8bbwe';
|
||||
const APP_ID = PKG_FAMILY + '!App';
|
||||
// Der Teil nach "!" (Application Id) ist NICHT stabil – Microsoft hat ihn
|
||||
// schon mindestens einmal geändert (früher "App", inzwischen "Game").
|
||||
// Deshalb zur Laufzeit über Get-StartApps auflösen statt fest zu verdrahten;
|
||||
// dieser Wert dient nur als letzter Rückfallwert, falls die Auflösung
|
||||
// fehlschlägt (z. B. PowerShell blockiert).
|
||||
const FALLBACK_APP_ID = PKG_FAMILY + '!Game';
|
||||
|
||||
function comMojangDir() {
|
||||
return path.join(process.env.LOCALAPPDATA || '', 'Packages', PKG_FAMILY,
|
||||
@@ -49,30 +54,87 @@ async function detect() {
|
||||
// Start
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let cachedAppId = null;
|
||||
|
||||
/*
|
||||
* Ermittelt die aktuell gültige AUMID über Get-StartApps – das ist derselbe
|
||||
* Weg, den auch das Startmenü nutzt, und bildet daher immer die Application
|
||||
* Id ab, die Windows gerade tatsächlich zum Starten akzeptiert.
|
||||
*/
|
||||
async function resolveAppId() {
|
||||
if (cachedAppId) return cachedAppId;
|
||||
const out = await runPwsh(
|
||||
`(Get-StartApps | Where-Object { $_.AppID -like '${PKG_FAMILY}*' } `
|
||||
+ `| Select-Object -First 1 -ExpandProperty AppID)`);
|
||||
cachedAppId = out || FALLBACK_APP_ID;
|
||||
return cachedAppId;
|
||||
}
|
||||
|
||||
/*
|
||||
* Startet Bedrock direkt.
|
||||
* Wichtig: Der AppsFolder-Pfad darf NICHT in Anführungszeichen stehen – sonst
|
||||
* öffnet der Explorer nur ein Ordnerfenster. Daher windowsVerbatimArguments.
|
||||
*/
|
||||
function launch() {
|
||||
const target = 'shell:appsFolder\\' + APP_ID;
|
||||
async function launch() {
|
||||
const appId = await resolveAppId();
|
||||
const target = 'shell:appsFolder\\' + appId;
|
||||
// Wichtig: Es darf pro Klick nur EINE Aktivierung tatsächlich lostreten.
|
||||
// Die App-Aktivierung lässt sich nicht abbrechen – wenn wir bei einer
|
||||
// Verzögerung (Xbox-Dienste, Kaltstart) automatisch einen zweiten Weg
|
||||
// probieren, laufen im Zweifel beide an und Bedrock öffnet sich doppelt.
|
||||
// Die anderen Wege dienen daher nur als Rückfall, falls das Spawnen
|
||||
// selbst scheitert (Programm nicht gefunden o. Ä.), nicht als
|
||||
// Reaktion auf einen bloß langsamen Start.
|
||||
const versuche = [
|
||||
// 1) genau der Aufruf, der sich unter Windows als zuverlässig erwiesen hat
|
||||
// 1) explorer direkt, ohne zusätzliche Anführungszeichen – entspricht
|
||||
// exakt dem manuellen "explorer.exe shell:appsFolder\...", der
|
||||
// nachweislich zuverlässig funktioniert
|
||||
['explorer.exe', [target], { windowsVerbatimArguments: true }],
|
||||
// 2) über die Eingabeaufforderung
|
||||
['cmd', ['/c', 'start', '', target], { windowsVerbatimArguments: true }],
|
||||
// 3) Rückfallweg über eine versteckte PowerShell – aus einem
|
||||
// windowless/detached Kindprozess heraus offenbar nicht überall
|
||||
// zuverlässig, daher nur letzter Versuch
|
||||
['powershell', ['-NoProfile', '-WindowStyle', 'Hidden', '-Command',
|
||||
`Start-Process explorer.exe -ArgumentList '${target}'`], {}],
|
||||
// 2) explorer direkt, ohne zusätzliche Anführungszeichen
|
||||
['explorer.exe', [target], { windowsVerbatimArguments: true }],
|
||||
// 3) über die Eingabeaufforderung
|
||||
['cmd', ['/c', 'start', '', target], { windowsVerbatimArguments: true }],
|
||||
];
|
||||
let methode = null;
|
||||
for (const [cmd, args, extra] of versuche) {
|
||||
try {
|
||||
const p = spawn(cmd, args, Object.assign({ detached: true, stdio: 'ignore', windowsHide: true }, extra));
|
||||
p.unref();
|
||||
return { ok: true, method: cmd };
|
||||
} catch { /* nächsten Weg versuchen */ }
|
||||
methode = cmd;
|
||||
break; // nur diesen einen Weg tatsächlich auslösen
|
||||
} catch { /* Spawnen selbst fehlgeschlagen – nächsten Weg versuchen */ }
|
||||
}
|
||||
if (!methode) return { ok: false, message: 'Bedrock konnte nicht gestartet werden.' };
|
||||
// Aktivierung kann dauern (v. a. beim ersten Start nach längerer Pause) –
|
||||
// hier nur zur ehrlichen Rückmeldung abwarten, OHNE bei Zeitüberschreitung
|
||||
// einen weiteren Versuch zu starten (siehe Hinweis oben).
|
||||
const bestaetigt = await waitForBedrockProcess(15000);
|
||||
return { ok: true, method: methode, confirmed: bestaetigt };
|
||||
}
|
||||
|
||||
function isBedrockRunning() {
|
||||
return new Promise((resolve) => {
|
||||
execFile('tasklist', ['/FI', 'IMAGENAME eq Minecraft.Windows.exe', '/FO', 'CSV', '/NH'],
|
||||
{ windowsHide: true, timeout: 5000 },
|
||||
(err, stdout) => resolve(!err && /Minecraft\.Windows\.exe/i.test(String(stdout || ''))));
|
||||
});
|
||||
}
|
||||
|
||||
function warten(ms) {
|
||||
return new Promise((resolve) => { setTimeout(resolve, ms); });
|
||||
}
|
||||
|
||||
async function waitForBedrockProcess(timeoutMs) {
|
||||
const start = Date.now();
|
||||
// sofort prüfen (falls schon ein Fenster lief) und danach im Kurztakt
|
||||
for (;;) {
|
||||
if (await isBedrockRunning()) return true;
|
||||
if (Date.now() - start >= timeoutMs) return false;
|
||||
await warten(500);
|
||||
}
|
||||
return { ok: false, message: 'Bedrock konnte nicht gestartet werden.' };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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 };
|
||||
+247
-10
@@ -10,7 +10,7 @@
|
||||
<body>
|
||||
<!-- Obere Werkzeugleiste (MultiMC-Stil: Icon + Text) -->
|
||||
<header class="toolbar">
|
||||
<button id="btn-new" class="tbtn" data-ic="plus">Instanz hinzufügen</button>
|
||||
<button id="btn-new" class="tbtn" data-ic="plus" title="Neue Instanz (Strg+N)">Instanz hinzufügen</button>
|
||||
<button id="btn-import" class="tbtn" data-ic="import" title="Instanzen importieren">Importieren</button>
|
||||
<button id="btn-open-dir" class="tbtn" data-ic="folder" title="Ordner öffnen">Ordner</button>
|
||||
<span class="tbsep"></span>
|
||||
@@ -19,7 +19,7 @@
|
||||
<button id="btn-skin" class="tbtn" data-ic="user" title="Skin-Hintergrund ein/aus">Skin</button>
|
||||
<span class="tbgrow"></span>
|
||||
<div class="search-wrap">
|
||||
<input id="search" type="text" placeholder="Suchen…" autocomplete="off" />
|
||||
<input id="search" type="text" placeholder="Suchen… (Strg+F)" autocomplete="off" title="Strg+F fokussiert die Suche" />
|
||||
</div>
|
||||
<button id="btn-account" class="tbtn acct-btn" title="Microsoft-Konto"><span id="account-av" class="acct-av"></span><span id="account-name">Konten</span></button>
|
||||
<button id="btn-settings" class="tbtn" data-ic="settings" title="Einstellungen">Optionen</button>
|
||||
@@ -51,6 +51,7 @@
|
||||
<button id="p-mods" class="sbtn">Mods anzeigen</button>
|
||||
<button id="p-worlds" class="sbtn">Welten verwalten</button>
|
||||
<button id="p-screens" class="sbtn">Bildschirmfotos verwalten</button>
|
||||
<button id="p-record" class="sbtn">Spiel aufzeichnen</button>
|
||||
<button id="p-modpack" class="sbtn hidden">Modpack aktualisieren</button>
|
||||
<button id="p-logs" class="sbtn">Logdateien ansehen</button>
|
||||
<button id="p-servers" class="sbtn">Server verwalten</button>
|
||||
@@ -61,7 +62,7 @@
|
||||
<div class="hr"></div>
|
||||
<button id="p-shortcut" class="sbtn">Verknüpfung erstellen</button>
|
||||
<button id="p-export" class="sbtn">Instanz exportieren</button>
|
||||
<button id="p-delete" class="sbtn del">Löschen</button>
|
||||
<button id="p-delete" class="sbtn del" title="Entf">Löschen</button>
|
||||
<button id="p-copy" class="sbtn">Instanz kopieren</button>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -130,6 +131,7 @@
|
||||
<option value="forge">Forge</option>
|
||||
<option value="neoforge">NeoForge</option>
|
||||
<option value="quilt">Quilt</option>
|
||||
<option value="optifine">OptiFine</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
@@ -139,6 +141,23 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="f-optifine-tools" class="field-row hidden" style="align-items:flex-start">
|
||||
<div class="field" style="flex:1">
|
||||
<span>OptiFine einrichten</span>
|
||||
<p class="hint" style="margin:2px 0 8px">
|
||||
OptiFine bietet kein automatisches Download-API (Downloads laufen bei OptiFine bewusst über eine Werbeseite).
|
||||
Lade den Installer zuerst selbst von <b>optifine.net</b> herunter, wähle ihn dann hier aus – AeroMC bereitet
|
||||
die passende Minecraft-Version vor und startet den echten OptiFine-Installer. Wähle darin als Zielordner den
|
||||
automatisch in die Zwischenablage kopierten Pfad, klicke „Install" und bestätige danach hier.
|
||||
</p>
|
||||
<div class="path-row">
|
||||
<button type="button" id="f-optifine-install" class="btn">Installer wählen und starten…</button>
|
||||
<button type="button" id="f-optifine-confirm" class="btn btn-primary hidden">Fertig – Version übernehmen</button>
|
||||
</div>
|
||||
<small id="f-optifine-status" class="hint"></small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="field">
|
||||
<span>Notizen <small class="opt">(optional)</small></span>
|
||||
<textarea id="f-notes" rows="2" placeholder="Modliste, Server-IP, To-dos …"></textarea>
|
||||
@@ -171,7 +190,11 @@
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Zusätzliche JVM-Argumente</span>
|
||||
<input id="f-extra-args" type="text" placeholder="z. B. -XX:+UseG1GC" />
|
||||
<div class="path-row">
|
||||
<input id="f-extra-args" type="text" placeholder="z. B. -XX:+UseG1GC" />
|
||||
<button id="f-extra-args-aikar" class="btn btn-mini" type="button"
|
||||
title="Fügt eine bewährte Sammlung von Garbage-Collector-Flags ein (bekannt als „Aikar's Flags"), die bei größeren Modpacks Ruckler durch Garbage Collection reduzieren kann.">GC-Flags einfügen</button>
|
||||
</div>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Pre-Launch-Befehl <small class="opt">(läuft vor dem Start)</small></span>
|
||||
@@ -253,6 +276,22 @@
|
||||
</label>
|
||||
<small id="s-bedrock-hint" class="hint">Wird automatisch ausgeblendet, wenn Bedrock nicht installiert ist.</small>
|
||||
</section>
|
||||
|
||||
<section class="settings-card">
|
||||
<h5>Benachrichtigungen</h5>
|
||||
<label class="checkbox-field">
|
||||
<input id="s-notify" type="checkbox" /> <span>Windows-Benachrichtigung, wenn ein großer Download oder ein Backup im Hintergrund fertig wird</span>
|
||||
</label>
|
||||
<small class="hint">Erscheint nur, während das Launcher-Fenster nicht im Vordergrund ist.</small>
|
||||
</section>
|
||||
|
||||
<section class="settings-card">
|
||||
<h5>Autostart</h5>
|
||||
<label class="checkbox-field">
|
||||
<input id="s-autostart" type="checkbox" /> <span>Mit Windows starten (im Infobereich)</span>
|
||||
</label>
|
||||
<small class="hint">Startet AeroMC beim Anmelden automatisch im Hintergrund. Solange aktiv, landet der Launcher beim Schließen (X) nur im Infobereich neben der Uhr, statt sich zu beenden – über das Symbol dort lässt er sich jederzeit wieder öffnen oder wirklich beenden.</small>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -294,6 +333,14 @@
|
||||
<input id="s-bgskin-opacity" type="range" min="10" max="100" step="5" />
|
||||
</label>
|
||||
</div>
|
||||
<label class="checkbox-field">
|
||||
<input id="s-bgskin-cycle" type="checkbox" /> <span>Bei mehreren angemeldeten Konten automatisch durchwechseln</span>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Wechsel alle <small class="opt">(Sekunden)</small></span>
|
||||
<input id="s-bgskin-cycle-seconds" type="number" min="5" step="5" />
|
||||
<small class="hint">Läuft nur, wenn 2 oder mehr Konten angemeldet sind, und beginnt nach dem letzten Konto wieder von vorn.</small>
|
||||
</label>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
@@ -614,6 +661,7 @@
|
||||
<option value="mod">Mods</option>
|
||||
<option value="resourcepack">Ressourcenpakete</option>
|
||||
<option value="shader">Shader</option>
|
||||
<option value="datapack">Datenpakete</option>
|
||||
</select>
|
||||
<input id="m-query" type="text" placeholder="Mods durchsuchen … (z. B. Sodium, JEI, Create)" />
|
||||
<button id="m-search-btn" class="btn btn-primary">Suchen</button>
|
||||
@@ -659,8 +707,14 @@
|
||||
</div>
|
||||
<div id="m-pane-installed" class="hidden">
|
||||
<div class="mmc-bar">
|
||||
<button id="m-add-file" class="btn btn-mini" data-ic="plus">Datei hinzufügen…</button>
|
||||
<button id="m-check-updates" class="btn btn-mini">Nach Updates suchen</button>
|
||||
<button id="m-apply-updates" class="btn btn-primary btn-mini hidden">Alle aktualisieren</button>
|
||||
<select id="m-installed-sort" title="Sortierung">
|
||||
<option value="name-asc">Name (A-Z)</option>
|
||||
<option value="name-desc">Name (Z-A)</option>
|
||||
<option value="status">Status (aktiv zuerst)</option>
|
||||
</select>
|
||||
<span id="m-update-status" class="hint"></span>
|
||||
</div>
|
||||
<div id="m-installed" class="mods-list"><div class="mods-empty">Noch keine Mods.</div></div>
|
||||
@@ -752,6 +806,123 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Spielaufzeichnung ===================== -->
|
||||
<div id="modal-record" class="modal hidden">
|
||||
<div class="modal-card modal-lg">
|
||||
<div class="modal-head">
|
||||
<h3 id="record-title">Spielaufzeichnung</h3>
|
||||
<button class="modal-close" data-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="record-search" class="record-search">
|
||||
<p id="record-search-text">Suche Spielfenster …</p>
|
||||
<button id="record-search-retry" class="btn hidden">Erneut suchen</button>
|
||||
</div>
|
||||
|
||||
<div id="record-picker" class="screens-grid hidden"></div>
|
||||
|
||||
<div id="record-stage" class="record-stage hidden">
|
||||
<video id="record-preview" class="record-video" autoplay muted></video>
|
||||
<div class="record-audio-bar">
|
||||
<span class="hint">Video: <span id="record-video-summary">Nativ · Mittel</span></span>
|
||||
<button id="record-video-edit" class="btn btn-mini">Video-Einstellungen…</button>
|
||||
</div>
|
||||
<div class="record-audio-bar">
|
||||
<span class="hint">Audio: <span id="record-audio-summary">Systemton</span></span>
|
||||
<button id="record-audio-edit" class="btn btn-mini">Audioquellen wählen…</button>
|
||||
</div>
|
||||
<div class="record-toolbar">
|
||||
<div id="record-live-badge" class="record-live-badge hidden">
|
||||
<span class="rec-dot"></span>
|
||||
<span>Aufnahme läuft · <span id="record-timer">00:00</span></span>
|
||||
</div>
|
||||
<button id="record-toggle" class="btn btn-primary">Aufnahme starten</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button id="record-open-folder" class="btn" data-ic="folder">Ordner öffnen</button>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn" data-close>Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Video-Einstellungen der Aufzeichnung ===================== -->
|
||||
<div id="modal-record-video" class="modal hidden">
|
||||
<div class="modal-card modal-sm">
|
||||
<div class="modal-head">
|
||||
<h3>Video-Einstellungen</h3>
|
||||
<button class="modal-close" data-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="field-row">
|
||||
<label class="field">
|
||||
<span>Auflösung</span>
|
||||
<select id="record-resolution">
|
||||
<option value="native">Nativ (Fenstergröße)</option>
|
||||
<option value="1080">1080p</option>
|
||||
<option value="720">720p</option>
|
||||
<option value="480">480p</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Qualität</span>
|
||||
<select id="record-quality">
|
||||
<option value="low">Niedrig (kleinere Datei)</option>
|
||||
<option value="medium" selected>Mittel</option>
|
||||
<option value="high">Hoch (größere Datei)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Bildrate</span>
|
||||
<select id="record-fps">
|
||||
<option value="30" selected>30 fps</option>
|
||||
<option value="60">60 fps (größere Datei)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p id="record-video-hint" class="hint hidden">Gilt erst für die nächste Aufnahme – während einer laufenden Aufnahme nicht änderbar.</p>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<div class="spacer"></div>
|
||||
<button class="btn btn-primary" data-close>Fertig</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Audioquellen der Aufzeichnung ===================== -->
|
||||
<div id="modal-record-audio" class="modal hidden">
|
||||
<div class="modal-card modal-sm">
|
||||
<div class="modal-head">
|
||||
<h3>Audioquellen wählen</h3>
|
||||
<button class="modal-close" data-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="hint">Mehrfachauswahl möglich – alle angehakten Quellen werden zu einer Tonspur gemischt, Lautstärke je Quelle einstellbar.</p>
|
||||
<div class="record-source-row">
|
||||
<label class="checkbox-field">
|
||||
<input id="record-src-system" type="checkbox" checked /> <span>Systemton</span>
|
||||
</label>
|
||||
<input id="record-vol-system" class="record-vol" type="range" min="0" max="100" value="100" title="Lautstärke" />
|
||||
</div>
|
||||
<div class="record-source-row">
|
||||
<label class="checkbox-field">
|
||||
<input id="record-src-game" type="checkbox" disabled /> <span>Nur Minecraft (isoliert)</span>
|
||||
</label>
|
||||
<input id="record-vol-game" class="record-vol" type="range" min="0" max="100" value="100" title="Lautstärke" />
|
||||
</div>
|
||||
<p id="record-src-game-hint" class="hint hidden">Kein laufender Minecraft-Prozess erkannt – bitte über den Launcher starten.</p>
|
||||
<div id="record-mic-list" class="record-mic-list"></div>
|
||||
<p class="hint" style="margin-top:10px">Systemton ist immer der komplette Mix (Spiel, Discord, Musik, Benachrichtigungen usw.). "Nur Minecraft" nimmt ausschließlich den Ton des Spiels auf (z. B. wenn nebenbei Musik läuft, die nicht mit rein soll) – beides lässt sich auch kombiniert mit einem Mikrofon anhaken.</p>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<div class="spacer"></div>
|
||||
<button class="btn btn-primary" data-close>Fertig</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Screenshots ===================== -->
|
||||
<div id="modal-screens" class="modal hidden">
|
||||
<div class="modal-card modal-lg">
|
||||
@@ -808,7 +979,7 @@
|
||||
<div class="field-row" style="margin-top:12px">
|
||||
<label class="field"><span>Name</span><input id="srv-name" type="text" placeholder="z. B. Mein Server" /></label>
|
||||
<label class="field"><span>Adresse</span><input id="srv-ip" type="text" placeholder="mc.example.de oder 192.168.1.50:25566" /></label>
|
||||
<button id="srv-add" class="btn btn-primary" style="align-self:flex-end">Hinzufügen</button>
|
||||
<div class="field" style="flex:0 0 auto"><span> </span><button id="srv-add" class="btn btn-primary">Hinzufügen</button></div>
|
||||
</div>
|
||||
<div id="srv-list" class="mods-list" style="margin-top:12px"></div>
|
||||
</div>
|
||||
@@ -851,19 +1022,19 @@
|
||||
<div class="skin-spalte">
|
||||
<div class="skin-label">Aktuell</div>
|
||||
<div class="skin-bild">
|
||||
<img id="skin-aktuell" alt="" class="hidden" />
|
||||
<canvas id="skin-aktuell" class="hidden"></canvas>
|
||||
<span id="skin-aktuell-leer" class="hint">wird geladen …</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="skin-spalte">
|
||||
<div class="skin-label">Neu</div>
|
||||
<div class="skin-bild">
|
||||
<img id="skin-neu" alt="" class="hidden" />
|
||||
<canvas id="skin-neu" class="hidden"></canvas>
|
||||
<span id="skin-neu-leer" class="hint">noch keine Datei gewählt</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="hint">Die Vorschau zeigt die Figur so, wie sie im Spiel aussieht. Die Armform wirkt sich auf die Darstellung aus.</small>
|
||||
<small class="hint">Mit der Maus ziehen dreht die Figur, Mausrad zoomt. Die Armform wirkt sich auf die Darstellung aus.</small>
|
||||
<div class="path-row" style="margin-top:12px">
|
||||
<input id="skin-pfad" type="text" readonly placeholder="PNG mit 64×64 Pixeln" />
|
||||
<button id="skin-pick" class="btn">Datei wählen…</button>
|
||||
@@ -937,6 +1108,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Lizenz ===================== -->
|
||||
<div id="modal-license" class="modal hidden">
|
||||
<div class="modal-card modal-lg">
|
||||
<div class="modal-head">
|
||||
<h3>Lizenz</h3>
|
||||
<button class="modal-close" data-close>✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="license-text" class="crash-voll">Lade …</pre>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<button class="btn" data-close>Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== Dialog: Update ===================== -->
|
||||
<div id="modal-update" class="modal hidden">
|
||||
<div class="modal-card modal-sm">
|
||||
@@ -980,6 +1167,7 @@
|
||||
<button id="import-tab-mc" class="mtab" data-kind="mc">Minecraft Launcher</button>
|
||||
<button id="import-tab-cf" class="mtab" data-kind="cf">CurseForge</button>
|
||||
<button id="import-tab-mmc" class="mtab" data-kind="mmc">MultiMC / Prism</button>
|
||||
<button id="import-tab-technic" class="mtab" data-kind="technic">Technic</button>
|
||||
</div>
|
||||
|
||||
<div id="import-pane-zip" class="import-pane">
|
||||
@@ -989,11 +1177,18 @@
|
||||
|
||||
<div id="import-pane-modpack" class="import-pane hidden">
|
||||
<p class="import-copy">Modpack aus dem Katalog installieren oder als Datei importieren.</p>
|
||||
<p class="hint">Suche läuft über Modrinth. Datei-Import unterstützt zusätzlich CurseForge-ZIP (manifest.json) – dafür ist ein API-Key oder eine Weiterleitung in den Netzwerk-Einstellungen nötig.</p>
|
||||
<p class="hint">Katalog-Suche läuft über Modrinth oder CurseForge. Für CurseForge wird ein eigener API-Key in den Netzwerk-Einstellungen gebraucht (die Weiterleitung reicht nur für den Datei-Import). Datei-Import unterstützt zusätzlich CurseForge-ZIP (manifest.json).</p>
|
||||
|
||||
<div class="field-row" style="margin-top:12px;align-items:flex-end">
|
||||
<label class="field" style="width:150px">
|
||||
<span>Quelle</span>
|
||||
<select id="mp-source">
|
||||
<option value="modrinth">Modrinth</option>
|
||||
<option value="curseforge">CurseForge</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="field" style="flex:1">
|
||||
<span>Suche (Modrinth)</span>
|
||||
<span>Suche</span>
|
||||
<input id="mp-query" type="text" placeholder="z. B. Fabulously Optimized, Create …" autocomplete="off" />
|
||||
</label>
|
||||
<label class="field" style="width:140px">
|
||||
@@ -1089,11 +1284,51 @@
|
||||
<div id="mmc-list" class="mods-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="import-pane-technic" class="import-pane hidden">
|
||||
<p class="import-copy">Modpack direkt von der Technic-Plattform installieren oder eine bereits vorhandene Technic-Installation importieren.</p>
|
||||
|
||||
<div class="field-row" style="align-items:flex-end">
|
||||
<label class="field" style="flex:1">
|
||||
<span>Technic-Katalog durchsuchen</span>
|
||||
<input id="tp-query" type="text" placeholder="z. B. Hexxit, Tekxit …" autocomplete="off" />
|
||||
</label>
|
||||
<button id="tp-search-btn" class="btn btn-primary">Suchen</button>
|
||||
</div>
|
||||
<div id="tp-status" class="hint" style="margin-top:8px">Gib einen Suchbegriff ein oder lasse das Feld leer für beliebte Packs.</div>
|
||||
<div id="tp-results" class="mods-list" style="margin-top:10px;max-height:220px;overflow:auto"></div>
|
||||
|
||||
<div class="modal-sep" style="margin:16px 0"></div>
|
||||
|
||||
<label class="field">
|
||||
<span>Oder: vorhandenen Technic-Ordner importieren</span>
|
||||
<div class="path-row">
|
||||
<input id="technic-path" type="text" placeholder="z. B. C:\Users\DeinName\AppData\Roaming\.technic" />
|
||||
<button id="technic-pick" class="btn">Wählen…</button>
|
||||
<button id="technic-scan" class="btn btn-primary">Suchen</button>
|
||||
</div>
|
||||
<small id="technic-status" class="hint">Leer lassen nutzt automatisch den Standardpfad unter Windows (%APPDATA%\.technic).</small>
|
||||
</label>
|
||||
|
||||
<div id="technic-result" class="hidden">
|
||||
<div class="mmc-bar">
|
||||
<label class="checkbox-field" style="margin:0">
|
||||
<input id="technic-all" type="checkbox" checked /> <span>Alle auswählen</span>
|
||||
</label>
|
||||
<label class="checkbox-field" style="margin:0">
|
||||
<input id="technic-copy" type="checkbox" checked /> <span>Spieldaten mitkopieren (Mods, Welten, Konfiguration)</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="technic-list" class="mods-list"></div>
|
||||
<p class="hint" style="margin-top:8px">Minecraft-Version und Mod-Loader werden nach bestem Wissen aus dem Pack erkannt – bitte nach dem Import kurz prüfen, falls ein Pack ungewöhnlich aufgebaut ist.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-foot">
|
||||
<div id="mc-progress" class="hint hidden"></div>
|
||||
<div id="mmc-progress" class="hint hidden"></div>
|
||||
<div id="cf-progress" class="hint hidden"></div>
|
||||
<div id="technic-progress" class="hint hidden"></div>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn" data-close>Abbrechen</button>
|
||||
<button id="import-zip-do" class="btn btn-primary">ZIP wählen…</button>
|
||||
@@ -1101,6 +1336,7 @@
|
||||
<button id="mc-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
||||
<button id="cf-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
||||
<button id="mmc-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
||||
<button id="technic-do" class="btn btn-primary hidden" disabled>Übernehmen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1162,6 +1398,7 @@
|
||||
<button class="ctx-item" data-act="p-mods">Mods anzeigen</button>
|
||||
<button class="ctx-item" data-act="p-worlds">Welten verwalten</button>
|
||||
<button class="ctx-item" data-act="p-screens">Bildschirmfotos verwalten</button>
|
||||
<button class="ctx-item" data-act="p-record">Spiel aufzeichnen</button>
|
||||
<button class="ctx-item" data-act="p-logs">Logdateien ansehen</button>
|
||||
<button class="ctx-item" data-act="p-servers">Server verwalten</button>
|
||||
<div class="ctx-sep"></div>
|
||||
|
||||
@@ -464,6 +464,37 @@ async function prepareAndLaunch(opts, onProgress) {
|
||||
classpath.unshift(path.join(libDir, rel.replace(/\//g, path.sep)));
|
||||
}
|
||||
effectiveVj = mergeLoader(vj, fvj);
|
||||
|
||||
} else if (opts.loader === 'optifine') {
|
||||
/*
|
||||
* OptiFine hat keine öffentliche Installations-API (Downloads laufen bewusst
|
||||
* über eine Werbeseite). AeroMC lädt hier nichts selbst herunter – der Nutzer
|
||||
* hat den offiziellen Installer bereits einmal sichtbar laufen lassen (siehe
|
||||
* optifine:setup/optifine:confirm in main.js), der dabei ein normales
|
||||
* Versions-Profil unter <sharedDir>/versions/<loaderVersion>/ abgelegt hat,
|
||||
* genau wie Fabric/Forge es tun. Wir lesen es nur noch ein und mergen es.
|
||||
*/
|
||||
const ofId = opts.loaderVersion;
|
||||
if (!ofId) {
|
||||
return { ok: false, message: 'Für diese Instanz ist keine OptiFine-Version hinterlegt. Bitte im Bearbeiten-Dialog einrichten.' };
|
||||
}
|
||||
const ofJsonPath = path.join(opts.sharedDir, 'versions', ofId, ofId + '.json');
|
||||
if (!fs.existsSync(ofJsonPath)) {
|
||||
return { ok: false, message: `OptiFine-Profil "${ofId}" wurde nicht gefunden. Bitte die Einrichtung im Bearbeiten-Dialog wiederholen.` };
|
||||
}
|
||||
let ofvj;
|
||||
try { ofvj = JSON.parse(fs.readFileSync(ofJsonPath, 'utf8')); }
|
||||
catch (err) { return { ok: false, message: 'OptiFine-Profil ist beschädigt: ' + err.message }; }
|
||||
for (const lib of ofvj.libraries || []) {
|
||||
const art = lib.downloads && lib.downloads.artifact;
|
||||
const rel = (art && art.path) || forge.mavenPath(lib.name);
|
||||
const dest = path.join(libDir, rel.replace(/\//g, path.sep));
|
||||
if (!fs.existsSync(dest) && art && art.url) {
|
||||
await downloadIfNeeded(art.url, dest, art.sha1 || null);
|
||||
}
|
||||
classpath.unshift(dest);
|
||||
}
|
||||
effectiveVj = mergeLoader(vj, ofvj);
|
||||
}
|
||||
|
||||
const finalClasspath = dedupeClasspath(classpath, opts.sharedDir);
|
||||
|
||||
+995
-106
File diff suppressed because it is too large
Load Diff
+150
@@ -0,0 +1,150 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* mcping.js – "Server List Ping" (SLP), das Protokoll, das auch der
|
||||
* Vanilla-Mehrspieler-Bildschirm nutzt, um MOTD/Spielerzahl/Version
|
||||
* abzufragen, ohne sich wirklich zu verbinden.
|
||||
*
|
||||
* Ablauf (siehe minecraft.wiki/w/Server_List_Ping):
|
||||
* 1) Handshake-Paket senden (Protokollversion, Adresse, Port, next state=1)
|
||||
* 2) Status-Request-Paket senden (leer)
|
||||
* 3) Status-Response-Paket empfangen: enthält einen JSON-String
|
||||
* Alle Pakete sind mit ihrer Länge als VarInt vorangestellt.
|
||||
*/
|
||||
|
||||
const net = require('net');
|
||||
|
||||
function encodeVarInt(value) {
|
||||
const bytes = [];
|
||||
let v = value;
|
||||
do {
|
||||
let temp = v & 0b01111111;
|
||||
v = v >>> 7;
|
||||
if (v !== 0) temp |= 0b10000000;
|
||||
bytes.push(temp);
|
||||
} while (v !== 0);
|
||||
return Buffer.from(bytes);
|
||||
}
|
||||
|
||||
function encodeString(str) {
|
||||
const strBuf = Buffer.from(str, 'utf8');
|
||||
return Buffer.concat([encodeVarInt(strBuf.length), strBuf]);
|
||||
}
|
||||
|
||||
function encodeUShort(n) {
|
||||
const b = Buffer.alloc(2);
|
||||
b.writeUInt16BE(n & 0xffff, 0);
|
||||
return b;
|
||||
}
|
||||
|
||||
// liest einen VarInt aus buf ab offset -> { value, next } oder null,
|
||||
// wenn noch nicht genug Bytes angekommen sind
|
||||
function decodeVarInt(buf, offset) {
|
||||
let numRead = 0;
|
||||
let result = 0;
|
||||
let read;
|
||||
do {
|
||||
if (offset + numRead >= buf.length) return null;
|
||||
read = buf[offset + numRead];
|
||||
result |= (read & 0b01111111) << (7 * numRead);
|
||||
numRead++;
|
||||
if (numRead > 5) throw new Error('VarInt zu groß');
|
||||
} while ((read & 0b10000000) !== 0);
|
||||
return { value: result >>> 0, next: offset + numRead };
|
||||
}
|
||||
|
||||
// "description" kann ein reiner String oder eine Chat-Komponente
|
||||
// ({text, extra:[...]}) sein – hier auf reinen Text reduzieren
|
||||
function extractMotdText(desc) {
|
||||
if (!desc) return '';
|
||||
if (typeof desc === 'string') return desc;
|
||||
let text = String(desc.text || '');
|
||||
if (Array.isArray(desc.extra)) {
|
||||
for (const part of desc.extra) text += extractMotdText(part);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
// Formatierungscodes (§a, §l, …) sind fürs Spiel gedacht, nicht für unsere Liste
|
||||
function stripFormatting(text) {
|
||||
return String(text || '').replace(/§./g, '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
/*
|
||||
* Fragt einen Server ab.
|
||||
* -> { ok, motd, version, protocol, online, max, sample, favicon, latencyMs }
|
||||
* oder { ok: false, message }
|
||||
*/
|
||||
function pingServer(host, port = 25565, timeoutMs = 4000) {
|
||||
return new Promise((resolve) => {
|
||||
const start = Date.now();
|
||||
const socket = new net.Socket();
|
||||
let buffer = Buffer.alloc(0);
|
||||
let finished = false;
|
||||
|
||||
const done = (result) => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
try { socket.destroy(); } catch { /* egal */ }
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
socket.setTimeout(timeoutMs);
|
||||
socket.on('timeout', () => done({ ok: false, message: 'Zeitüberschreitung.' }));
|
||||
socket.on('error', (err) => done({ ok: false, message: err.code === 'ENOTFOUND' ? 'Adresse nicht gefunden.' : err.message }));
|
||||
|
||||
socket.connect(port, host, () => {
|
||||
const handshakeBody = Buffer.concat([
|
||||
encodeVarInt(0x00),
|
||||
encodeVarInt(-1), // Protokollversion "unbekannt" – für den Status-Abfrage-Zweck egal
|
||||
encodeString(host),
|
||||
encodeUShort(port),
|
||||
encodeVarInt(1), // next state: 1 = status
|
||||
]);
|
||||
const handshakePacket = Buffer.concat([encodeVarInt(handshakeBody.length), handshakeBody]);
|
||||
|
||||
const requestBody = encodeVarInt(0x00);
|
||||
const requestPacket = Buffer.concat([encodeVarInt(requestBody.length), requestBody]);
|
||||
|
||||
socket.write(Buffer.concat([handshakePacket, requestPacket]));
|
||||
});
|
||||
|
||||
socket.on('data', (chunk) => {
|
||||
if (finished) return;
|
||||
buffer = Buffer.concat([buffer, chunk]);
|
||||
try {
|
||||
const lenInfo = decodeVarInt(buffer, 0);
|
||||
if (!lenInfo) return; // Länge noch nicht vollständig angekommen
|
||||
const packetEnd = lenInfo.next + lenInfo.value;
|
||||
if (buffer.length < packetEnd) return; // Paket noch nicht vollständig da
|
||||
|
||||
const idInfo = decodeVarInt(buffer, lenInfo.next);
|
||||
if (!idInfo) return;
|
||||
const jsonLenInfo = decodeVarInt(buffer, idInfo.next);
|
||||
if (!jsonLenInfo) return;
|
||||
const jsonStart = jsonLenInfo.next;
|
||||
const jsonEnd = jsonStart + jsonLenInfo.value;
|
||||
if (buffer.length < jsonEnd) return;
|
||||
|
||||
const data = JSON.parse(buffer.toString('utf8', jsonStart, jsonEnd));
|
||||
done({
|
||||
ok: true,
|
||||
latencyMs: Date.now() - start,
|
||||
motd: stripFormatting(extractMotdText(data.description)),
|
||||
version: (data.version && data.version.name) || '',
|
||||
protocol: data.version ? data.version.protocol : null,
|
||||
online: data.players ? data.players.online : null,
|
||||
max: data.players ? data.players.max : null,
|
||||
sample: (data.players && Array.isArray(data.players.sample))
|
||||
? data.players.sample.map((p) => String(p.name || '')).filter(Boolean)
|
||||
: [],
|
||||
favicon: typeof data.favicon === 'string' ? data.favicon : '',
|
||||
});
|
||||
} catch (err) {
|
||||
done({ ok: false, message: 'Ungültige Antwort: ' + err.message });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { pingServer };
|
||||
@@ -0,0 +1,50 @@
|
||||
// AudioWorklet-Prozessor für die isolierte "Nur Minecraft"-Tonaufnahme.
|
||||
//
|
||||
// loopback-capture liefert PCM-Häppchen unregelmäßig getaktet über IPC (mal
|
||||
// etwas früher, mal etwas später). Würde man sie direkt nacheinander als
|
||||
// einzelne AudioBufferSourceNode-Aufrufe abspielen, würde jede kleine
|
||||
// Verzögerung als hörbares Knacksen/Aussetzer auffallen. Stattdessen landen
|
||||
// ankommende Häppchen hier in einem Ringpuffer; der Audio-Rendering-Thread
|
||||
// liest in gleichmäßigem Takt (process() läuft alle 128 Samples) daraus.
|
||||
// Kleine Zeitschwankungen bei der Anlieferung werden so unhörbar
|
||||
// abgefedert. Läuft der Puffer doch mal leer, gibt es kurz Stille statt
|
||||
// eines Knacksens.
|
||||
class PcmRingBufferProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.capacity = sampleRate * 2; // 2 Sekunden Puffer je Kanal
|
||||
this.ring = [new Float32Array(this.capacity), new Float32Array(this.capacity)];
|
||||
this.writeIdx = 0;
|
||||
this.readIdx = 0;
|
||||
this.available = 0; // gepufferte Frames (nicht Samples)
|
||||
this.port.onmessage = (e) => this._enqueue(e.data);
|
||||
}
|
||||
|
||||
_enqueue([left, right]) {
|
||||
const n = left.length;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const idx = (this.writeIdx + i) % this.capacity;
|
||||
this.ring[0][idx] = left[i];
|
||||
this.ring[1][idx] = right[i];
|
||||
}
|
||||
this.writeIdx = (this.writeIdx + n) % this.capacity;
|
||||
this.available = Math.min(this.capacity, this.available + n);
|
||||
}
|
||||
|
||||
process(_inputs, outputs) {
|
||||
const output = outputs[0];
|
||||
const frames = output[0].length;
|
||||
const playable = Math.min(frames, this.available);
|
||||
for (let i = 0; i < playable; i++) {
|
||||
const idx = (this.readIdx + i) % this.capacity;
|
||||
output[0][i] = this.ring[0][idx];
|
||||
output[1][i] = this.ring[1][idx];
|
||||
}
|
||||
for (let i = playable; i < frames; i++) { output[0][i] = 0; output[1][i] = 0; }
|
||||
this.readIdx = (this.readIdx + playable) % this.capacity;
|
||||
this.available -= playable;
|
||||
return true; // Prozessor am Leben halten, auch wenn gerade nichts ankommt
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('pcm-player', PcmRingBufferProcessor);
|
||||
+104
-3
@@ -7,6 +7,15 @@
|
||||
|
||||
const { contextBridge, ipcRenderer } = require('electron');
|
||||
|
||||
// Debug-Hilfe: JS-Fehler im Renderer landen im Terminal des Hauptprozesses,
|
||||
// damit man sie auch ohne offene DevTools sieht
|
||||
window.addEventListener('error', (e) => {
|
||||
ipcRenderer.send('debug:rendererError', (e.error && e.error.stack) || e.message);
|
||||
});
|
||||
window.addEventListener('unhandledrejection', (e) => {
|
||||
ipcRenderer.send('debug:rendererError', 'unhandledrejection: ' + ((e.reason && e.reason.stack) || e.reason));
|
||||
});
|
||||
|
||||
contextBridge.exposeInMainWorld('api', {
|
||||
// Einstellungen
|
||||
getSettings: () => ipcRenderer.invoke('settings:get'),
|
||||
@@ -19,6 +28,9 @@ contextBridge.exposeInMainWorld('api', {
|
||||
// Minecraft-Versionen (Mojang)
|
||||
mcVersions: () => ipcRenderer.invoke('mc:versions'),
|
||||
loaderVersions: (loader, mcVersion) => ipcRenderer.invoke('loaders:versions', loader, mcVersion),
|
||||
optifineFind: (mcVersion) => ipcRenderer.invoke('optifine:find', mcVersion),
|
||||
optifineSetup: (id) => ipcRenderer.invoke('optifine:setup', id),
|
||||
optifineConfirm: (id) => ipcRenderer.invoke('optifine:confirm', id),
|
||||
|
||||
// Mods (Modrinth)
|
||||
modSearch: (opts) => ipcRenderer.invoke('mods:search', opts),
|
||||
@@ -27,6 +39,8 @@ contextBridge.exposeInMainWorld('api', {
|
||||
modList: (id, art) => ipcRenderer.invoke('mods:list', id, art),
|
||||
modInstall: (id, project, art) => ipcRenderer.invoke('mods:install', id, project, art),
|
||||
modRemove: (id, filename, art) => ipcRenderer.invoke('mods:remove', id, filename, art),
|
||||
modDependents: (id, filename, art) => ipcRenderer.invoke('mods:dependents', id, filename, art),
|
||||
modAddFile: (id, art) => ipcRenderer.invoke('mods:addFile', id, art),
|
||||
modSetEnabled: (id, filename, enabled, art) => ipcRenderer.invoke('mods:setEnabled', id, filename, enabled, art),
|
||||
systemMemory: () => ipcRenderer.invoke('system:memory'),
|
||||
checkMemory: (minMb, maxMb) => ipcRenderer.invoke('system:checkMemory', minMb, maxMb),
|
||||
@@ -42,6 +56,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
serversRemove: (id, ip) => ipcRenderer.invoke('servers:remove', id, ip),
|
||||
serversRename: (id, ip, neuerName) => ipcRenderer.invoke('servers:rename', id, ip, neuerName),
|
||||
serversCopyTo: (zielIds, eintrag) => ipcRenderer.invoke('servers:copyTo', zielIds, eintrag),
|
||||
serversPing: (ip) => ipcRenderer.invoke('servers:ping', ip),
|
||||
logsList: (id) => ipcRenderer.invoke('logs:list', id),
|
||||
logsRead: (id, name) => ipcRenderer.invoke('logs:read', id, name),
|
||||
logsOpenFolder: (id) => ipcRenderer.invoke('logs:openFolder', id),
|
||||
@@ -68,6 +83,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
resourcepacks: (id) => ipcRenderer.invoke('instance:resourcepacks', id),
|
||||
datapacks: (id) => ipcRenderer.invoke('instance:datapacks', id),
|
||||
deleteContent: (id, sub, name) => ipcRenderer.invoke('instance:deleteContent', id, sub, name),
|
||||
backupWorld: (id, worldName) => ipcRenderer.invoke('world:backup', id, worldName),
|
||||
openSubdir: (id, sub) => ipcRenderer.invoke('instance:openSubdir', id, sub),
|
||||
screenshots: (id) => ipcRenderer.invoke('instance:screenshots', id),
|
||||
screenshotData: (id, name) => ipcRenderer.invoke('instance:screenshotData', id, name),
|
||||
@@ -83,16 +99,24 @@ contextBridge.exposeInMainWorld('api', {
|
||||
return () => ipcRenderer.removeListener('backup:progress', l);
|
||||
},
|
||||
importInstance: () => ipcRenderer.invoke('instances:import'),
|
||||
createShortcut: (id) => ipcRenderer.invoke('instances:shortcut', id),
|
||||
// Start über eine Desktop-Verknüpfung ("--launch <id>")
|
||||
createShortcut: (id, customName) => ipcRenderer.invoke('instances:shortcut', id, customName),
|
||||
serversShortcut: (id, ip, name, customName) => ipcRenderer.invoke('servers:shortcut', id, ip, name, customName),
|
||||
// Start über eine Desktop-Verknüpfung ("--launch=<id>", optional "--server=<ip>")
|
||||
onLaunchRequest: (cb) => {
|
||||
const l = (_e, id) => cb(id);
|
||||
const l = (_e, payload) => cb(payload);
|
||||
ipcRenderer.on('instance:launch-request', l);
|
||||
return () => ipcRenderer.removeListener('instance:launch-request', l);
|
||||
},
|
||||
// Signalisiert dem Hauptprozess, dass onLaunchRequest jetzt registriert ist –
|
||||
// verhindert, dass ein Start-Wunsch aus einer Verknüpfung verloren geht,
|
||||
// weil er ankommt, bevor die Oberfläche fertig eingerichtet ist
|
||||
notifyReady: () => ipcRenderer.send('renderer:ready'),
|
||||
importModpack: () => ipcRenderer.invoke('modpack:import'),
|
||||
// Datei per Drag&Drop aufs Fenster gezogen (.jar/.mrpack/.zip)
|
||||
filesDrop: (filePath, targetId, targetArt) => ipcRenderer.invoke('files:drop', filePath, targetId, targetArt),
|
||||
modpackSearch: (opts) => ipcRenderer.invoke('modpack:search', opts),
|
||||
modpackInstallFromModrinth: (project) => ipcRenderer.invoke('modpack:installFromModrinth', project),
|
||||
modpackInstallFromCurseForge: (project) => ipcRenderer.invoke('modpack:installFromCurseForge', project),
|
||||
modpackCheck: (id) => ipcRenderer.invoke('modpack:check', id),
|
||||
modpackUpdate: (id, versionId) => ipcRenderer.invoke('modpack:update', id, versionId),
|
||||
onLaunchProgress: (cb) => {
|
||||
@@ -169,6 +193,21 @@ contextBridge.exposeInMainWorld('api', {
|
||||
ipcRenderer.on('mc:progress', l);
|
||||
return () => ipcRenderer.removeListener('mc:progress', l);
|
||||
},
|
||||
technicPickFolder: () => ipcRenderer.invoke('technic:pickFolder'),
|
||||
technicScan: (folder) => ipcRenderer.invoke('technic:scan', folder),
|
||||
technicImport: (entries, copyData) => ipcRenderer.invoke('technic:import', entries, copyData),
|
||||
onTechnicProgress: (cb) => {
|
||||
const l = (_e, d) => cb(d);
|
||||
ipcRenderer.on('technic:progress', l);
|
||||
return () => ipcRenderer.removeListener('technic:progress', l);
|
||||
},
|
||||
technicPlatformSearch: (query) => ipcRenderer.invoke('technicPlatform:search', query),
|
||||
technicPlatformInstall: (slug) => ipcRenderer.invoke('technicPlatform:install', slug),
|
||||
onTechnicPlatformProgress: (cb) => {
|
||||
const l = (_e, d) => cb(d);
|
||||
ipcRenderer.on('technicPlatform:progress', l);
|
||||
return () => ipcRenderer.removeListener('technicPlatform:progress', l);
|
||||
},
|
||||
|
||||
// Bedrock (Windows-Store-Ausgabe)
|
||||
bedrockDetect: () => ipcRenderer.invoke('bedrock:detect'),
|
||||
@@ -192,6 +231,14 @@ contextBridge.exposeInMainWorld('api', {
|
||||
ipcRenderer.on('update:available', l);
|
||||
return () => ipcRenderer.removeListener('update:available', l);
|
||||
},
|
||||
// Ergebnis des automatischen Checks beim Start – auch wenn kein Update
|
||||
// verfügbar ist (im Unterschied zu onUpdateAvailable, das nur bei
|
||||
// verfügbarem Update feuert)
|
||||
onUpdateChecked: (cb) => {
|
||||
const l = (_e, d) => cb(d);
|
||||
ipcRenderer.on('update:checked', l);
|
||||
return () => ipcRenderer.removeListener('update:checked', l);
|
||||
},
|
||||
onUpdateProgress: (cb) => {
|
||||
const l = (_e, d) => cb(d);
|
||||
ipcRenderer.on('update:progress', l);
|
||||
@@ -202,4 +249,58 @@ contextBridge.exposeInMainWorld('api', {
|
||||
pickFolder: (title) => ipcRenderer.invoke('dialog:pickFolder', title),
|
||||
pickJava: () => ipcRenderer.invoke('dialog:pickJava'),
|
||||
appInfo: () => ipcRenderer.invoke('app:info'),
|
||||
appLicense: () => ipcRenderer.invoke('app:license'),
|
||||
|
||||
// Einstellungen/Konsole als eigenes, frei verschiebbares Fenster
|
||||
popoutOpen: (modalId, params) => ipcRenderer.send('popout:open', modalId, params),
|
||||
popoutHide: () => ipcRenderer.send('popout:hide'),
|
||||
onPopoutReopen: (cb) => {
|
||||
const l = (_e, params) => cb(params);
|
||||
ipcRenderer.on('popout:reopen', l);
|
||||
return () => ipcRenderer.removeListener('popout:reopen', l);
|
||||
},
|
||||
onSettingsChanged: (cb) => {
|
||||
const l = (_e, s) => cb(s);
|
||||
ipcRenderer.on('settings:changed', l);
|
||||
return () => ipcRenderer.removeListener('settings:changed', l);
|
||||
},
|
||||
onInstancesChanged: (cb) => {
|
||||
const l = () => cb();
|
||||
ipcRenderer.on('instances:changed', l);
|
||||
return () => ipcRenderer.removeListener('instances:changed', l);
|
||||
},
|
||||
onSkinChanged: (cb) => {
|
||||
const l = () => cb();
|
||||
ipcRenderer.on('skin:changed', l);
|
||||
return () => ipcRenderer.removeListener('skin:changed', l);
|
||||
},
|
||||
updateGetPending: () => ipcRenderer.invoke('update:getPending'),
|
||||
|
||||
// Spielaufzeichnung
|
||||
recordingListWindows: () => ipcRenderer.invoke('recording:listWindows'),
|
||||
recordingGetAudioSourceId: () => ipcRenderer.invoke('recording:getAudioSourceId'),
|
||||
recordingSave: (instanceName, buffer, serverLabel) => ipcRenderer.invoke('recording:save', instanceName, buffer, serverLabel),
|
||||
recordingOpenFolder: (instanceName) => ipcRenderer.invoke('recording:openFolder', instanceName),
|
||||
recordingGetServer: (instanceId) => ipcRenderer.invoke('recording:getServer', instanceId),
|
||||
onRecordingServerDetected: (cb) => {
|
||||
const l = (_e, payload) => cb(payload);
|
||||
ipcRenderer.on('recording:serverDetected', l);
|
||||
return () => ipcRenderer.removeListener('recording:serverDetected', l);
|
||||
},
|
||||
recordingGetPid: (instanceId) => ipcRenderer.invoke('recording:getPid', instanceId),
|
||||
recordingSetCaptureTarget: (sourceId) => ipcRenderer.invoke('recording:setCaptureTarget', sourceId),
|
||||
recordingGetWindowBounds: (instanceId, fensterTitel) => ipcRenderer.invoke('recording:getWindowBounds', instanceId, fensterTitel),
|
||||
recordingStartGameAudio: (pid) => ipcRenderer.invoke('recording:startGameAudio', pid),
|
||||
recordingStopGameAudio: () => ipcRenderer.invoke('recording:stopGameAudio'),
|
||||
onGameAudioChunk: (cb) => {
|
||||
const l = (_e, chunk) => cb(chunk);
|
||||
ipcRenderer.on('recording:gameAudioChunk', l);
|
||||
return () => ipcRenderer.removeListener('recording:gameAudioChunk', l);
|
||||
},
|
||||
recordingSetGuard: (active) => ipcRenderer.send('recording:guard', active),
|
||||
onRecordCloseAttempt: (cb) => {
|
||||
const l = () => cb();
|
||||
ipcRenderer.on('record:closeAttempt', l);
|
||||
return () => ipcRenderer.removeListener('record:closeAttempt', l);
|
||||
},
|
||||
});
|
||||
|
||||
+1519
-107
File diff suppressed because it is too large
Load Diff
+17
-2
@@ -236,6 +236,7 @@ const INHALTSARTEN = {
|
||||
mod: { projectType: 'mod', ordner: 'mods', label: 'Mods', loaderNoetig: true },
|
||||
resourcepack: { projectType: 'resourcepack', ordner: 'resourcepacks', label: 'Ressourcenpakete', loaderNoetig: false },
|
||||
shader: { projectType: 'shader', ordner: 'shaderpacks', label: 'Shader', loaderNoetig: false },
|
||||
datapack: { projectType: 'datapack', ordner: 'datapacks', label: 'Datenpakete', loaderNoetig: false },
|
||||
modpack: { projectType: 'modpack', ordner: null, label: 'Modpacks', loaderNoetig: false },
|
||||
};
|
||||
|
||||
@@ -515,7 +516,9 @@ function forgeCdnUrl(fileId, fileName) {
|
||||
}
|
||||
|
||||
const CF_LOADER = { forge: 1, fabric: 4, quilt: 5, neoforge: 6 };
|
||||
const CF_CLASS = { mod: 6, resourcepack: 12, shader: 6552 };
|
||||
// classId je Inhaltsart bei gameId 432 (Minecraft). modpack=4471 ist die auf
|
||||
// CurseForge selbst verwendete Kategorie-ID (siehe curseforge.com/minecraft/modpacks).
|
||||
const CF_CLASS = { mod: 6, resourcepack: 12, shader: 6552, modpack: 4471 };
|
||||
|
||||
function cfLoaderType(loader) {
|
||||
return CF_LOADER[String(loader || '').toLowerCase()] || 0;
|
||||
@@ -598,7 +601,12 @@ async function curseforgeSearch(zugang, {
|
||||
+ 'Die Weiterleitung reicht für Downloads, nicht für den Katalog.',
|
||||
};
|
||||
}
|
||||
const classId = CF_CLASS[art] || CF_CLASS.mod;
|
||||
if (!CF_CLASS[art]) {
|
||||
// Kein zugeordneter CurseForge-Klassencode für diese Inhaltsart -> lieber gar
|
||||
// keine Treffer als falsch einsortierte (z. B. Mods unter "Datenpakete").
|
||||
return { total: 0, hits: [], error: 'CurseForge unterstützt diese Inhaltsart hier nicht – bitte Modrinth nutzen.' };
|
||||
}
|
||||
const classId = CF_CLASS[art];
|
||||
const url = new URL(CURSEFORGE + '/mods/search');
|
||||
url.searchParams.set('gameId', '432');
|
||||
url.searchParams.set('classId', String(classId));
|
||||
@@ -713,6 +721,12 @@ async function curseforgePickFile(zugang, modId, gameVersion, loader, art) {
|
||||
};
|
||||
}
|
||||
|
||||
// Beste Modpack-Datei eines CurseForge-Projekts auflösen (für den Katalog-Import,
|
||||
// analog zu modrinthResolveModpack).
|
||||
async function curseforgeResolveModpackFile(zugang, modId, gameVersion) {
|
||||
return curseforgePickFile(zugang, modId, gameVersion, '', 'modpack');
|
||||
}
|
||||
|
||||
async function curseforgeInstall({ zugang, projectId, title, gameVersion, loader, modsDir, art = 'mod' }) {
|
||||
if (!zugang) return { ok: false, message: 'Kein CurseForge-Zugang eingerichtet.' };
|
||||
const rawId = String(projectId || '').replace(/^cf-/, '');
|
||||
@@ -814,4 +828,5 @@ module.exports = {
|
||||
modrinthSearch, modrinthResolveFile, modrinthResolveModpack, modrinthInstall, modrinthProjectTitle,
|
||||
modrinthFindUpdates, downloadFile, downloadBuffer, inhaltsArt, INHALTSARTEN,
|
||||
curseforgeResolveFiles, curseforgeSearch, curseforgeInstall, curseforgeTestKey, forgeCdnUrl, curseforgeZugang,
|
||||
curseforgeResolveModpackFile,
|
||||
};
|
||||
|
||||
+40
-2
@@ -75,9 +75,14 @@ const DEFAULT_SETTINGS = {
|
||||
sortBy: 'lastPlayed', // lastPlayed | name
|
||||
iso3dIcons: true, // Block-Symbole räumlich darstellen
|
||||
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)
|
||||
bgSkinCycle: false, // bei 2+ angemeldeten Konten automatisch durchwechseln
|
||||
bgSkinCycleSeconds: 30, // Sekunden zwischen zwei Kontowechseln
|
||||
wrapperCommand: '', // Externe Programme: Startprogramm, das Java umschließt
|
||||
proxyEnabled: false, // HTTP-Proxy für Downloads/Netzwerk
|
||||
proxyHost: '',
|
||||
@@ -85,6 +90,16 @@ const DEFAULT_SETTINGS = {
|
||||
proxyUser: '',
|
||||
proxyPass: '',
|
||||
theme: 'dark',
|
||||
// Spielaufzeichnung - bleibt über Neustarts erhalten (letzte Wahl je Feld)
|
||||
recordingResolution: 'native',
|
||||
recordingQuality: 'medium',
|
||||
recordingFramerate: 30,
|
||||
recordingAudioSystem: true,
|
||||
recordingAudioGame: false,
|
||||
recordingVolSystem: 100,
|
||||
recordingVolGame: 100,
|
||||
recordingMicSelected: [], // Liste gewählter Mikrofon-deviceIds
|
||||
recordingMicVolumes: {}, // deviceId -> Lautstärke (0-100)
|
||||
};
|
||||
|
||||
function settingsFile() {
|
||||
@@ -130,8 +145,12 @@ function gameDir(id) {
|
||||
* "mod" behält die bisherigen Namen, damit vorhandene Instanzen unverändert
|
||||
* weiterlaufen.
|
||||
*/
|
||||
const INHALT_ORDNER = { mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks' };
|
||||
const INHALT_INDEX = { mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json' };
|
||||
const INHALT_ORDNER = {
|
||||
mod: 'mods', resourcepack: 'resourcepacks', shader: 'shaderpacks', datapack: 'datapacks',
|
||||
};
|
||||
const INHALT_INDEX = {
|
||||
mod: 'mods.json', resourcepack: 'resourcepacks.json', shader: 'shaders.json', datapack: 'datapacks.json',
|
||||
};
|
||||
|
||||
function inhaltsDir(id, art = 'mod') {
|
||||
return path.join(gameDir(id), INHALT_ORDNER[art] || INHALT_ORDNER.mod);
|
||||
@@ -232,6 +251,24 @@ function addMod(id, entry, art = 'mod') {
|
||||
return list;
|
||||
}
|
||||
|
||||
/*
|
||||
* Findet installierte Mods, die den per "filename" identifizierten Mod als
|
||||
* Pflicht-Abhängigkeit brauchen (requiredDeps wird beim Installieren über
|
||||
* AeroMC gesetzt – von Hand hinzugefügte oder vor diesem Feature installierte
|
||||
* Mods haben keine requiredDeps und werden daher nicht als Ziel erkannt,
|
||||
* können aber selbst weiterhin als Abhängiger auftauchen).
|
||||
* -> [Titel, Titel, ...] der abhängigen Mods
|
||||
*/
|
||||
function findDependents(id, filename, art = 'mod') {
|
||||
const list = listMods(id, art);
|
||||
const target = list.find((m) => m.filename === filename);
|
||||
if (!target || !target.projectId) return [];
|
||||
return list
|
||||
.filter((m) => m.filename !== filename
|
||||
&& Array.isArray(m.requiredDeps) && m.requiredDeps.includes(target.projectId))
|
||||
.map((m) => m.title || m.filename);
|
||||
}
|
||||
|
||||
function removeMod(id, filename, art = 'mod') {
|
||||
const list = listMods(id, art).filter((m) => m.filename !== filename);
|
||||
writeJson(modsIndexFile(id, art), list);
|
||||
@@ -443,6 +480,7 @@ module.exports = {
|
||||
listMods,
|
||||
addMod,
|
||||
removeMod,
|
||||
findDependents,
|
||||
setModEnabled,
|
||||
listInstances,
|
||||
getInstance,
|
||||
|
||||
+74
-2
@@ -32,6 +32,7 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
||||
:root[data-theme="dark"] .field select,
|
||||
:root[data-theme="dark"] .field textarea,
|
||||
:root[data-theme="dark"] .mods-search input,
|
||||
:root[data-theme="dark"] .path-row input,
|
||||
:root[data-theme="dark"] .login-usercode{background:#1e1e1e;color:var(--text);border-color:var(--border)}
|
||||
:root[data-theme="dark"] .modal-card{background:#2d2d30;border-color:#161618}
|
||||
:root[data-theme="dark"] .modal-head{background:#333337}
|
||||
@@ -67,7 +68,8 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
||||
:root[data-theme="dark"] #btn-console svg{color:#c8c8cc}
|
||||
:root[data-theme="dark"] .acct-av svg{color:#c8c8cc}
|
||||
:root[data-theme="dark"] .mods-filters select,
|
||||
:root[data-theme="dark"] .mods-search select{background:#1e1e1e;color:var(--text);border-color:var(--border)}
|
||||
:root[data-theme="dark"] .mods-search select,
|
||||
:root[data-theme="dark"] .mmc-bar select{background:#1e1e1e;color:var(--text);border-color:var(--border)}
|
||||
:root[data-theme="dark"] .upd-notes{background:#1e1e1e;color:var(--text)}
|
||||
:root[data-theme="dark"] .modal-close{color:#aaa}
|
||||
:root[data-theme="dark"] .modal-close:hover{color:#fff}
|
||||
@@ -141,6 +143,17 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
||||
.group-header.drop-hover,.group-section.drop-hover{outline:1px dashed var(--accent);outline-offset:1px}
|
||||
.card.dragging{opacity:.4}
|
||||
|
||||
/* Datei von außen übers Fenster gezogen (.jar/.mrpack/.zip) */
|
||||
body.file-drop-active::after{
|
||||
content:"Datei loslassen zum Importieren";
|
||||
position:fixed;inset:0;z-index:9999;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
background:rgba(0,0,0,.55);
|
||||
color:#fff;font-size:20px;font-weight:600;
|
||||
border:3px dashed var(--accent);
|
||||
pointer-events:none;
|
||||
}
|
||||
|
||||
/* ---------- Leerer Zustand ---------- */
|
||||
.empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;color:var(--dim);gap:8px}
|
||||
.empty h2{color:#333;margin:0}
|
||||
@@ -174,6 +187,38 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
||||
.modal-body{padding:16px;overflow:auto;background:#f6f6f6}
|
||||
.modal-foot{display:flex;justify-content:flex-end;gap:8px;padding:10px 14px;border-top:1px solid var(--border);background:#efefef}
|
||||
|
||||
/* Popup-Fenster (Einstellungen/Konsole): Haupt-UI ausblenden, das eine Modal
|
||||
füllt das ganze (rahmenlose) Fenster; der eigene Kopfbereich ersetzt die
|
||||
native Titelleiste – per -webkit-app-region per Maus verschiebbar */
|
||||
body.is-popout>header.toolbar,body.is-popout>main.workspace,body.is-popout>footer.statusbar{display:none}
|
||||
.popout-host{position:static;inset:auto;background:none;display:block}
|
||||
/* !important: schlägt auch die spezifischere #modal-settings-Breitenregel oben */
|
||||
.popout-host .modal-card{width:100%!important;max-width:none!important;height:100vh!important;max-height:none!important;border:none;border-radius:0;box-shadow:none;margin:0}
|
||||
/* Inhalt soll die restliche Fensterhöhe füllen (scrollen statt Leerraum unter dem Fuß) */
|
||||
.popout-host .modal-body{flex:1 1 auto;min-height:0}
|
||||
/* Konsole/Logs/Screenshots haben einen einzelnen scrollbaren Kasten mit
|
||||
fester Höhe (für den normalen Dialog gedacht) – im Popup-Fenster soll der
|
||||
stattdessen die volle verbleibende Höhe ausfüllen statt Leerraum darunter
|
||||
zu lassen */
|
||||
#modal-console.popout-host .modal-body,
|
||||
#modal-logs.popout-host .modal-body,
|
||||
#modal-screens.popout-host .modal-body,
|
||||
#modal-servers.popout-host .modal-body,
|
||||
#modal-mods.popout-host .modal-body,
|
||||
#modal-record.popout-host .modal-body{display:flex;flex-direction:column}
|
||||
.popout-host .console-out,
|
||||
#modal-logs.popout-host .crash-voll,
|
||||
.popout-host .screens-grid,
|
||||
.popout-host .mods-list,
|
||||
.popout-host .record-stage{flex:1 1 auto;height:auto;max-height:none;min-height:0}
|
||||
/* Mods-Fenster: die aktive Ansicht (Katalog/Installiert) muss selbst ein
|
||||
Flex-Container werden, damit ihre eigene Liste (.mods-list) die Regel oben
|
||||
nutzen und den restlichen Platz füllen kann */
|
||||
#modal-mods.popout-host #m-pane-catalog,
|
||||
#modal-mods.popout-host #m-pane-installed{display:flex;flex-direction:column;flex:1 1 auto;min-height:0}
|
||||
.popout-host .modal-head{-webkit-app-region:drag;cursor:default}
|
||||
.popout-host .modal-head button,.popout-host .modal-head input,.popout-host .modal-head select{-webkit-app-region:no-drag}
|
||||
|
||||
/* Formular */
|
||||
.field{display:flex;flex-direction:column;gap:5px;margin-bottom:12px}
|
||||
.field>span{font-size:12px;color:#333}
|
||||
@@ -185,7 +230,8 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
||||
.backup-select-field{margin-top:14px;margin-bottom:0}
|
||||
.backup-select-field select{width:100%}
|
||||
.hint{font-size:12px}
|
||||
.path-row{display:flex;gap:8px}.path-row input{flex:1}
|
||||
.path-row{display:flex;gap:8px}
|
||||
.path-row input{flex:1;padding:6px 8px;border:1px solid var(--border);border-radius:3px;background:#fff;color:var(--text);font-family:inherit;font-size:13px}
|
||||
.advanced{border:1px solid var(--border);border-radius:3px;padding:12px 14px 2px;margin-top:4px;background:#fbfbfb}
|
||||
.advanced legend{color:var(--dim);font-size:12px;padding:0 6px}
|
||||
.version-row{display:flex;gap:8px}.version-row #f-version-type{max-width:120px}.version-row #f-version{flex:1}
|
||||
@@ -275,6 +321,8 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
||||
/* Die gerenderte Figur soll das Feld ausfüllen; eine rohe 64x64-Textur wird
|
||||
ohne Weichzeichnen vergrößert, damit sie nicht verschwommen wirkt. */
|
||||
.skin-bild img{max-width:100%;max-height:100%;image-rendering:pixelated}
|
||||
.skin-bild canvas{max-width:100%;max-height:100%;cursor:grab}
|
||||
.skin-bild canvas:active{cursor:grabbing}
|
||||
.crash-details{margin-top:12px}
|
||||
.crash-details summary{cursor:pointer;font-size:12px;color:var(--dim)}
|
||||
.crash-voll{max-height:320px;overflow:auto;margin-top:8px;padding:10px;background:rgba(0,0,0,.25);border-radius:4px;font-family:var(--mono);font-size:11px;line-height:1.45;white-space:pre-wrap;word-break:break-word}
|
||||
@@ -288,6 +336,8 @@ body{display:grid;grid-template-rows:auto 1fr auto;height:100vh;overflow:hidden}
|
||||
|
||||
/* MultiMC-Import */
|
||||
.mmc-bar{display:flex;gap:18px;align-items:center;flex-wrap:wrap;margin-bottom:10px}
|
||||
.mmc-bar select{padding:6px 8px;border:1px solid var(--border);border-radius:3px;background:var(--input,#fff);color:var(--text);font-family:inherit;font-size:12px}
|
||||
.mmc-bar select:focus{outline:none;border-color:var(--accent)}
|
||||
.mmc-item{display:flex;gap:10px;align-items:center;padding:7px 8px;border-radius:3px}
|
||||
.mmc-item+.mmc-item{border-top:1px solid var(--border2)}
|
||||
.mmc-item:hover{background:rgba(128,128,128,.10)}
|
||||
@@ -329,6 +379,21 @@ img.account-av{object-fit:cover;image-rendering:auto}
|
||||
.screens-toolbar .hint{font-size:11px}
|
||||
.screens-empty{color:var(--dim);text-align:center;padding:30px;grid-column:1/-1}
|
||||
|
||||
/* Spielaufzeichnung */
|
||||
.record-search{display:flex;flex-direction:column;align-items:center;gap:10px;padding:48px 0;color:var(--dim)}
|
||||
.record-stage{display:flex;flex-direction:column;gap:10px}
|
||||
.record-video{width:100%;flex:1 1 auto;min-height:360px;background:#000;border-radius:4px;object-fit:contain}
|
||||
.record-toolbar{display:flex;flex-direction:column;align-items:flex-end;gap:6px}
|
||||
.record-live-badge{display:flex;align-items:center;gap:8px;color:var(--danger);font-size:13px;font-weight:600}
|
||||
#record-timer{font-variant-numeric:tabular-nums}
|
||||
.rec-dot{width:10px;height:10px;border-radius:50%;background:var(--danger);animation:rec-pulse 1s infinite}
|
||||
@keyframes rec-pulse{0%,100%{opacity:1}50%{opacity:.25}}
|
||||
.record-audio-bar{display:flex;align-items:center;gap:10px;padding:2px 2px}
|
||||
.record-mic-list{display:flex;flex-direction:column;gap:8px;margin-top:8px}
|
||||
.record-source-row{display:flex;align-items:center;gap:10px;margin-bottom:8px}
|
||||
.record-source-row .checkbox-field{margin-bottom:0;flex:1;min-width:0;font-size:13px;gap:6px}
|
||||
.record-vol{width:110px}
|
||||
|
||||
/* Konsole (dunkel wie ein Log) */
|
||||
.console-out{height:440px;overflow:auto;margin:0;background:#1e1e1e;border:1px solid var(--border);border-radius:3px;padding:10px;font-family:var(--mono);font-size:12px;color:#d4d4d4;white-space:pre-wrap;word-break:break-word;line-height:1.5}
|
||||
.log-line{white-space:pre-wrap}
|
||||
@@ -364,6 +429,7 @@ img.account-av{object-fit:cover;image-rendering:auto}
|
||||
.info-card{max-width:760px}
|
||||
.info-actions{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:18px 0 0;padding-top:14px;border-top:1px solid var(--border2)}
|
||||
.info-status{font-size:12px;color:var(--dim)}
|
||||
.modal-sep{border-top:1px solid var(--border2)}
|
||||
.info-meta{margin-top:10px}
|
||||
.info-meta .settings-kv-row{align-items:center}
|
||||
.info-meta .settings-kv-label{text-transform:uppercase;letter-spacing:.04em;font-size:11px}
|
||||
@@ -401,6 +467,12 @@ img.account-av{object-fit:cover;image-rendering:auto}
|
||||
.path-row{flex-direction:column}
|
||||
}
|
||||
|
||||
/* Popup-Fenster sind eigenständige, meist schmalere Fenster -- die
|
||||
Zeilenumbruch-Regel oben ist für die (deutlich breitere) Haupt-App gedacht
|
||||
und soll hier nicht greifen (sonst z. B. Version/Icon im Instanz-Fenster
|
||||
untereinander statt nebeneinander) */
|
||||
.is-popout .field-row{flex-direction:row}
|
||||
|
||||
/* Konto-Button mit 3D-Skin-Kopf */
|
||||
.acct-btn{gap:8px}
|
||||
.acct-av{display:inline-flex;align-items:center;justify-content:center}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* technicdiscover.js – Technic-Modpacks direkt über die Technic-Plattform
|
||||
* durchsuchen und installieren, ohne vorher manuell eine Datei herunterladen
|
||||
* zu müssen. Genutzt wird dieselbe öffentliche API wie bei Prism Launcher:
|
||||
*
|
||||
* Suche: GET https://api.technicpack.net/search?build=multimc&q=<term>
|
||||
* Beliebt: GET https://api.technicpack.net/trending?build=multimc
|
||||
* Pack-Infos: GET https://api.technicpack.net/modpack/<slug>?build=multimc
|
||||
* -> entweder "url" (ein fertiges ZIP) oder "solder" (Solder-Server)
|
||||
* Solder: GET <solder>modpack/<slug> – Liste der Builds
|
||||
* GET <solder>modpack/<slug>/<build> – Mods dieser Fassung
|
||||
* { minecraft, forge, mods: [{ name, url, ... }] }
|
||||
*
|
||||
* "build=multimc" ist die Kennung, mit der sich auch andere Drittlauncher
|
||||
* bei der Technic-Plattform melden – kein echter Minecraft-Build, sondern
|
||||
* nur ein Client-Identifier, den die API erwartet.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const forge = require('./forge');
|
||||
const technicimport = require('./technicimport');
|
||||
|
||||
const API_BASE = 'https://api.technicpack.net/';
|
||||
const API_BUILD = 'multimc';
|
||||
const UA = 'AeroMC-Launcher/1.0';
|
||||
|
||||
async function getJson(url) {
|
||||
const res = await fetch(url, { headers: { 'User-Agent': UA, Accept: 'application/json' } });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function getBuffer(url) {
|
||||
const res = await fetch(url, { headers: { 'User-Agent': UA } });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
return Buffer.from(await res.arrayBuffer());
|
||||
}
|
||||
|
||||
/*
|
||||
* query leer -> beliebte Packs (trending), sonst Suchtreffer.
|
||||
* -> [{ slug, name, iconUrl }]
|
||||
*/
|
||||
async function search(query) {
|
||||
const q = String(query || '').trim();
|
||||
const url = q
|
||||
? `${API_BASE}search?build=${API_BUILD}&q=${encodeURIComponent(q)}`
|
||||
: `${API_BASE}trending?build=${API_BUILD}`;
|
||||
const data = await getJson(url);
|
||||
const packs = Array.isArray(data.modpacks) ? data.modpacks : [];
|
||||
return packs
|
||||
.filter((p) => p && p.slug && p.slug !== 'vanilla')
|
||||
.map((p) => ({ slug: p.slug, name: p.name || p.slug, iconUrl: p.iconUrl || '' }));
|
||||
}
|
||||
|
||||
/*
|
||||
* Entpackt einen ZIP-Buffer nach destDir.
|
||||
* Enthält der ZIP-Inhalt bereits eine erkennbare Ordnerstruktur (mods/,
|
||||
* config/, …), wird sie 1:1 übernommen. Liegen nur einzelne Dateien direkt
|
||||
* im ZIP-Wurzelverzeichnis (typisch für ältere, einzeln gepackte Solder-
|
||||
* Mods), landen sie stattdessen im mods-Ordner.
|
||||
*/
|
||||
function extractZipBuffer(buf, destDir) {
|
||||
const idx = forge.readZipIndex(buf);
|
||||
const namen = Object.keys(idx);
|
||||
const hatOrdnerStruktur = namen.some((n) => /^(mods|config|resources|resourcepacks|scripts|structures|bin)\//i.test(n));
|
||||
for (const name of namen) {
|
||||
if (name.endsWith('/')) continue; // reines Verzeichnis – wird beim Schreiben automatisch angelegt
|
||||
const relPfad = hatOrdnerStruktur ? name : path.posix.join('mods', name);
|
||||
const zielPfad = path.join(destDir, ...relPfad.split('/'));
|
||||
if (!zielPfad.startsWith(destDir + path.sep)) continue; // Pfad-Ausbruch verhindern
|
||||
fs.mkdirSync(path.dirname(zielPfad), { recursive: true });
|
||||
fs.writeFileSync(zielPfad, forge.readZipEntry(buf, idx[name]));
|
||||
}
|
||||
}
|
||||
|
||||
// Solder-Basis-URLs enden mal mit "/api/", mal ohne abschließenden Slash
|
||||
function solderUrl(basis, pfad) {
|
||||
const b = String(basis || '').replace(/\/+$/, '');
|
||||
return `${b}/${pfad}`;
|
||||
}
|
||||
|
||||
async function installSolderBuild(solderBasis, slug, gameDir, onProgress) {
|
||||
const info = await getJson(solderUrl(solderBasis, 'modpack/' + encodeURIComponent(slug)));
|
||||
const build = info.recommended || info.latest || (info.builds || [])[0];
|
||||
if (!build) throw new Error('Keine Fassung (Build) für dieses Modpack gefunden.');
|
||||
const daten = await getJson(solderUrl(solderBasis, `modpack/${encodeURIComponent(slug)}/${encodeURIComponent(build)}`));
|
||||
const mods = Array.isArray(daten.mods) ? daten.mods : [];
|
||||
|
||||
for (let i = 0; i < mods.length; i++) {
|
||||
const mod = mods[i];
|
||||
if (onProgress) onProgress({ current: i + 1, total: mods.length, name: mod.name || '' });
|
||||
if (!mod || !mod.url) continue;
|
||||
try {
|
||||
const buf = await getBuffer(mod.url);
|
||||
extractZipBuffer(buf, gameDir);
|
||||
} catch { /* einzelner Mod fehlgeschlagen – Rest trotzdem weiter versuchen */ }
|
||||
}
|
||||
|
||||
let loader = 'vanilla';
|
||||
const loaderVersion = daten.forge || '';
|
||||
if (loaderVersion) loader = 'forge';
|
||||
return { version: daten.minecraft || '', loader, loaderVersion };
|
||||
}
|
||||
|
||||
async function installSingleZip(url, gameDir) {
|
||||
const buf = await getBuffer(url);
|
||||
// in einen Temp-Ordner entpacken, damit wir wie beim Ordner-Import
|
||||
// (technicimport.js) Version/Loader aus bin/modpack.jar erkennen können,
|
||||
// bevor die Dateien an ihren endgültigen Platz kommen
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'technic-dl-'));
|
||||
try {
|
||||
extractZipBuffer(buf, tmp);
|
||||
let base = tmp;
|
||||
if (!fs.existsSync(path.join(base, 'bin')) && !fs.existsSync(path.join(base, 'mods'))) {
|
||||
// manche Packs haben einen umschließenden Ordner im ZIP
|
||||
const sub = fs.readdirSync(tmp).map((n) => path.join(tmp, n))
|
||||
.find((p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } });
|
||||
if (sub) base = sub;
|
||||
}
|
||||
const { version, loader, loaderVersion } = technicimport.detectVersion(base);
|
||||
fs.cpSync(base, gameDir, { recursive: true });
|
||||
return { version, loader, loaderVersion };
|
||||
} finally {
|
||||
try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { /* egal */ }
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Lädt ein Modpack von der Technic-Plattform und legt daraus eine neue
|
||||
* Instanz an. onProgress(info) wird bei Solder-Packs pro Mod aufgerufen.
|
||||
* -> { ok, instance } oder { ok: false, message }
|
||||
*/
|
||||
async function installFromPlatform(store, slug, onProgress) {
|
||||
let created = null;
|
||||
try {
|
||||
const detail = await getJson(`${API_BASE}modpack/${encodeURIComponent(slug)}?build=${API_BUILD}`);
|
||||
// die Technic-API antwortet bei unbekanntem Slug mit HTTP 200 und
|
||||
// {"error": "..."} statt einem Fehlerstatus – daher explizit prüfen
|
||||
if (detail.error) throw new Error(detail.error);
|
||||
|
||||
created = store.createOrReplaceImportedInstance({
|
||||
name: detail.displayName || detail.name || slug,
|
||||
notes: detail.description || '',
|
||||
});
|
||||
const gameDir = store.gameDir(created.id);
|
||||
fs.mkdirSync(gameDir, { recursive: true });
|
||||
|
||||
let ergebnis;
|
||||
if (detail.solder) {
|
||||
ergebnis = await installSolderBuild(detail.solder, slug, gameDir, onProgress);
|
||||
} else if (detail.url) {
|
||||
ergebnis = await installSingleZip(detail.url, gameDir);
|
||||
} else {
|
||||
throw new Error('Für dieses Modpack ist kein Download hinterlegt.');
|
||||
}
|
||||
store.updateInstance(created.id, {
|
||||
minecraft: {
|
||||
version: ergebnis.version || detail.minecraft || '',
|
||||
loader: ergebnis.loader || 'vanilla',
|
||||
loaderVersion: ergebnis.loaderVersion || '',
|
||||
},
|
||||
});
|
||||
return { ok: true, instance: store.getInstance(created.id) };
|
||||
} catch (err) {
|
||||
// fehlgeschlagene Installation nicht als leere Karteileiche stehen lassen
|
||||
if (created) { try { store.deleteInstance(created.id); } catch { /* egal */ } }
|
||||
return { ok: false, message: 'Installation fehlgeschlagen: ' + err.message };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { search, installFromPlatform };
|
||||
@@ -0,0 +1,172 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* technicimport.js – Instanzen aus dem Technic Launcher übernehmen
|
||||
* -------------------------------------------------------------------
|
||||
* Der Technic Launcher legt jedes installierte Modpack als eigenen Ordner
|
||||
* unter "<Technic-Wurzel>/modpacks/<name>" an (Standard unter Windows:
|
||||
* %APPDATA%\.technic\modpacks). Darin liegen "bin" (Launcher-interner
|
||||
* Jar-/Versions-Cache), "config" und "mods" als Geschwisterordner – Welten,
|
||||
* Ressourcenpakete usw. landen ebenfalls direkt im Pack-Ordner, genau wie
|
||||
* bei einem klassischen .minecraft.
|
||||
*
|
||||
* Es gibt keine für uns nutzbare, stabil dokumentierte JSON-Datei mit einem
|
||||
* schönen Anzeigenamen – "installedPacks" im Wurzelordner listet Packs nur
|
||||
* über ihren Ordnernamen (Slug). Wir lesen sie deshalb nur als optionale,
|
||||
* fehlertolerante Ergänzung; die eigentliche Erkennung läuft – wie bei den
|
||||
* anderen Importern – über die tatsächlich vorhandenen Ordner/Dateien.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const forge = require('./forge');
|
||||
|
||||
// Ordner, die zum Technic-internen Verwaltungs-/Jar-Cache gehören und beim
|
||||
// Übernehmen NICHT mit ins Spielverzeichnis der neuen Instanz sollen.
|
||||
const SKIP_DIRS = new Set(['bin']);
|
||||
|
||||
function technicRoot() {
|
||||
return path.join(process.env.APPDATA || '', '.technic');
|
||||
}
|
||||
|
||||
function readJson(file) {
|
||||
try { return JSON.parse(fs.readFileSync(file, 'utf8').replace(/^/, '')); }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
// findet den modpacks-Ordner, egal ob Technic-Wurzel, modpacks-Ordner direkt
|
||||
// oder ein einzelner Pack-Ordner angegeben wurde
|
||||
function resolveModpacksDir(inputPath) {
|
||||
const root = inputPath && inputPath.trim() ? inputPath.trim() : technicRoot();
|
||||
if (!fs.existsSync(root)) return null;
|
||||
|
||||
const direct = path.join(root, 'modpacks');
|
||||
if (fs.existsSync(direct)) return direct;
|
||||
if (path.basename(root).toLowerCase() === 'modpacks') return root;
|
||||
return root; // z. B. ein selbst gewählter Ordner mit Pack-Unterordnern
|
||||
}
|
||||
|
||||
// ein Pack-Ordner hat i. d. R. bin/, config/ und/oder mods/ als Geschwister
|
||||
function looksLikePack(dir) {
|
||||
const markers = ['bin', 'mods', 'config', 'saves', 'resourcepacks', 'options.txt'];
|
||||
return markers.some((name) => fs.existsSync(path.join(dir, name)));
|
||||
}
|
||||
|
||||
// optionale Anreicherung aus der "installedPacks"-Datei der Technic-Wurzel;
|
||||
// bewusst rein additiv und fehlertolerant, da das Format nicht offiziell
|
||||
// als stabile API dokumentiert ist
|
||||
function readInstalledPacksMeta(root) {
|
||||
const data = readJson(path.join(root, 'installedPacks'));
|
||||
const packs = data && data.installedPacks;
|
||||
return packs && typeof packs === 'object' ? packs : {};
|
||||
}
|
||||
|
||||
// versucht, aus bin/modpack.jar (version.json) oder der alten bin/version
|
||||
// -Textdatei Minecraft-Version und Mod-Loader zu bestimmen. Rein
|
||||
// bestmöglich – schlägt die Erkennung fehl, bleibt die Instanz einfach
|
||||
// ohne Version/Loader stehen und lässt sich manuell nachtragen.
|
||||
function detectVersion(packDir) {
|
||||
const bin = path.join(packDir, 'bin');
|
||||
const jarFile = path.join(bin, 'modpack.jar');
|
||||
if (fs.existsSync(jarFile)) {
|
||||
try {
|
||||
const buf = fs.readFileSync(jarFile);
|
||||
const idx = forge.readZipIndex(buf);
|
||||
const entry = idx['version.json'];
|
||||
if (entry) {
|
||||
// manche Jars speichern JSON mit BOM – ohne Entfernen bricht JSON.parse
|
||||
const text = forge.readZipEntry(buf, entry).toString('utf8').replace(/^/, '');
|
||||
const profile = JSON.parse(text);
|
||||
const libs = Array.isArray(profile.libraries) ? profile.libraries : [];
|
||||
let loader = 'vanilla';
|
||||
let loaderVersion = '';
|
||||
for (const lib of libs) {
|
||||
const name = String((lib && lib.name) || '');
|
||||
if (/net\.minecraftforge:forge:/i.test(name)) {
|
||||
loader = 'forge';
|
||||
loaderVersion = (name.split(':')[2] || '').split('-').pop() || '';
|
||||
} else if (/net\.neoforged:neoforge:/i.test(name) && loader === 'vanilla') {
|
||||
loader = 'neoforge';
|
||||
loaderVersion = name.split(':')[2] || '';
|
||||
}
|
||||
}
|
||||
const id = String(profile.id || profile.inheritsFrom || '');
|
||||
const version = String(profile.inheritsFrom || id.split('-')[0] || '').trim();
|
||||
if (version) return { version, loader, loaderVersion };
|
||||
}
|
||||
} catch { /* modpack.jar nicht lesbar/kein gültiges ZIP – weiter unten Fallback */ }
|
||||
}
|
||||
// ältere Packs hinterlegen nur eine reine Textdatei mit der MC-Version
|
||||
const legacy = path.join(bin, 'version');
|
||||
try {
|
||||
if (fs.existsSync(legacy)) {
|
||||
const version = fs.readFileSync(legacy, 'utf8').trim();
|
||||
if (version) return { version, loader: 'vanilla', loaderVersion: '' };
|
||||
}
|
||||
} catch { /* egal, bleibt leer */ }
|
||||
return { version: '', loader: 'vanilla', loaderVersion: '' };
|
||||
}
|
||||
|
||||
function scan(inputPath) {
|
||||
const modpacksDir = resolveModpacksDir(inputPath);
|
||||
if (!modpacksDir || !fs.existsSync(modpacksDir)) return { ok: false, reason: 'not-found' };
|
||||
|
||||
const root = path.basename(modpacksDir).toLowerCase() === 'modpacks'
|
||||
? path.dirname(modpacksDir) : modpacksDir;
|
||||
const meta = readInstalledPacksMeta(root);
|
||||
|
||||
const list = [];
|
||||
for (const entry of fs.readdirSync(modpacksDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const dir = path.join(modpacksDir, entry.name);
|
||||
if (!looksLikePack(dir)) continue;
|
||||
|
||||
const { version, loader, loaderVersion } = detectVersion(dir);
|
||||
const info = meta[entry.name] || {};
|
||||
list.push({
|
||||
folder: entry.name,
|
||||
dir,
|
||||
name: info.displayName || entry.name,
|
||||
group: '',
|
||||
notes: '',
|
||||
version,
|
||||
loader,
|
||||
loaderVersion,
|
||||
gameDir: dir,
|
||||
hasGameData: true,
|
||||
});
|
||||
}
|
||||
list.sort((a, b) => a.name.localeCompare(b.name));
|
||||
return { ok: true, instancesDir: modpacksDir, count: list.length, instances: list };
|
||||
}
|
||||
|
||||
// kopiert den Pack-Ordner, lässt aber den Technic-internen bin-Cache weg
|
||||
function copyInto(sourceDir, targetDir) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
for (const name of fs.readdirSync(sourceDir)) {
|
||||
if (SKIP_DIRS.has(name)) continue;
|
||||
fs.cpSync(path.join(sourceDir, name), path.join(targetDir, name), { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function importOne(store, entry, copyData = true) {
|
||||
const created = store.createOrReplaceImportedInstance({
|
||||
name: entry.name,
|
||||
group: entry.group,
|
||||
notes: entry.notes,
|
||||
minecraft: {
|
||||
version: entry.version,
|
||||
loader: entry.loader,
|
||||
loaderVersion: entry.loaderVersion,
|
||||
},
|
||||
});
|
||||
|
||||
let copied = false;
|
||||
if (copyData && entry.gameDir && fs.existsSync(entry.gameDir)) {
|
||||
copyInto(entry.gameDir, store.gameDir(created.id));
|
||||
copied = true;
|
||||
}
|
||||
return { id: created.id, name: created.name, copied };
|
||||
}
|
||||
|
||||
module.exports = { scan, importOne, resolveModpacksDir, technicRoot, detectVersion };
|
||||
+11
-2
@@ -230,8 +230,17 @@ async function downloadAndInstall(asset, onProgress) {
|
||||
|
||||
const dir = updateDir();
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
const dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_'));
|
||||
fs.rmSync(dest, { force: true });
|
||||
let dest = path.join(dir, asset.name.replace(/[^\w.\-]/g, '_'));
|
||||
try {
|
||||
fs.rmSync(dest, { force: true });
|
||||
} catch {
|
||||
// Alte Datei ist gesperrt (Virenscanner prüft sie gerade, oder ein vorheriger
|
||||
// Installer-Versuch läuft noch) - dann eben unter neuem Namen laden, statt mit
|
||||
// EPERM abzubrechen. Der Rest wird bei nächster Gelegenheit von
|
||||
// cleanupUpdateCache() aufgeräumt (ab 10 Minuten Alter).
|
||||
const parsed = path.parse(dest);
|
||||
dest = path.join(parsed.dir, `${parsed.name}-${Date.now()}${parsed.ext}`);
|
||||
}
|
||||
|
||||
const res = await fetch(asset.url, { headers: { 'User-Agent': UA } });
|
||||
if (!res.ok) throw new Error('Download fehlgeschlagen: HTTP ' + res.status);
|
||||
|
||||
Reference in New Issue
Block a user