Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
009038d48d | ||
|
|
3a7822302f | ||
|
|
aae442ae48 | ||
|
|
5611c5883a | ||
|
|
d976cb4595 | ||
|
|
3ac5a6c14d | ||
|
|
7e7a2021ed | ||
|
|
7e5d743367 | ||
|
|
6ef51edede | ||
|
|
93c74f94a1 | ||
|
|
384354f44e | ||
|
|
9afe91a592 | ||
|
|
de81575bcf | ||
|
|
76762ab567 | ||
|
|
6a0e92eb0f | ||
|
|
7e1fb616aa | ||
|
|
642d229d7e | ||
|
|
e782cc3058 | ||
|
|
972a3e73da | ||
|
|
e83c951256 | ||
|
|
67fe4cba48 | ||
|
|
e5ccf0b4cc | ||
|
|
10ee45cbc2 | ||
|
|
bc68395437 | ||
|
|
41c63ef0ed | ||
|
|
0595b7ab1a | ||
|
|
cbe7a6d81d | ||
|
|
75be217546 | ||
|
|
78246284d6 | ||
|
|
1d432d08ae | ||
|
|
47b8b88f6e | ||
|
|
906ac75443 | ||
|
|
d268e548f5 | ||
|
|
ee0e6dec32 | ||
|
|
3b54217989 |
@@ -0,0 +1,240 @@
|
||||
# Changelog
|
||||
|
||||
Alle nennenswerten Änderungen an AeroMC werden hier festgehalten.
|
||||
|
||||
## [1.2.1] - 2026-10-05
|
||||
|
||||
### Neu
|
||||
- Garderobe im Skin-Fenster: Skins (samt Armform) und eigene AeroMC-Umhänge speichern und mit einem Klick wieder anziehen – vor jedem Skinwechsel im Launcher wird der bisherige Skin automatisch gesichert, denn Mojang merkt sich keine alten Skins
|
||||
- Erfolge: 17 Erfolge (z. B. Spielzeit, Starts, Instanzen, Mods, Modpack, erster Freund, eigener Umhang, Skin aus dem Editor) mit „Erfolg erzielt!“-Meldung passend zum Design (im Minecraft-Design wie im Spiel); ein Klick auf die Meldung öffnet die Übersicht
|
||||
- Freunde mit 3D-Figur: Neben der Freundesliste dreht sich groß die Figur des ausgewählten Freundes mit Skin, 2. Ebene und Umhang (online läuft sie, offline steht sie) – das Freunde-Fenster ist dafür breiter, schmal gezogen steht die Figur über der Liste; die Köpfe in der Liste zeigen ebenfalls die 2. Ebene
|
||||
- Leistungs-Paket im Mod-Dialog: installiert mit einem Klick passende Performance-Mods – Sodium (bzw. Embeddium), Lithium (bzw. Radium), FerriteCore, Entity Culling, ImmediatelyFast, ModernFix und Dynamic FPS; Vorhandenes und für die Version nicht Verfügbares wird übersprungen, mit OptiFine bleibt Sodium weg
|
||||
- Starten-Knopf mit Fortschritt: füllt sich während Download und Vorbereitung und zeigt die Phase (z. B. „Assets 57 %“), danach kurz „Läuft ✓“
|
||||
- Mehr Leben in der Oberfläche (alles über „Bewegte Oberfläche“ abschaltbar):
|
||||
- Laufende Instanzen haben einen pulsierenden Rand – man sieht sofort, was gerade gespielt wird
|
||||
- Instanzen löschen, verschieben oder Gruppen auf-/zuklappen: Karten schrumpfen weg bzw. blenden ein, die übrigen rutschen weich nach
|
||||
- Skin übernommen: Die Figur dreht sich einmal um sich selbst; nach einem Umhang-Wechsel weht der Umhang kurz auf
|
||||
- Konto gewechselt: Der Spielerkopf oben rechts dreht sich wie eine Münze zum neuen Konto
|
||||
- Erfolg erzielt: Funken um die Meldung, im Minecraft-Design grüne XP-Kugeln
|
||||
- Freund kommt online: Sein Kopf hüpft in der Freundesliste, der grüne Punkt pulsiert
|
||||
- AeroMC/Nachtlila: dezent treibende Lichtpunkte im Instanzbereich
|
||||
- Minecraft-Design: Wolken ziehen durch die Werkzeugleiste, und beim Download zeigt das Block-Symbol der Instanz Abbau-Risse als Fortschritt
|
||||
- Das Zahnrad dreht sich beim Darüberfahren, das Block-Symbol einer Instanz kippt leicht zur Maus
|
||||
- Armform-Prüfung im Skin-Fenster: Passt die eingestellte Armform nicht zur Skin-Datei (gequetschte Arme oder dunkle Streifen), zeigt AeroMC einen Hinweis und stellt sie mit einem Klick richtig
|
||||
- Absturz-Assistent erkennt zu alte Forge-Versionen, die mit aktuellem Java 8 abstürzen („ManifestEntryVerifier“), und aktualisiert Forge auf Wunsch auf die neueste Version für dieselbe Minecraft-Version
|
||||
|
||||
### Geändert
|
||||
- Statistik-Fenster mit zwei Reitern: „Spielzeit“ und „Erfolge (x/17)“
|
||||
- Update-Meldung zeigt die Änderungshinweise formatiert an (Überschriften, Listen, fett/kursiv, Code, Links) statt als rohen Markdown-Text; das Fenster ist dafür größer, Links öffnen im Browser
|
||||
- Einstellungen aufgeräumt: jede Option als eigene Zeile mit kurzer Erklärung und Schalter statt Häkchen, alles einspaltig statt verstreuter Kacheln; neu sortiert in Allgemein, Darstellung, Spiel (inkl. Konsole), Java & Erweitert, Freunde & Umhänge (neu), Netzwerk, Konten, Backup (Welt-Sicherung jetzt bei „Automatisch sichern“)
|
||||
|
||||
### Behoben
|
||||
- Alte Forge-Versionen vor Minecraft 1.13 (z. B. 1.12.2) starteten nicht („Hauptklasse net.minecraft.launchwrapper.Launch konnte nicht gefunden werden“) – Klassenpfad und Startargumente fehlten
|
||||
- Bei Forge und NeoForge wurde ohne festgelegte Version teils die älteste statt der neuesten Version gewählt – die Versionsliste ist jetzt richtig sortiert
|
||||
- Der Mod-Katalog zeigte bei von Hand kopierten oder importierten Mods „Installieren“, obwohl sie schon da waren – AeroMC erkennt sie jetzt per Prüfsumme über Modrinth
|
||||
- Der ZIP-Import (AeroMC-Export oder gezippte MultiMC/Prism-Instanz) blockierte bei großen Archiven den Launcher – die Dateien werden jetzt im Hintergrund verschoben bzw. kopiert
|
||||
|
||||
## [1.2.0] - 2026-10-04
|
||||
|
||||
### Neu
|
||||
- Untergruppen (optional): im Feld „Gruppe“ mit „/“ trennen, z. B. „Modpacks/Survival“ – auch mehrere Ebenen tief. Untergruppen erscheinen eingerückt unter ihrer Gruppe, lassen sich einzeln einklappen und per Drag & Drop befüllen; die Anzahl an einer Gruppe zählt ihre Untergruppen mit
|
||||
- Importe aus anderen Launchern zeigen jetzt genauen Fortschritt: Instanz x von y, übertragene GB, Prozent, Tempo, Restzeit und die gerade kopierte Datei
|
||||
- Import abbrechen: fertig übernommene Instanzen bleiben erhalten, die gerade halb kopierte wird wieder entfernt
|
||||
- Vor dem Kopieren prüft AeroMC, ob genug Speicherplatz frei ist, und nennt sonst benötigte und freie GB
|
||||
- Konto-Menü oben rechts (wie in MultiMC): Ein Klick auf den Kontonamen klappt alle Konten mit Spielerkopf auf – das aktive ist markiert, ein Klick wechselt sofort. Dazu „Konto hinzufügen…“, „Skin ändern…“ und „Konten verwalten…“
|
||||
|
||||
### Geändert
|
||||
- Plugin „AeroMC Freunde“ 1.5.1: merkt sich den von Mojang bestätigten Namen schon bei der Anmeldung und beim Senden einer Anfrage – Spieler sind damit per Name auffindbar, bevor sie zum ersten Mal über AeroMC gespielt haben (Server-Update empfohlen, der Launcher funktioniert auch mit 1.5.0)
|
||||
|
||||
### Behoben
|
||||
- Freundschaftsanfragen zeigten statt des Namens nur eine lange ID (UUID), wenn der Absender noch nie über AeroMC gespielt hatte – fehlende Namen holt der Launcher jetzt direkt bei Mojang (auch in der Freundesliste und bei gesendeten Anfragen)
|
||||
- Große Importe (viele GB, z. B. 60 GB aus MultiMC/Prism, CurseForge, Modrinth App, Minecraft Launcher oder Technic) froren den Launcher komplett ein – kopiert wird jetzt im Hintergrund, der Launcher bleibt bedienbar
|
||||
- Einzelne gesperrte Dateien (z. B. eine gerade geöffnete Welt) brechen den Import nicht mehr ab, sie werden übersprungen und gemeldet
|
||||
- „Instanz kopieren“, „Löschen“ und das Ersetzen einer vorhandenen Instanz beim erneuten Import blockierten bei großen Instanzen den Launcher – das läuft jetzt im Hintergrund
|
||||
- Der Spielerkopf oben neben dem Kontonamen und in der Kontoliste zeigte die 2. Skin-Ebene (Mütze, Haare, Kragen, Ärmel) nicht sichtbar – er wird jetzt selbst in 3D gezeichnet, die 2. Ebene steht wie im Spiel plastisch ab
|
||||
|
||||
## [1.1.9] - 2026-10-04
|
||||
|
||||
### Neu
|
||||
- Startanimation: Beim Öffnen dreht sich das AeroMC-Logo mit Schriftzug, danach baut sich der Launcher von der Mitte nach außen auf – Schriftzug passend zum Design (Minecraft mit Pixel-Schrift), nicht beim Start im Infobereich
|
||||
- Lebendige Skin-Figur im Hintergrund: atmet, wiegt sich leicht, der Umhang schwingt, und der Kopf folgt dem Mauszeiger – pausiert, solange der Launcher minimiert oder im Hintergrund ist
|
||||
- Fenster wie Optionen, Mods, Skin und Freunde wachsen beim Öffnen aus dem angeklickten Knopf heraus
|
||||
- Instanzen blenden beim Öffnen des Launchers nacheinander ein; beim Darüberfahren hebt sich das Block-Symbol und dreht sich ein Stück
|
||||
- Designwechsel blenden weich über statt hart umzuschalten
|
||||
- Optionen → Darstellung → Animationen: „Startanimation“ und „Bewegte Oberfläche“ einzeln abschaltbar; ist in Windows „Animationen anzeigen“ aus, bleibt die Oberfläche ruhig
|
||||
|
||||
## [1.1.8] - 2026-10-04
|
||||
|
||||
### Behoben
|
||||
- Das Skin-Fenster und „Aktuellen Skin laden“ im Skin-Editor zeigten bei manchen Konten einen Standard-Skin (z. B. Efe) statt des eigenen Skins – angezeigt wird jetzt der Skin, den andere Spieler im Spiel sehen; direkt nach einer eigenen Skin-Änderung noch kurz der neue Stand aus dem Konto, bis Mojang ihn überall verteilt hat
|
||||
|
||||
## [1.1.7] - 2026-10-03
|
||||
|
||||
### Neu
|
||||
- Umhänge im Skin-Fenster: offiziellen Mojang-Umhang auswählen oder ablegen (Liste zeigt alle Umhänge, die das Konto besitzt) und den eigenen AeroMC-Umhang verwalten – Figur und Skin-Hintergrund zeigen den Umhang mit
|
||||
- Skin-Editor, neuer Modus „Umhang“: eigene Umhänge gestalten, auch in HD bis 1024×512 – mit allen Werkzeugen, Pinselgröße, Spiegeln (linke ↔ rechte Kante), Zoom und Verschieben, Rückgängig auch über Größenwechsel
|
||||
- Umhang auf der 3D-Figur bemalen, wahlweise als Elytra ansehen, beliebiges Bild (PNG, JPG …) mit „Bild einsetzen…“ auf die Außenseite setzen, aktuellen Umhang laden, als PNG speichern oder mit „Als Umhang übernehmen“ hochladen
|
||||
- AeroMC-Umhänge im Spiel: in jeder Minecraft-Version und mit jedem Loader (Vanilla, Fabric, Quilt, Forge, NeoForge) ohne zusätzliche Mod sichtbar – für einen selbst und für andere AeroMC-Spieler; abschaltbar unter Optionen → Spiel → Umhänge
|
||||
- Mod „AeroMC Umhänge“ für Fabric und Quilt: damit sehen auch Spieler mit anderen Launchern (offizieller Launcher, MultiMC, Prism, Modrinth App …) die AeroMC-Umhänge – ein Jar für Minecraft 1.14 bis 26.x, nur auf dem Client nötig
|
||||
|
||||
### Geändert
|
||||
- Suche in der Werkzeugleiste: statt des dauerhaft sichtbaren Suchfelds gibt es eine Lupe – ein Klick oder Strg+F klappt das Feld auf, Esc leert und schließt es, mit Suchbegriff bleibt es offen
|
||||
- Benötigt für eigene Umhänge das Plugin „AeroMC Freunde“ 1.5.0 auf dem Server
|
||||
|
||||
## [1.1.6] - 2026-10-03
|
||||
|
||||
### Neu
|
||||
- Skin-Editor: Skins direkt im Launcher pixeln – erreichbar im Skin-Fenster über „Im Editor bearbeiten…“
|
||||
- Zwei Schichten: Grundschicht und 2. Schicht getrennt bemalen – die 2. Schicht liegt wie eine Hülle über der Figur und lässt Haare, Kapuzen oder Ärmel plastisch wirken
|
||||
- Wahlweise auf der aufgeklappten 2D-Vorlage oder direkt auf der drehbaren 3D-Figur malen – die andere Ansicht läuft als Live-Vorschau mit und ist ebenfalls bemalbar
|
||||
- Werkzeuge: Stift, Radierer (nur 2. Schicht), Pipette (auch Alt+Klick), Füllen, Farbwähler mit den zuletzt benutzten Farben
|
||||
- Rückgängig/Wiederholen mit Strg+Z und Strg+Y
|
||||
- Spiegeln: malt gleichzeitig auf der anderen Körperseite (linker ↔ rechter Arm/Bein)
|
||||
- Ebenen-Werkzeuge: 2. Schicht ein-/ausblenden, Grundschicht durchscheinen lassen, 2. Schicht leeren
|
||||
- Aktuellen Skin laden, PNG öffnen oder speichern und das Ergebnis mit einem Klick als Skin übernehmen – klassische und schmale Arme, alte 64×32-Skins werden automatisch umgewandelt
|
||||
- Skin-Hintergrund: Armform wählbar (Optionen → Darstellung) – „Automatisch“ übernimmt die Armform des Kontos, alternativ fest „Normal“ oder „Schmal“
|
||||
- Designs zur Auswahl (Optionen → Darstellung → Design, mit Vorschau-Kacheln): „Klassisch“ hell oder dunkel wie bisher, „AeroMC“ im Look der Webseite (Nachtblau, Cyan-Verläufe, runde Knöpfe), „Nachtlila“ (wie AeroMC, nur in dunklem Lila) und „Minecraft“ mit Block-Optik, Stein-Knöpfen, grünem Starten-Knopf und eigener Pixel-Schrift
|
||||
- Ein Klick auf eine Kachel zeigt das Design sofort zur Probe, nach dem Speichern gilt es in allen offenen Fenstern
|
||||
- Eigene Titelleiste im Design statt der weißen Windows-Leiste – Minimieren, Maximieren und Schließen bleiben echte Windows-Knöpfe (Andocken, Fensterlayouts) und werden passend eingefärbt
|
||||
- Import aus der Modrinth App (Importieren → „Modrinth App“, auch im Installer): übernimmt Name, Gruppe, Symbol, Minecraft-Version, Loader, Arbeitsspeicher, Java-Einstellungen, Spielzeit sowie Mods, Welten und Einstellungen – auch bei verlegtem Datenordner und während die Modrinth App läuft
|
||||
- Aus der Modrinth App übernommene Modrinth-Modpacks bleiben mit ihrem Paket verknüpft und lassen sich in AeroMC aktualisieren
|
||||
|
||||
### Geändert
|
||||
- Die Design-Auswahl (bisher „Design: Dunkel/Hell“ unter Optionen → Allgemein) steht jetzt unter Optionen → Darstellung
|
||||
|
||||
### Behoben
|
||||
- Das Symbol für „Optionen“ sah aus wie eine Sonne – jetzt ein Zahnrad
|
||||
- Der Skin-Hintergrund zeigte schmale (Alex-)Skins mit breiten Armen – dadurch erschienen falsche Streifen an den Armen
|
||||
|
||||
## [1.1.5] - 2026-10-02
|
||||
|
||||
### Neu
|
||||
- Instanz teilen per Code: „Instanz teilen“ in der Seitenleiste erzeugt einen Code (30 Tage gültig) – Freunde lösen ihn unter Importieren → „Per Code“ ein und bekommen Minecraft-Version, Loader sowie alle Mods, Ressourcenpakete und Shader in denselben Versionen
|
||||
- Absturz-Assistent: erklärt die Ursache eines Absturzes in einfachen Worten – fehlende Abhängigkeiten, Mods für eine andere Minecraft-Version, unverträgliche oder doppelte Mods, falsches Java, zu viel oder zu wenig Arbeitsspeicher, Grafiktreiber – und behebt sie auf Wunsch mit einem Klick
|
||||
- Der Absturz-Assistent meldet sich auch, wenn Minecraft schon beim Start scheitert und keinen Absturzbericht schreibt
|
||||
- Nach einer Lösung lässt sich die Instanz direkt aus dem Absturz-Fenster erneut starten
|
||||
- Fehlerberichte per Klick: „An M_Viper senden“ im Absturz-Assistenten schickt Bericht und Mod-Liste an den Entwickler – Windows-Benutzername und Anmeldedaten werden vorher entfernt, ohne Häkchen wird anonym gesendet
|
||||
- Kurznachrichten an Freunde: kleiner Chat im Freunde-Fenster mit Schnellantworten wie „Bin in 10 Min. online“ – neue Nachrichten erscheinen im Launcher und als Meldung im Spiel
|
||||
- Screenshots verschicken: mit 📤 in der Screenshot-Galerie ein Bild direkt an einen Freund schicken, er sieht es im Chat
|
||||
- Schnellstart-Tasten: Lieblings-Instanzen per Rechtsklick auf Strg+1 bis Strg+5 legen und mit der Taste direkt starten
|
||||
- Rechtsklick auf das AeroMC-Symbol neben der Uhr startet Schnellstart- und zuletzt gespielte Instanzen, ohne das Fenster zu öffnen
|
||||
- Welten kopieren: „Kopieren nach …“ übernimmt eine Welt mit einem Klick in eine andere Instanz
|
||||
- Automatische Welt-Sicherung: nach jeder Spielsitzung wird die zuletzt gespielte Welt gesichert, die letzten 5 Stände bleiben erhalten (abschaltbar unter Optionen → Backup)
|
||||
- Rechtsklick-Menü in allen Textfeldern (Ausschneiden, Kopieren, Einfügen, Alles auswählen)
|
||||
|
||||
### Geändert
|
||||
- Freundesystem abgesichert: Anmeldung über Mojangs Sitzungsprüfung wie bei Minecraft-Servern – niemand kann sich mehr als ein anderer Spieler ausgeben, ganz ohne eigenes Passwort
|
||||
- Das Symbol im Infobereich neben der Uhr ist jetzt immer da, nicht nur bei Autostart
|
||||
- Der Zähler am „Freunde“-Knopf zählt jetzt offene Anfragen und ungelesene Nachrichten
|
||||
- Installer: Lizenztext übersichtlich mit Überschriften und Aufzählungen, Rückfragen im Setup-Look statt Windows-Meldungsfenster, der nicht abwählbare Startmenü-Eintrag wird nicht mehr angezeigt
|
||||
- Benötigt das Plugin „AeroMC Freunde“ 1.4.0 auf dem Server (Kurznachrichten, Screenshots, Fehlerberichte, Instanz teilen)
|
||||
|
||||
### Behoben
|
||||
- Windows-Benachrichtigungen zeigten „electron.app.AeroMC“ ohne Logo statt „AeroMC“
|
||||
- Im Import-Fenster standen „Wählen…“, „Suchen“ und „Prüfen“ unter statt neben dem Eingabefeld
|
||||
|
||||
## [1.1.4] - 2026-10-02
|
||||
|
||||
### Neu
|
||||
- Neuer Installer im modernen Look: Schritt-für-Schritt-Assistent mit Lizenz, Ordnerwahl und Verknüpfungen, eigene Deinstallation (Instanzen und Welten bleiben auf Wunsch erhalten) – eine vorhandene Installation wird automatisch übernommen
|
||||
- Optionaler Import im Installer: erkennt Minecraft Launcher, CurseForge, MultiMC, Prism Launcher, PolyMC und Technic und bietet deren Instanzen beim ersten Start zum Übernehmen an
|
||||
- Updates aus dem Launcher heraus laufen jetzt ohne Rückfragen durch und starten AeroMC danach von selbst neu
|
||||
- Freunde: „Beitreten“ startet eine passende Instanz und verbindet direkt mit dem Server, auf dem ein Freund spielt – gibt es keine passende Instanz, wird eine angelegt
|
||||
- Freunde: „Einladen“ schickt einem Freund deinen aktuellen Server – er bekommt die Einladung im Launcher und als Meldung im Spiel (Strg+Alt+J kopiert dort die Adresse)
|
||||
- Freunde: Der eigene Server lässt sich in den Einstellungen vor Freunden verbergen
|
||||
- Clip-Modus in der Spielaufzeichnung: läuft im Hintergrund mit, Alt+F10 speichert die letzten 30, 60 oder 90 Sekunden als MP4
|
||||
- Performance-Paket beim Anlegen einer Fabric-, Quilt- oder NeoForge-Instanz: Sodium, Lithium, FerriteCore, ImmediatelyFast, Entity Culling und ModernFix per Häkchen mitinstallieren
|
||||
- Automatische Mod-Updates vor dem Spielstart (Optionen → Spiel → Mods)
|
||||
- Spielzeit-Statistik: Gesamtspielzeit, Spielstarts, meistgespielte Instanz und Spielzeit pro Instanz auf einen Blick
|
||||
|
||||
### Geändert
|
||||
- Die Suche nach Updates startet jetzt sofort beim Öffnen des Launchers, die Info-Seite zeigt das Ergebnis direkt an
|
||||
- Direkt einem Server beitreten funktioniert jetzt auch bei Minecraft-Versionen vor 1.20
|
||||
|
||||
### Behoben
|
||||
- Die Info-Seite zeigte „Kein Update-Check ausgeführt“, obwohl der Launcher beim Start bereits gesucht hatte
|
||||
|
||||
## [1.1.3] - 2026-09-29
|
||||
|
||||
### Neu
|
||||
- Freundesystem: Freunde per Minecraft-Name hinzufügen, Anfragen annehmen/ablehnen, gesendete Anfragen zurückziehen, Spitznamen vergeben – mit 3D-Spielerkopf und Online-Status
|
||||
- In-Game-Meldung mit 3D-Spielerkopf, sobald ein Freund online kommt – bei mehreren gleichzeitig laufenden Konten erscheint sie im jeweils richtigen Spielfenster
|
||||
- Tastenkombination Strg+Alt+F zeigt im Spiel an, welche Freunde gerade online sind (inklusive Server)
|
||||
- Zähler für offene Freundschaftsanfragen am "Freunde"-Knopf
|
||||
- Freundesystem in den Einstellungen abschaltbar – man ist dann weder sichtbar noch als Freund hinzufügbar, der "Freunde"-Knopf wird ausgeblendet
|
||||
- Neuer Reiter "Aufzeichnungen" im Screenshot-Fenster mit Vorschaubildern, Abspielen und Löschen
|
||||
|
||||
### Geändert
|
||||
- Aufzeichnungen werden jetzt als MP4 gespeichert und laufen damit in jedem gängigen Player
|
||||
- Der Mauszeiger ist in Aufzeichnungen nicht mehr zu sehen
|
||||
|
||||
### Behoben
|
||||
- Aufzeichnungen ruckelten bzw. hatten eine falsche Bildrate
|
||||
- Wechsel des aktiven Microsoft-Kontos wurde nicht in allen Fenstern übernommen
|
||||
|
||||
## [1.1.2] - 2026-09-27
|
||||
|
||||
### Neu
|
||||
- Auflösung, Qualität und Bildrate der Spielaufzeichnung einstellbar (Nativ/1080p/720p/480p, Niedrig/Mittel/Hoch, 30/60 fps) – inklusive Anzeige der tatsächlich aufgenommenen Pixelgröße
|
||||
- Lautstärke jeder einzelnen Tonspur der Aufzeichnung (Systemton, Nur Minecraft, jedes Mikrofon einzeln) regelbar, auch während eine Aufnahme bereits läuft
|
||||
- Video- und Audioeinstellungen der Spielaufzeichnung bleiben jetzt über einen Neustart des Launchers hinweg erhalten
|
||||
|
||||
### Geändert
|
||||
- Anzeige "Aufnahme läuft" samt Timer sitzt jetzt über dem Start/Stopp-Knopf statt auf dem Videobild
|
||||
- Mikrofonnamen in der Audioquellen-Auswahl zeigen keine technische USB-Kennung mehr (z. B. "(9886:0038)")
|
||||
|
||||
### Behoben
|
||||
- Servername in der Spielaufzeichnung fehlte, wenn man sich schon vor dem Öffnen des Aufzeichnungsfensters mit dem Server verbunden hatte – wird jetzt zentral erkannt und trägt sich auch nachträglich in den Titel ein
|
||||
- Vorschaubild der Aufzeichnung blieb beim ersten Öffnen manchmal schwarz – wird jetzt automatisch erkannt und neu erfasst
|
||||
|
||||
## [1.0.9] - 2026-08-09
|
||||
|
||||
### Behoben
|
||||
- Update-Installation brach mit "EPERM: operation not permitted, lstat …" ab, wenn vom vorherigen Versuch noch eine gesperrte Installer-Datei im Temp-Ordner lag (z. B. durch einen Virenscanner) – der Download weicht jetzt automatisch auf einen neuen Dateinamen aus
|
||||
- Autostart mit Windows öffnete im Entwicklungsmodus ein leeres Electron-Fenster statt AeroMC (gleiche Ursache wie zuvor bei den Desktop-Verknüpfungen), zusätzlich zeigte der Autostart-Eintrag bei der Portable-Fassung potenziell auf einen flüchtigen Temp-Pfad
|
||||
- Wiederherstellen aus einem ungültigen oder beschädigten Backup-Archiv löschte die aktuellen, unveränderten Instanzen ersatzlos, obwohl der Vorgang mit einer Fehlermeldung abgebrochen wurde – der Rollback greift jetzt nur noch, wenn am Originalbestand überhaupt schon etwas verändert wurde
|
||||
|
||||
## [1.0.8] - 2026-08-08
|
||||
|
||||
### Neu
|
||||
- Datenpakete lassen sich jetzt wie Mods, Ressourcenpakete und Shader direkt aus dem Modrinth-Katalog durchsuchen, installieren, aktivieren/deaktivieren und aktualisieren
|
||||
- Modpacks lassen sich jetzt auch aus dem CurseForge-Katalog durchsuchen und mit einem Klick installieren (bisher nur Modrinth und Technic; CurseForge ging nur per manuellem ZIP-Import) – braucht wie die bestehende CurseForge-Suche einen eigenen API-Key
|
||||
- OptiFine als eigenständige Loader-Option: Installer-Datei auswählen, AeroMC bereitet die passende Minecraft-Version vor und startet den offiziellen OptiFine-Installer sichtbar; nach Abschluss wird die neue Version automatisch übernommen
|
||||
- Von Hand hinzugefügte OptiFine-Dateien im Mods-Dialog werden jetzt erkannt und eigens gekennzeichnet (funktioniert bei jedem Loader, u. a. für Fabric + OptiFabric)
|
||||
- Lizenztext direkt in den Einstellungen unter Info einsehbar
|
||||
|
||||
### Geändert
|
||||
- Lizenzangabe von MIT auf die tatsächlich geltenden Nutzungsbedingungen umgestellt (nur privater, nicht-kommerzieller Gebrauch – siehe LICENSE)
|
||||
|
||||
## [1.0.7] - 2026-08-06
|
||||
|
||||
### Neu
|
||||
- Desktop-Verknüpfung pro Server: startet die Instanz und verbindet direkt mit dem hinterlegten Server (Quick Play, ab MC 1.20)
|
||||
- Name der Desktop-Verknüpfung (Instanz oder Server) lässt sich beim Erstellen frei selbst vergeben
|
||||
|
||||
### Behoben
|
||||
- Desktop-Verknüpfungen zeigten im Entwicklungsmodus einen leeren Electron-Startbildschirm statt AeroMC zu öffnen
|
||||
- Desktop-Verknüpfungen zeigten im Entwicklungsmodus das Electron-Symbol statt des AeroMC-Logos
|
||||
- Desktop-Verknüpfungen (Instanz und Server) starteten bei kaltem Start unzuverlässig gar keine Instanz – der Startwunsch traf manchmal ein, bevor die Oberfläche bereit war, ihn zu empfangen, und ging dann verloren
|
||||
|
||||
## [1.0.6] - 2026-08-05
|
||||
|
||||
### Neu
|
||||
- Technic-Modpacks direkt durchsuchen und installieren (Katalog), oder eine vorhandene Technic-Installation importieren
|
||||
- Mods lassen sich jetzt auch als eigene Datei hinzufügen, nicht nur über die Suche
|
||||
- Dateien (Mods, Modpacks, Instanzen) per Drag & Drop aufs Fenster ziehen
|
||||
- Tastenkürzel: Strg+F (Suche), Strg+N (neue Instanz), Entf (Instanz löschen)
|
||||
- Windows-Benachrichtigung, wenn ein großer Download oder ein Backup im Hintergrund fertig wird
|
||||
- Serverliste zeigt jetzt Live-Status (Spieleranzahl, MOTD, Version)
|
||||
- Ein-Klick-Knopf für empfohlene GC-Flags bei den JVM-Argumenten
|
||||
- Einzelne Welten lassen sich separat schnellsichern
|
||||
- Warnung, wenn ein Mod entfernt wird, den ein anderer installierter Mod noch braucht
|
||||
- Skin-Hintergrund kann automatisch zwischen mehreren angemeldeten Konten durchwechseln
|
||||
- Optionaler Autostart mit Windows (im Infobereich statt als Fenster)
|
||||
|
||||
### Geändert
|
||||
- Update-Suche läuft automatisch beim Start, Anzeige wird grün wenn alles aktuell ist
|
||||
|
||||
### Behoben
|
||||
- Bedrock-Start repariert (ging nach einem Windows-Update nicht mehr) und startete zwischenzeitlich doppelt
|
||||
- Absturz beim Schließen des Spiels durch hängende Mod-Threads (z. B. JourneyMap-Webmap) behoben
|
||||
@@ -1,245 +1,429 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="src/assets/logo.png" alt="AeroMC Logo" width="140" />
|
||||
|
||||
# AeroMC
|
||||
|
||||
Ein Minecraft-Launcher für Windows mit getrennten Instanzen, Mod-Unterstützung und Microsoft-Anmeldung.
|
||||
|
||||
Aktueller Stand: Version 1.0.9
|
||||
|
||||
</div>
|
||||
|
||||
## Überblick
|
||||
|
||||
AeroMC richtet sich an Spieler, die mehrere Minecraft-Setups sauber voneinander trennen wollen. Jede Instanz kann ihre eigene Version, ihren eigenen Mod-Loader, eigene Mods, eigene Welten und eigene Java-Einstellungen haben.
|
||||
|
||||
Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen, Mods installieren, starten.
|
||||
|
||||
## Funktionen
|
||||
|
||||
- Beliebig viele getrennte Minecraft-Instanzen
|
||||
- Unterstützung für Vanilla, Fabric, Quilt, Forge und NeoForge
|
||||
- Versionsliste mit Filter: Releases, Snapshots oder alle
|
||||
- Modrinth- **und CurseForge**-Integration für **Mods, Ressourcenpakete und Shader** (Suche wahlweise über beide Quellen, Installation inkl. rekursiver Pflicht-Abhängigkeiten, Update-Prüfung samt Sammel-Update)
|
||||
- Modpacks direkt von Modrinth **oder von der Technic-Plattform** durchsuchen und mit einem Klick als neue Instanz installieren – ohne vorherigen manuellen Download
|
||||
- Microsoft-Login mit mehreren Konten
|
||||
- Gemeinsamer Import für AeroMC-ZIP, Modrinth-Modpacks (`.mrpack`), Minecraft Launcher, CurseForge, MultiMC/Prism sowie Technic
|
||||
- Reimport überschreibt vorhandene Instanzen mit gleichem Namen
|
||||
- Export, Kopieren, Gruppieren, Löschen und Desktop-Verknüpfung pro Instanz
|
||||
- Mehrfachauswahl mit Strg-Klick für Sammellöschen, Tastenkürzel für die wichtigsten Aktionen (Strg+F, Strg+N, Entf)
|
||||
- Mods/Modpacks/Instanzen auch per Drag & Drop aufs Fenster ziehen
|
||||
- Mods zusätzlich als eigene Datei hinzufügen, wenn sie nicht über Modrinth/CurseForge zu finden sind
|
||||
- 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), Ü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
|
||||
- Eigenen Minecraft-Skin direkt im Launcher hochladen, mit Live-Vorschau anpassen (klassisch/schlank) und zurücksetzen
|
||||
- Skin-Hintergrund des aktiven Kontos (mehrere Posen, einstellbare Deckkraft) – wechselt bei mehreren angemeldeten Konten optional automatisch durch
|
||||
- Eigene Icons pro Instanz – vorgefertigte Symbole, direkt aus Minecraft extrahierte Block-/Item-Icons oder ein eigenes Bild – plus Notizen
|
||||
- Java pro Instanz oder global, inkl. automatischem Download fehlender Versionen (Eclipse Temurin)
|
||||
- RAM-, JVM-, Pre-Launch- und Post-Exit-Einstellungen (global und pro Instanz), inkl. Ein-Klick-Knopf für empfohlene GC-Flags
|
||||
- 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
|
||||
|
||||
Es gibt zwei Varianten:
|
||||
|
||||
- **AeroMC-Setup.exe** – installiert den Launcher, schneller Start im Alltag
|
||||
- **AeroMC-Portable.exe** – startet ohne Installation, ideal zum Mitnehmen (etwas längerer Start)
|
||||
|
||||
Automatische Updates bleiben getrennt: Setup erhält nur Setup-Updates, Portable nur Portable-Updates. Ein automatischer Wechsel zwischen beiden Varianten findet nicht statt.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Windows x64
|
||||
- Legale Minecraft Java Edition mit Microsoft-Konto
|
||||
- Internetverbindung beim ersten Download von Minecraft-Dateien, Loadern oder Inhalten
|
||||
- Für Bedrock: installierte Minecraft Bedrock Edition aus dem Microsoft Store
|
||||
|
||||
## Erste Schritte
|
||||
|
||||
1. AeroMC installieren oder die portable Datei starten.
|
||||
2. Über **Konten** ein Microsoft-Konto hinzufügen.
|
||||
3. Eine neue Instanz anlegen.
|
||||
4. Minecraft-Version und optional einen Loader wählen.
|
||||
5. Optional Mods/Ressourcenpakete/Shader hinzufügen oder bestehende Setups über **Importieren** übernehmen.
|
||||
6. Instanz starten (online oder offline).
|
||||
|
||||
## Instanzen
|
||||
|
||||
Jede Instanz hat:
|
||||
|
||||
- Namen und optionale Gruppe
|
||||
- Minecraft-Version und optionalen Mod-Loader
|
||||
- optionales Icon (vorgegeben, aus Minecraft extrahiert oder eigenes Bild) und Notizen
|
||||
- eigene Mods, Ressourcenpakete, Shader, Welten, Datenpakete, Screenshots und Serverliste
|
||||
- eigene Java-, RAM-, JVM-, Pre-Launch- und Post-Exit-Einstellungen (leer = globale Vorgabe), inkl. Knopf für empfohlene GC-Flags bei den JVM-Argumenten
|
||||
|
||||
Über die Seitenleiste einer ausgewählten Instanz erreichst du u. a.:
|
||||
|
||||
- Starten / Offline starten
|
||||
- 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, Desktop-Verknüpfung pro Server)
|
||||
- Logdateien und Absturzberichte ansehen
|
||||
- Minecraft-, Konfigurations- und Instanz-Ordner öffnen
|
||||
- Desktop-Verknüpfung erstellen
|
||||
- Exportieren, Kopieren, Gruppe ändern, Löschen
|
||||
- Bei importierten Modpacks: Modpack aktualisieren
|
||||
|
||||
In der Instanzliste: Suche, Sortierung und **Strg + Klick** für Mehrfachauswahl.
|
||||
|
||||
### Tastenkürzel
|
||||
|
||||
| Taste | Wirkung |
|
||||
|---|---|
|
||||
| Strg+F | Suche fokussieren |
|
||||
| Strg+N | Neue Instanz anlegen |
|
||||
| Entf | Markierte Instanz(en) löschen |
|
||||
| Esc | Aktuellen Dialog schließen |
|
||||
|
||||
Greifen nicht, während ein Textfeld fokussiert ist oder ein Dialog mit eigener Suche offen ist.
|
||||
|
||||
## Import
|
||||
|
||||
Über **Importieren**:
|
||||
|
||||
| Quelle | Typische Nutzung |
|
||||
|---|---|
|
||||
| ZIP-Archiv | AeroMC-Export (`instance.json`) sowie einzelne MultiMC/Prism-Instanzen (auch gezippt) |
|
||||
| Modpack | Modrinth-Katalog direkt durchsuchen und installieren, oder Datei importieren: `.mrpack` sowie CurseForge-ZIP (`manifest.json`) |
|
||||
| Minecraft Launcher | Profile aus dem offiziellen Launcher |
|
||||
| CurseForge | Instanzen aus dem CurseForge-Ordner |
|
||||
| MultiMC / Prism | Vorhandene MultiMC- oder Prism-Instanzen |
|
||||
| Technic | Technic-Katalog direkt durchsuchen und installieren, oder eine vorhandene Technic-Installation importieren (`%APPDATA%\.technic\modpacks`) |
|
||||
|
||||
Existiert bereits eine Instanz mit demselben Namen, wird sie ersetzt.
|
||||
|
||||
Beim offiziellen Minecraft Launcher werden nur benutzerdefinierte Profile angeboten (keine „Latest Release“/„Latest Snapshot“). Bei gemeinsamen Standard-Spielordnern übernimmt AeroMC spielrelevante Daten (Mods, Welten, Konfiguration), nicht den kompletten Cache.
|
||||
|
||||
Im Reiter **Modpack** desselben Dialogs lässt sich zusätzlich direkt im Modrinth-Katalog suchen und ein Modpack mit einem Klick installieren – ganz ohne vorherigen manuellen Download der `.mrpack`-Datei. Im Reiter **Technic** gibt es dasselbe für den Technic-Katalog.
|
||||
|
||||
Alternativ: `.jar`-, `.mrpack`- oder `.zip`-Dateien direkt aufs Launcher-Fenster ziehen. AeroMC erkennt anhand des Inhalts, ob es sich um einen Mod (landet in der gerade offenen/ausgewählten Instanz), ein Modpack oder einen Instanz-Export handelt.
|
||||
|
||||
## Mods, Ressourcenpakete und Shader
|
||||
|
||||
Im Inhalts-Dialog einer Instanz:
|
||||
|
||||
- Katalog durchsuchen (Quelle Modrinth oder CurseForge, Sortierung, Kategorien)
|
||||
- Installation inkl. **automatischer** Installation aller Pflicht-Abhängigkeiten (rekursiv, auch quellübergreifend)
|
||||
- Eigene Datei hinzufügen (`.jar`/`.zip`) – für Mods, die nicht über Modrinth/CurseForge zu finden sind, oder per Drag & Drop aufs Fenster ziehen
|
||||
- Installierte Einträge aktivieren, deaktivieren, entfernen – beim Entfernen warnt AeroMC, falls ein anderer installierter Mod das noch als Pflicht-Abhängigkeit braucht
|
||||
- Nach Updates suchen und alle aktualisieren
|
||||
- Ordner der jeweiligen Inhaltsart öffnen
|
||||
|
||||
Ressourcenpakete und Shader funktionieren auch in Vanilla-Instanzen. Mods benötigen einen Mod-Loader. Für CurseForge-Inhalte wird ein eigener API-Key oder eine Weiterleitungs-Adresse benötigt (siehe **Einstellungen → Netzwerk**).
|
||||
|
||||
## Skin ändern
|
||||
|
||||
Über **Konten → Skin ändern…** lässt sich der Skin des aktiven Microsoft-Kontos direkt im Launcher ändern:
|
||||
|
||||
- PNG-Datei auswählen, sofortige Prüfung auf gültige Maße
|
||||
- Live-Vorschau am 3D-Modell vor dem Anwenden
|
||||
- Modell wählen: klassisch oder schlank (slim)
|
||||
- Auf einen früheren Standard-Skin zurücksetzen
|
||||
|
||||
Die Änderung wird direkt bei Mojang für das angemeldete Konto hinterlegt und gilt somit auch außerhalb von AeroMC.
|
||||
|
||||
## Backup und Wiederherstellung
|
||||
|
||||
Unter **Optionen → Backup**:
|
||||
|
||||
- Manuelles globales Backup (alle Instanzen + Launcher-Einstellungen)
|
||||
- Automatische Sicherungen (täglich oder wöchentlich, einstellbare Anzahl)
|
||||
- Optional zusätzlich vor jedem Spielstart sichern
|
||||
- Wiederherstellung aus vorhandenem Backup
|
||||
|
||||
Speicherort: `Dokumente\AeroMC Launcher\Backups`.
|
||||
|
||||
**Wichtig:** Konten und Tokens werden nicht mitgesichert. Beim Wiederherstellen werden aktuelle Instanzen und Launcher-Einstellungen durch den Backup-Inhalt ersetzt.
|
||||
|
||||
## Einstellungen
|
||||
|
||||
### Allgemein
|
||||
- Design (Dunkel / Hell)
|
||||
- Instanzen sortieren (nach letztem Start / nach Name)
|
||||
- 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)
|
||||
- Skin-Hintergrund: Quelle, Pose, Deckkraft, optional automatischer Wechsel zwischen mehreren angemeldeten Konten (mit einstellbarem Zeitabstand)
|
||||
|
||||
### Spiel
|
||||
- Standard-RAM (min./max.)
|
||||
- Fenstergröße bzw. maximiert starten
|
||||
|
||||
### Start & Java
|
||||
- Globale Java-Auswahl und Erkennung
|
||||
- Fehlende Java-Version automatisch herunterladen
|
||||
- Konsole: während des Spiels, nach Beenden schließen, bei Absturz anzeigen, Historie-Limit
|
||||
- Globale Pre-Launch- / Post-Exit-Befehle
|
||||
- Optionaler Wrapper-Befehl
|
||||
|
||||
### Netzwerk
|
||||
- HTTP-Proxy (Host, Port, optional Benutzer/Passwort)
|
||||
- CurseForge: eigener API-Key und/oder Weiterleitungs-Adresse inkl. Token
|
||||
|
||||
Ohne eigenen Key und ohne Weiterleitung funktionieren Modrinth-Inhalte weiterhin. Für CurseForge-Downloads wird einer der beiden Wege benötigt.
|
||||
|
||||
### Konten
|
||||
- Mehrere Microsoft-Konten verwalten, aktiv setzen, entfernen
|
||||
|
||||
### Backup
|
||||
- Manuell, Zeitplan und Wiederherstellung
|
||||
|
||||
### Info
|
||||
- Version, Autor und Links
|
||||
- Update-Suche läuft automatisch kurz nach dem Start (Status erscheint grün, wenn alles aktuell ist) sowie jederzeit manuell per Knopf
|
||||
|
||||
## Bedrock
|
||||
|
||||
Der Bedrock-Bereich startet die vorhandene Windows-Store-Version und erleichtert den Zugriff auf Welten, Pakete, Add-ons und Ordner. Zusätzliche Add-on-Dateien lassen sich direkt über AeroMC installieren. AeroMC installiert Bedrock selbst (die App aus dem Microsoft Store) nicht. Fehlt die Installation, wird der Bereich automatisch ausgeblendet.
|
||||
|
||||
## Konsole, Logs und Crash-Reports
|
||||
|
||||
- Konsole über die obere Leiste oder bei Absturz
|
||||
- Pro Instanz: gespeicherte Logdateien und Crash-Reports einsehen, kopieren und Ordner öffnen
|
||||
|
||||
## Datenort
|
||||
|
||||
Nutzerdaten liegen lokal unter `%APPDATA%\aeromc`.
|
||||
|
||||
## Links
|
||||
|
||||
- Webseite: https://m-viper.de
|
||||
- Launcher-Webseite: https://aeromc.viper.ipv64.net
|
||||
- Autor: M_Viper
|
||||
|
||||
## Rechtliches
|
||||
|
||||
AeroMC ist kein offizielles Produkt von Mojang oder Microsoft.
|
||||
Minecraft ist eine Marke von Mojang AB.
|
||||
|
||||
### Urheberrecht & Nutzungsbedingungen
|
||||
|
||||
AeroMC ist urheberrechtlich geschützt. © M_Viper – Alle Rechte vorbehalten.
|
||||
|
||||
- ❌ Die Weitergabe dieses Launchers an Dritte ist strengstens untersagt
|
||||
- ❌ Das Verändern, Dekompilieren oder Bearbeiten des Codes ist nicht erlaubt
|
||||
- ❌ Eine Veröffentlichung – auch in veränderter Form – ist verboten
|
||||
- ❌ Der Verkauf oder die kommerzielle Nutzung ist nicht gestattet
|
||||
- ✅ Die Nutzung ist ausschließlich für den privaten, nicht-kommerziellen Gebrauch erlaubt
|
||||
|
||||
Wer gegen diese Bedingungen verstößt, macht sich strafbar und kann rechtlich belangt werden.
|
||||
<div align="center">
|
||||
|
||||
<img src="src/assets/logo.png" alt="AeroMC Logo" width="140" />
|
||||
|
||||
# AeroMC
|
||||
|
||||
Ein Minecraft-Launcher für Windows mit getrennten Instanzen, Mod-Unterstützung, Spielaufzeichnung, Freundesystem und Microsoft-Anmeldung.
|
||||
|
||||
Aktueller Stand: Version 1.2.0
|
||||
|
||||
<img src="docs/aeromc.png" alt="AeroMC – Instanzliste mit Seitenleiste und Skin-Vorschau" width="820" />
|
||||
|
||||
</div>
|
||||
|
||||
## Überblick
|
||||
|
||||
AeroMC richtet sich an Spieler, die mehrere Minecraft-Setups sauber voneinander trennen wollen. Jede Instanz kann ihre eigene Version, ihren eigenen Mod-Loader, eigene Mods, eigene Welten und eigene Java-Einstellungen haben.
|
||||
|
||||
Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen, Mods installieren, starten – und alles, was drumherum gebraucht wird, vom Aufnehmen bis zum Zusammenspielen mit Freunden.
|
||||
|
||||
## Funktionen
|
||||
|
||||
**Instanzen**
|
||||
- Beliebig viele getrennte Minecraft-Instanzen
|
||||
- Unterstützung für Vanilla, Fabric, Quilt, Forge, NeoForge und OptiFine
|
||||
- Versionsliste mit Filter: Releases, Snapshots oder alle
|
||||
- Performance-Paket beim Anlegen: Sodium, Lithium, FerriteCore, ImmediatelyFast, Entity Culling und ModernFix per Häkchen
|
||||
- Export, Kopieren, Gruppieren, Löschen und Desktop-Verknüpfung pro Instanz
|
||||
- Eigene Icons pro Instanz – vorgefertigte Symbole, aus Minecraft extrahierte Block-/Item-Icons oder ein eigenes Bild – plus Notizen
|
||||
- Mehrfachauswahl mit Strg-Klick, Tastenkürzel für die wichtigsten Aktionen
|
||||
- Schnellstart-Tasten: Lieblings-Instanzen auf **Strg+1** bis **Strg+5** legen
|
||||
- Instanzen per Rechtsklick auf das Symbol neben der Uhr starten, ohne das Fenster zu öffnen
|
||||
- **Instanz teilen per Code**: Freunde bekommen Version, Loader und alle Mods in denselben Versionen
|
||||
- Spielzeit-Statistik: Gesamtspielzeit, Spielstarts und Spielzeit pro Instanz
|
||||
|
||||
**Mods, Modpacks und Inhalte**
|
||||
- Modrinth- **und CurseForge**-Integration für Mods, Ressourcenpakete, Shader und Datenpakete (Installation inkl. rekursiver Pflicht-Abhängigkeiten)
|
||||
- Update-Prüfung samt Sammel-Update, auf Wunsch automatisch vor jedem Spielstart
|
||||
- Modpacks direkt von Modrinth, CurseForge oder der Technic-Plattform mit einem Klick installieren
|
||||
- Mods als eigene Datei hinzufügen oder per Drag & Drop aufs Fenster ziehen
|
||||
- Warnung beim Entfernen eines Mods, der noch als Pflicht-Abhängigkeit gebraucht wird
|
||||
|
||||
**Import**
|
||||
- Gemeinsamer Import für AeroMC-ZIP, Modrinth-Modpacks (`.mrpack`), Minecraft Launcher, CurseForge, **Modrinth App**, MultiMC/Prism sowie Technic
|
||||
- Schon bei der Installation: der Installer erkennt andere Launcher und AeroMC bietet deren Instanzen beim ersten Start zum Übernehmen an
|
||||
- Von Freunden geteilte Instanzen per Code einlösen
|
||||
|
||||
**Spielaufzeichnung**
|
||||
- Nimmt nur das Minecraft-Fenster auf – ohne Desktop-Ränder und ohne Mauszeiger – und speichert als MP4
|
||||
- Auflösung, Qualität und Bildrate einstellbar, mehrere Tonspuren (Systemton, nur Minecraft, Mikrofone) mit eigener Lautstärke
|
||||
- Clip-Modus: läuft im Hintergrund mit, **Alt+F10** speichert die letzten 30, 60 oder 90 Sekunden
|
||||
- Servername im Aufnahmetitel, Aufzeichnungen-Galerie mit Vorschaubildern
|
||||
|
||||
**Freunde**
|
||||
- Freunde per Minecraft-Name hinzufügen, mit 3D-Spielerkopf, Online-Status und Spitznamen
|
||||
- Meldung im Spiel, sobald ein Freund online kommt – bei mehreren Konten im jeweils richtigen Fenster
|
||||
- **Beitreten**: startet eine passende Instanz und verbindet direkt mit dem Server eines Freundes
|
||||
- **Einladen**: schickt Freunden deinen aktuellen Server – als Hinweis im Launcher und als Meldung im Spiel
|
||||
- **Kurznachrichten** mit Schnellantworten wie „Bin in 10 Min. online“, auch als Meldung im Spiel
|
||||
- **Screenshots** direkt aus der Galerie an Freunde schicken
|
||||
- Anmeldung über Mojangs Sitzungsprüfung – niemand kann sich als ein anderer Spieler ausgeben
|
||||
- Eigener Server auf Wunsch verborgen, Freundesystem komplett abschaltbar
|
||||
|
||||
**Konten, Welten und Backups**
|
||||
- Microsoft-Login mit mehreren Konten – Wechsel mit einem Klick über das Konto-Menü oben rechts
|
||||
- Eigenen Skin im Launcher hochladen, mit Live-Vorschau anpassen (klassisch/schlank) und zurücksetzen
|
||||
- **Skin-Editor mit zwei Schichten**: Skins selbst pixeln – auf der Vorlage oder direkt auf der 3D-Figur, mit Spiegeln, Füllen, Pipette und Rückgängig
|
||||
- **Eigene Umhänge, auch HD bis 1024×512**: im Skin-Editor gestalten, im Spiel in jeder Version und mit jedem Loader sichtbar – für Spieler mit anderen Launchern gibt es den Mod „AeroMC Umhänge“
|
||||
- Offiziellen Mojang-Umhang direkt im Launcher wechseln oder ablegen
|
||||
- **Garderobe**: Skins und Umhänge speichern und mit einem Klick wieder anziehen – der bisherige Skin wird vor jedem Wechsel automatisch gesichert
|
||||
- Armform-Prüfung: Hinweis und Ein-Klick-Korrektur, wenn die Armform nicht zur Skin-Datei passt
|
||||
- Skin-Hintergrund des aktiven Kontos (mehrere Posen, passende Armform, einstellbare Deckkraft, optional wechselnd)
|
||||
- Welten, Ressourcenpakete, Datenpakete, Screenshots, Serverliste und Logdateien verwalten
|
||||
- Serverliste mit Live-Status und Desktop-Verknüpfung pro Server (startet die Instanz und verbindet direkt)
|
||||
- Welten mit einem Klick in eine andere Instanz kopieren
|
||||
- Globales Backup inkl. automatischer Zeitplan-Sicherungen, dazu Schnellsicherung einzelner Welten
|
||||
- Automatische Welt-Sicherung nach jeder Spielsitzung (die letzten 5 Stände je Welt)
|
||||
|
||||
**Technik und Komfort**
|
||||
- Java pro Instanz oder global, inkl. automatischem Download fehlender Versionen (Eclipse Temurin)
|
||||
- RAM-, JVM-, Pre-Launch- und Post-Exit-Einstellungen (global und pro Instanz), Ein-Klick-Knopf für empfohlene GC-Flags
|
||||
- **Absturz-Assistent**: erklärt Abstürze in einfachen Worten und behebt sie oft mit einem Klick; auf Wunsch geht der Bericht an den Entwickler
|
||||
- Eingebaute Konsole/Log-Ansicht inkl. Absturzberichte pro Instanz
|
||||
- Bedrock-Bereich für eine vorhandene Bedrock-Installation, inkl. Add-on-Installation
|
||||
- Update-Suche sofort beim Start, Updates installieren sich ohne Rückfragen
|
||||
- Optionaler Autostart mit Windows (im Infobereich statt als Fenster)
|
||||
- Rechtsklick-Menü in allen Textfeldern (Ausschneiden, Kopieren, Einfügen)
|
||||
- **Fünf Designs**: Klassisch hell oder dunkel, „AeroMC“ im Look der Webseite, „Nachtlila“ (dasselbe in dunklem Lila) und „Minecraft“ mit Block-Optik und Pixel-Schrift – samt passender Titelleiste
|
||||
- Optionaler HTTP-Proxy und Wrapper-Befehl
|
||||
|
||||
## Designs
|
||||
|
||||
Unter **Optionen → Darstellung → Design** stehen fünf Designs zur Wahl. Ein Klick auf eine Vorschau-Kachel zeigt es sofort an, mit **Speichern** gilt es in allen Fenstern – inklusive Titelleiste und Fensterknöpfen.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><img src="docs/design-aeromc.png" alt="AeroMC-Design: Nachtblau mit Cyan-Verläufen wie die Webseite" width="270" /><br /><b>AeroMC</b><br /><sub>Nachtblau, Cyan-Verläufe, runde Knöpfe – wie die Webseite</sub></td>
|
||||
<td align="center"><img src="docs/design-nachtlila.png" alt="Nachtlila-Design: wie AeroMC in dunklem Lila" width="270" /><br /><b>Nachtlila</b><br /><sub>wie AeroMC, nur in dunklem Lila</sub></td>
|
||||
<td align="center"><img src="docs/design-minecraft.png" alt="Minecraft-Design: Block-Optik mit Grasnarbe, Erde und Stein-Knöpfen" width="270" /><br /><b>Minecraft</b><br /><sub>Grasnarbe, Erde, Stein-Knöpfe und Pixel-Schrift</sub></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
Dazu **Klassisch Dunkel** (Bild ganz oben) und **Klassisch Hell** im bewährten Launcher-Look.
|
||||
|
||||
## Installation
|
||||
|
||||
Es gibt zwei Varianten:
|
||||
|
||||
- **AeroMC-Setup.exe** – installiert den Launcher, schneller Start im Alltag
|
||||
- **AeroMC-Portable.exe** – startet ohne Installation, ideal zum Mitnehmen (etwas längerer Start)
|
||||
|
||||
Der Installer führt Schritt für Schritt durch Willkommen, Lizenz, Optionen (Ordner, Desktop-Verknüpfung) und – falls andere Launcher gefunden werden – einen optionalen Import. Administratorrechte sind nicht nötig, installiert wird nur für das eigene Benutzerkonto. Eine bereits vorhandene Installation wird automatisch übernommen; Instanzen, Welten und Einstellungen bleiben erhalten.
|
||||
|
||||
Deinstallieren über **Windows-Einstellungen → Apps → AeroMC**. Instanzen und Welten bleiben dabei standardmäßig erhalten und lassen sich auf Wunsch mit entfernen (Papierkorb).
|
||||
|
||||
Automatische Updates bleiben getrennt: Setup erhält nur Setup-Updates, Portable nur Portable-Updates. Ein automatischer Wechsel zwischen beiden Varianten findet nicht statt.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- Windows 10 oder 11 (x64)
|
||||
- Legale Minecraft Java Edition mit Microsoft-Konto
|
||||
- Internetverbindung beim ersten Download von Minecraft-Dateien, Loadern oder Inhalten
|
||||
- Für Bedrock: installierte Minecraft Bedrock Edition aus dem Microsoft Store
|
||||
|
||||
## Erste Schritte
|
||||
|
||||
1. AeroMC installieren oder die portable Datei starten.
|
||||
2. Über den Konto-Knopf oben rechts (oder **Optionen → Konten**) ein Microsoft-Konto hinzufügen.
|
||||
3. Eine neue Instanz anlegen, Minecraft-Version und optional einen Loader wählen – bei Fabric, Quilt oder NeoForge auf Wunsch gleich mit Performance-Paket.
|
||||
4. Optional Mods/Ressourcenpakete/Shader hinzufügen oder bestehende Setups über **Importieren** übernehmen.
|
||||
5. Instanz starten (online oder offline).
|
||||
|
||||
## Instanzen
|
||||
|
||||
Jede Instanz hat:
|
||||
|
||||
- Namen und optionale Gruppe – Untergruppen mit „/“ trennen, z. B. `Modpacks/Survival` (eingerückt, einzeln einklappbar, per Drag & Drop befüllbar)
|
||||
- Minecraft-Version und optionalen Mod-Loader
|
||||
- optionales Icon (vorgegeben, aus Minecraft extrahiert oder eigenes Bild) und Notizen
|
||||
- eigene Mods, Ressourcenpakete, Shader, Welten, Datenpakete, Screenshots, Aufzeichnungen und Serverliste
|
||||
- eigene Java-, RAM-, JVM-, Pre-Launch- und Post-Exit-Einstellungen (leer = globale Vorgabe)
|
||||
|
||||
Über die Seitenleiste einer ausgewählten Instanz erreichst du u. a.:
|
||||
|
||||
- Starten / Offline starten
|
||||
- Instanz und Notizen bearbeiten
|
||||
- Mods, Ressourcenpakete und Shader
|
||||
- Welten (inkl. Schnellsicherung und „Kopieren nach …“ in eine andere Instanz), Datenpakete, Bildschirmfotos und Aufzeichnungen
|
||||
- Spiel aufzeichnen
|
||||
- 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
|
||||
- Exportieren, **Instanz teilen** (Code für Freunde), Kopieren, Gruppe ändern, Löschen
|
||||
- Bei importierten Modpacks: Modpack aktualisieren
|
||||
|
||||
In der Instanzliste: Suche, Sortierung und **Strg + Klick** für Mehrfachauswahl. Über **Statistik** in der oberen Leiste gibt es die Spielzeit aller Instanzen im Überblick.
|
||||
|
||||
**Schnellstart:** Rechtsklick auf eine Instanz → **Schnellstart-Taste …** legt sie auf Strg+1 bis Strg+5; die Kachel zeigt dann ein kleines Abzeichen. Schnellstart- und zuletzt gespielte Instanzen stehen außerdem im Rechtsklick-Menü des AeroMC-Symbols neben der Uhr – ein Klick startet das Spiel, ohne das Launcher-Fenster zu öffnen.
|
||||
|
||||
**Instanz teilen:** „Instanz teilen“ in der Seitenleiste erzeugt einen Code wie `K7QD-M2XP` (30 Tage gültig). Freunde lösen ihn unter **Importieren → Per Code** ein und bekommen Minecraft-Version, Loader sowie alle Mods, Ressourcenpakete und Shader in denselben Versionen – geladen direkt von Modrinth bzw. CurseForge. Von Hand hinzugefügte Dateien sind nicht dabei; AeroMC nennt sie.
|
||||
|
||||
### Tastenkürzel
|
||||
|
||||
Im Launcher:
|
||||
|
||||
| Taste | Wirkung |
|
||||
|---|---|
|
||||
| Strg+F | Suche öffnen (Lupe in der Werkzeugleiste), Esc leert und schließt sie |
|
||||
| Strg+N | Neue Instanz anlegen |
|
||||
| Strg+1 … Strg+5 | Schnellstart-Instanz starten |
|
||||
| Entf | Markierte Instanz(en) löschen |
|
||||
| Esc | Aktuellen Dialog schließen |
|
||||
|
||||
Greifen nicht, während ein Textfeld fokussiert ist oder ein Dialog mit eigener Suche offen ist.
|
||||
|
||||
Im Spiel (solange ein über AeroMC gestartetes Minecraft läuft):
|
||||
|
||||
| Taste | Wirkung |
|
||||
|---|---|
|
||||
| Strg+Alt+F | Zeigt, welche Freunde gerade online sind |
|
||||
| Strg+Alt+J | Kopiert die Server-Adresse der letzten Einladung |
|
||||
| Alt+F10 | Speichert im Clip-Modus die letzten Sekunden (ersatzweise Strg+Alt+C) |
|
||||
|
||||
## Import
|
||||
|
||||
Über **Importieren**:
|
||||
|
||||
| Quelle | Typische Nutzung |
|
||||
|---|---|
|
||||
| ZIP-Archiv | AeroMC-Export (`instance.json`) sowie einzelne MultiMC/Prism-Instanzen (auch gezippt) |
|
||||
| Modpack | Modrinth- oder CurseForge-Katalog direkt durchsuchen und installieren, oder Datei importieren: `.mrpack` sowie CurseForge-ZIP (`manifest.json`) |
|
||||
| Minecraft Launcher | Profile aus dem offiziellen Launcher |
|
||||
| CurseForge | Instanzen aus dem CurseForge-Ordner |
|
||||
| Modrinth App | Instanzen aus der Modrinth App (`%APPDATA%\ModrinthApp`) inkl. Einstellungen, Spielzeit und Modpack-Verknüpfung |
|
||||
| MultiMC / Prism | Vorhandene MultiMC- oder Prism-Instanzen |
|
||||
| Technic | Technic-Katalog direkt durchsuchen und installieren, oder eine vorhandene Technic-Installation importieren (`%APPDATA%\.technic\modpacks`) |
|
||||
| Per Code | Von einem Freund geteilte Instanz einlösen (siehe **Instanzen → Instanz teilen**) |
|
||||
|
||||
Existiert bereits eine Instanz mit demselben Namen, wird sie ersetzt.
|
||||
|
||||
**Große Importe:** Auch viele Gigabyte werden im Hintergrund kopiert – der Launcher bleibt bedienbar. Vorher prüft AeroMC, ob genug Speicherplatz frei ist. Oben im Import-Fenster stehen Instanz, übertragene GB, Prozent, Tempo, Restzeit und die aktuelle Datei; **Abbrechen** behält fertig übernommene Instanzen und entfernt die halb kopierte. Einzelne gesperrte Dateien werden übersprungen und gemeldet.
|
||||
|
||||
Beim offiziellen Minecraft Launcher werden nur benutzerdefinierte Profile angeboten (keine „Latest Release“/„Latest Snapshot“). Bei gemeinsamen Standard-Spielordnern übernimmt AeroMC spielrelevante Daten (Mods, Welten, Konfiguration), nicht den kompletten Cache.
|
||||
|
||||
**Modrinth App:** Übernommen werden Name, Gruppe, Symbol, Minecraft-Version, Loader, Arbeitsspeicher, Java-Einstellungen, Spielzeit sowie Mods, Welten und Einstellungen. Modrinth-Modpacks bleiben mit ihrem Paket verknüpft und lassen sich weiter aktualisieren. Ein in der App verlegter Datenordner wird automatisch gefunden, und die Modrinth App darf dabei geöffnet bleiben – AeroMC liest auch ihr Schreibprotokoll mit. An den Daten der Modrinth App wird nichts verändert.
|
||||
|
||||
**Schon bei der Installation:** Der Installer sucht nach Minecraft Launcher, CurseForge, Modrinth App, MultiMC, Prism Launcher, PolyMC und Technic – ein portables MultiMC auch über seine Verknüpfung auf dem Desktop oder im Startmenü. Was dort angekreuzt wird, öffnet AeroMC beim ersten Start fertig durchsucht im Import; ausgewählt wird dann nur noch, was übernommen werden soll. In den anderen Launchern wird nichts verändert.
|
||||
|
||||
Alternativ: `.jar`-, `.mrpack`- oder `.zip`-Dateien direkt aufs Launcher-Fenster ziehen. AeroMC erkennt anhand des Inhalts, ob es sich um einen Mod (landet in der gerade ausgewählten Instanz), ein Modpack oder einen Instanz-Export handelt.
|
||||
|
||||
## Mods, Ressourcenpakete und Shader
|
||||
|
||||
Im Inhalts-Dialog einer Instanz:
|
||||
|
||||
- Katalog durchsuchen (Quelle Modrinth oder CurseForge, Sortierung, Kategorien)
|
||||
- Installation inkl. **automatischer** Installation aller Pflicht-Abhängigkeiten (rekursiv, auch quellübergreifend)
|
||||
- Eigene Datei hinzufügen (`.jar`/`.zip`) – für Mods, die nicht über Modrinth/CurseForge zu finden sind, oder per Drag & Drop
|
||||
- Installierte Einträge aktivieren, deaktivieren, entfernen – mit Warnung, falls ein anderer Mod sie noch braucht
|
||||
- Nach Updates suchen und alle aktualisieren
|
||||
- Ordner der jeweiligen Inhaltsart öffnen
|
||||
|
||||
Ressourcenpakete und Shader funktionieren auch in Vanilla-Instanzen. Mods benötigen einen Mod-Loader. Für CurseForge-Inhalte wird ein eigener API-Key oder eine Weiterleitungs-Adresse benötigt (siehe **Einstellungen → Netzwerk**).
|
||||
|
||||
**Performance-Paket:** Beim Anlegen einer Fabric-, Quilt- oder NeoForge-Instanz installiert ein Häkchen Sodium, Lithium, FerriteCore, ImmediatelyFast, Entity Culling und ModernFix gleich mit. Was es für die gewählte Version noch nicht gibt, wird übersprungen.
|
||||
|
||||
**Automatische Mod-Updates:** Unter **Optionen → Spiel → Mods** sucht AeroMC auf Wunsch vor jedem Start nach neueren Fassungen der Modrinth-Mods und installiert sie. Dauert die Suche länger als 20 Sekunden, startet Minecraft ohne Update.
|
||||
|
||||
## Spielaufzeichnung
|
||||
|
||||
Über **Spiel aufzeichnen** in der Seitenleiste:
|
||||
|
||||
- Aufgenommen wird nur das Minecraft-Fenster – ohne Desktop-Ränder und ohne Mauszeiger
|
||||
- Auflösung (Nativ, 1080p, 720p, 480p), Qualität (Niedrig/Mittel/Hoch) und Bildrate (30/60 fps) einstellbar
|
||||
- Tonspuren: Systemton, nur Minecraft (isoliert) und beliebig viele Mikrofone, jede mit eigenem Lautstärkeregler – auch während der Aufnahme
|
||||
- Der Server, auf dem gerade gespielt wird, erscheint automatisch im Titel
|
||||
- Gespeichert wird als MP4 unter `Videos\AeroMC Launcher\<Instanz>`, zu finden auch unter **Bildschirmfotos verwalten → Aufzeichnungen**
|
||||
|
||||
**Clip-Modus:** Läuft im Hintergrund mit, auch bei geschlossenem Aufzeichnungsfenster. **Alt+F10** (ist das belegt, z. B. durch ShadowPlay: Strg+Alt+C) speichert die letzten 30, 60 oder 90 Sekunden als `Clip_….mp4`, mit kurzer Meldung im Spiel. Endet Minecraft, endet auch der Clip-Modus.
|
||||
|
||||
## Freunde
|
||||
|
||||
Über **Freunde** in der oberen Leiste:
|
||||
|
||||
- Freunde per Minecraft-Name hinzufügen, Anfragen annehmen, ablehnen oder zurückziehen – offene Anfragen zählt der Knopf mit
|
||||
- Spitznamen vergeben, Online-Status und Server der Freunde sehen
|
||||
- Meldung im Spiel mit 3D-Spielerkopf, sobald ein Freund online kommt
|
||||
- **Beitreten** startet eine passende eigene Instanz (gleiche Minecraft-Version, bevorzugt mit dem Server in der Liste) und verbindet direkt; gibt es keine, wird eine Vanilla-Instanz angelegt
|
||||
- **Einladen** schickt Freunden den eigenen aktuellen Server. Im Launcher erscheint die Einladung mit „Beitreten“, im Spiel als Meldung – dort kopiert **Strg+Alt+J** die Adresse für „Direkt verbinden“
|
||||
- **Kurznachrichten**: 💬 beim Freund öffnet eine kleine Unterhaltung mit Schnellantworten („Bin in 10 Min. online“, „Komm online!“ …) oder eigenem Text bis 200 Zeichen. Neue Nachrichten erscheinen als Hinweis im Launcher und – beim Spielen – als Meldung im Spiel; der Zähler am „Freunde“-Knopf zählt offene Anfragen und ungelesene Nachrichten. Nachrichten werden nach 7 Tagen gelöscht.
|
||||
- **Screenshots verschicken**: In **Bildschirmfotos verwalten** schickt 📤 ein Bild (verkleinert auf höchstens 1600 Pixel Breite) samt optionalem Text an einen Freund; er sieht es im Chat.
|
||||
|
||||
**Sicherheit:** AeroMC meldet sich beim Freundesystem wie ein Minecraft-Server über Mojangs Sitzungsprüfung an – ohne eigenes Passwort, und niemand kann sich mit einer fremden Spieler-ID als jemand anderes ausgeben.
|
||||
|
||||
Unter **Optionen → Allgemein → Freundesystem** lässt sich der eigene Server vor Freunden verbergen oder das Freundesystem ganz abschalten – dann ist man für niemanden sichtbar und kann nicht hinzugefügt werden.
|
||||
|
||||
Das Freundesystem funktioniert nur zwischen Spielern, die AeroMC nutzen und mindestens einmal mit eingeschaltetem Freundesystem darüber gespielt haben.
|
||||
|
||||
## Skin ändern
|
||||
|
||||
Über den **eigenen Kontonamen oben rechts → Skin ändern…** lässt sich der Skin des aktiven Microsoft-Kontos direkt im Launcher ändern (der Knopf **Skin** in der Werkzeugleiste schaltet dagegen nur den Skin-Hintergrund ein/aus):
|
||||
|
||||
- PNG-Datei auswählen, sofortige Prüfung auf gültige Maße
|
||||
- Live-Vorschau am 3D-Modell vor dem Anwenden
|
||||
- Modell wählen: klassisch oder schlank (slim)
|
||||
- Auf den Standard-Skin (Steve/Alex) zurücksetzen – einen früheren eigenen Skin holt das nicht zurück; dafür vorher im Skin-Editor „Als PNG speichern…“ nutzen
|
||||
|
||||
Die Änderung wird direkt bei Mojang für das angemeldete Konto hinterlegt und gilt somit auch außerhalb von AeroMC.
|
||||
|
||||
### Skin-Editor
|
||||
|
||||
**Im Editor bearbeiten…** öffnet einen eigenen Pixel-Editor – mit der gewählten PNG-Datei oder dem aktuellen Skin des Kontos:
|
||||
|
||||
- **Zwei Schichten:** Grundschicht und 2. Schicht (die Hülle über der Figur für Haare, Jacken, Ärmel …) getrennt bemalen; 2. Schicht ein-/ausblenden, Grundschicht darunter durchscheinen lassen, 2. Schicht leeren
|
||||
- **2D oder 3D:** auf der aufgeklappten Vorlage oder direkt auf der drehbaren Figur malen – die andere Ansicht läuft als Live-Vorschau mit und ist ebenfalls bemalbar
|
||||
- **Werkzeuge:** Stift, Radierer (nur 2. Schicht), Pipette (auch Alt+Klick), Füllen, Farbwähler mit zuletzt benutzten Farben
|
||||
- **Spiegeln:** malt gleichzeitig auf der anderen Körperseite
|
||||
- Rückgängig/Wiederholen, klassische oder schmale Arme, alte 64×32-Skins werden automatisch umgewandelt
|
||||
- Neu beginnen, aktuellen Skin laden oder eine PNG-Datei öffnen
|
||||
- **Als Skin übernehmen** (direkt hochladen) oder **Als PNG speichern…**
|
||||
|
||||
| Taste | Wirkung |
|
||||
|---|---|
|
||||
| B / E / I / G | Stift / Radierer / Pipette / Füllen |
|
||||
| M | Spiegeln ein/aus |
|
||||
| L | Schicht wechseln |
|
||||
| Strg+Z / Strg+Y | Rückgängig / Wiederholen |
|
||||
| 0 | Umhang: Ansicht einpassen |
|
||||
|
||||
### Umhänge
|
||||
|
||||
Im Skin-Fenster (**Kontoname oben rechts → Skin ändern…**) gibt es den Bereich **Umhang**:
|
||||
|
||||
- **Offizieller Umhang:** zeigt alle Umhänge, die dein Konto bei Mojang besitzt (z. B. Migrator, Pan …) – auswählen oder „Kein Umhang“. Das gilt wie der Skin überall, auch außerhalb von AeroMC.
|
||||
- **AeroMC-Umhang:** dein eigener Umhang, auch in HD. **Gestalten…** öffnet den Skin-Editor im Modus **Umhang**, **Entfernen** löscht ihn wieder.
|
||||
|
||||
Im Editor-Modus **Umhang**:
|
||||
|
||||
- Größe wählen: Normal (64×32) oder HD 128×64, 256×128, 512×256, 1024×512
|
||||
- Alle Werkzeuge wie beim Skin, dazu Pinselgröße (1–16 Pixel); Spiegeln malt linke und rechte Kante gleichzeitig
|
||||
- Mausrad zoomt, rechte oder mittlere Maustaste verschiebt die Ansicht, **0** passt sie wieder ein
|
||||
- In der 3D-Ansicht direkt auf dem Umhang an der Figur malen; **Als Elytra zeigen** zeigt dieselbe Datei als Flügel
|
||||
- **Bild einsetzen…** setzt ein beliebiges Bild (PNG, JPG …) auf die Außenseite
|
||||
- **Aktuellen Umhang laden**, PNG öffnen oder speichern, **Als Umhang übernehmen** lädt ihn hoch
|
||||
|
||||
**Wer sieht den AeroMC-Umhang?**
|
||||
|
||||
| Spieler | sieht den AeroMC-Umhang? |
|
||||
|---|---|
|
||||
| Spielt über AeroMC (Optionen → Spiel → Umhänge eingeschaltet) | Ja – in jeder Minecraft-Version und mit jedem Loader, ohne Mod |
|
||||
| Anderer Launcher mit Fabric oder Quilt und dem Mod **„AeroMC Umhänge“** | Ja – ein Jar für Minecraft 1.14 bis 26.x, nur auf dem Client nötig |
|
||||
| Anderer Launcher ohne den Mod (auch mit OptiFine) | Nein – sieht weiter den offiziellen Mojang-Umhang |
|
||||
|
||||
Der AeroMC-Umhang hat Vorrang vor dem offiziellen Umhang. Benötigt das Plugin „AeroMC Freunde“ 1.5.0 auf dem Server.
|
||||
|
||||
## Backup und Wiederherstellung
|
||||
|
||||
Unter **Optionen → Backup**:
|
||||
|
||||
- Manuelles globales Backup (alle Instanzen + Launcher-Einstellungen)
|
||||
- Automatische Sicherungen (täglich oder wöchentlich, einstellbare Anzahl)
|
||||
- Optional zusätzlich vor jedem Spielstart sichern
|
||||
- Wiederherstellung aus vorhandenem Backup
|
||||
|
||||
Speicherort: `Dokumente\AeroMC Launcher\Backups`.
|
||||
|
||||
**Welten einzeln:** In **Welten verwalten** sichert „Sichern“ eine einzelne Welt als ZIP. Zusätzlich sichert AeroMC nach jeder Spielsitzung automatisch die zuletzt gespielte Welt; pro Welt bleiben die letzten 5 Stände erhalten (`Dokumente\AeroMC Launcher\Welten-Sicherungen\<Instanz>\Automatisch`). Abschaltbar unter **Optionen → Backup → Welten**.
|
||||
|
||||
**Wichtig:** Konten und Tokens werden nicht mitgesichert. Beim Wiederherstellen werden aktuelle Instanzen und Launcher-Einstellungen durch den Backup-Inhalt ersetzt.
|
||||
|
||||
## Einstellungen
|
||||
|
||||
### Allgemein
|
||||
- Instanzen sortieren (nach letztem Start / nach Name)
|
||||
- Instanz-Verzeichnis
|
||||
- Bereiche: Bedrock-Bereich anzeigen
|
||||
- Windows-Benachrichtigung bei fertigem Hintergrund-Download/-Backup (nur wenn das Fenster nicht im Vordergrund ist)
|
||||
- Freundesystem ein-/ausschalten, eigenen Server für Freunde sichtbar oder verborgen
|
||||
- Autostart: mit Windows starten, im Infobereich statt als Fenster – solange aktiv, versteckt „Schließen“ (X) den Launcher nur. Das Symbol im Infobereich gibt es immer; per Rechtsklick startest du dort Instanzen.
|
||||
|
||||
### Darstellung
|
||||
- Design: Klassisch Dunkel, Klassisch Hell, AeroMC, Nachtlila oder Minecraft – mit Vorschau-Kacheln, gilt nach dem Speichern in allen Fenstern
|
||||
- Block-Symbole räumlich (3D)
|
||||
- Animationen:
|
||||
- **Startanimation**: Beim Öffnen dreht sich das AeroMC-Logo, danach baut sich das Fenster von der Mitte aus auf (nicht beim Start im Infobereich)
|
||||
- **Bewegte Oberfläche**: Die Skin-Figur im Hintergrund atmet und schaut dem Mauszeiger nach, Fenster wachsen aus dem angeklickten Knopf, Instanzen blenden beim Öffnen ein und heben sich beim Darüberfahren, Designs blenden weich über
|
||||
- Beides einzeln abschaltbar; ist in Windows „Animationen anzeigen“ aus, bleibt die Oberfläche ruhig
|
||||
- Skin-Hintergrund: Quelle, Pose, Armform (automatisch vom Konto, normal oder schmal), Deckkraft, optional automatischer Wechsel zwischen mehreren Konten
|
||||
|
||||
### Spiel
|
||||
- Standard-RAM (min./max.)
|
||||
- Umhänge: AeroMC-Umhänge im Spiel anzeigen (gilt ab dem nächsten Start)
|
||||
- Fenstergröße bzw. maximiert starten
|
||||
- Mods: vor jedem Start automatisch nach Updates suchen und installieren
|
||||
|
||||
### Start & Java
|
||||
- Globale Java-Auswahl und Erkennung
|
||||
- Fehlende Java-Version automatisch herunterladen
|
||||
- Konsole: während des Spiels, nach Beenden schließen, bei Absturz anzeigen, Historie-Limit
|
||||
- Globale Pre-Launch- / Post-Exit-Befehle
|
||||
- Optionaler Wrapper-Befehl
|
||||
|
||||
### Netzwerk
|
||||
- HTTP-Proxy (Host, Port, optional Benutzer/Passwort)
|
||||
- CurseForge: eigener API-Key und/oder Weiterleitungs-Adresse inkl. Token
|
||||
|
||||
Ohne eigenen Key und ohne Weiterleitung funktionieren Modrinth-Inhalte weiterhin. Für CurseForge-Downloads wird einer der beiden Wege benötigt.
|
||||
|
||||
### Konten
|
||||
- Mehrere Microsoft-Konten verwalten, aktiv setzen, entfernen
|
||||
|
||||
### Backup
|
||||
- Manuell, Zeitplan und Wiederherstellung
|
||||
- Welten: nach jeder Spielsitzung die zuletzt gespielte Welt sichern (Standard: an)
|
||||
|
||||
### Info
|
||||
- Version, Autor, Links und Lizenz
|
||||
- Update-Suche läuft sofort beim Start, das Ergebnis steht direkt hier (grün, wenn alles aktuell ist); dazu jederzeit manuell per Knopf. Updates installieren sich ohne Rückfragen und starten AeroMC danach neu.
|
||||
|
||||
## Bedrock
|
||||
|
||||
Der Bedrock-Bereich startet die vorhandene Windows-Store-Version und erleichtert den Zugriff auf Welten, Pakete, Add-ons und Ordner. Zusätzliche Add-on-Dateien lassen sich direkt über AeroMC installieren. AeroMC installiert Bedrock selbst (die App aus dem Microsoft Store) nicht. Fehlt die Installation, wird der Bereich automatisch ausgeblendet.
|
||||
|
||||
## Absturz-Assistent
|
||||
|
||||
Stürzt Minecraft ab – oder scheitert es schon beim Start –, wertet AeroMC Absturzbericht, Spielprotokoll und Konsolenausgabe aus und erklärt unter „Das ist wahrscheinlich passiert“ in einfachen Worten, was los ist:
|
||||
|
||||
- fehlende Abhängigkeiten (Fabric, Quilt, Forge, NeoForge) – mit Knopf zum Nachinstallieren
|
||||
- Mods für eine andere Minecraft-Version, unverträgliche oder doppelte Mods – mit Knopf zum Abschalten
|
||||
- falsche Java-Version – passendes Java wird automatisch gewählt und bei Bedarf geladen
|
||||
- zu wenig oder zu viel Arbeitsspeicher – mit Knopf zum Anpassen
|
||||
- Grafiktreiber-Probleme, OptiFine zusammen mit Sodium
|
||||
|
||||
Nach einer Lösung startet „Erneut starten“ die Instanz direkt. Bleibt die Ursache unklar, schickt **„An M_Viper senden“** Bericht und Mod-Liste an den Entwickler – vorher zeigt AeroMC genau, was gesendet wird. Windows-Benutzername und Anmeldedaten werden entfernt; ohne Häkchen wird anonym gesendet.
|
||||
|
||||
## Konsole, Logs und Crash-Reports
|
||||
|
||||
- Konsole über die obere Leiste oder bei Absturz
|
||||
- Pro Instanz: gespeicherte Logdateien und Crash-Reports einsehen, kopieren und Ordner öffnen
|
||||
|
||||
## Datenort
|
||||
|
||||
- Nutzerdaten (Instanzen, Konten-Verknüpfung, Einstellungen): `%APPDATA%\aeromc`
|
||||
- Aufzeichnungen und Clips: `Videos\AeroMC Launcher`
|
||||
- Backups: `Dokumente\AeroMC Launcher\Backups`
|
||||
- Welt-Sicherungen: `Dokumente\AeroMC Launcher\Welten-Sicherungen`
|
||||
|
||||
## Links
|
||||
|
||||
- Webseite: https://m-viper.de
|
||||
- Launcher-Webseite: https://aeromc.viper.ipv64.net
|
||||
- Autor: M_Viper
|
||||
|
||||
## Rechtliches
|
||||
|
||||
AeroMC ist kein offizielles Produkt von Mojang oder Microsoft.
|
||||
Minecraft ist eine Marke von Mojang AB.
|
||||
|
||||
### Urheberrecht & Nutzungsbedingungen
|
||||
|
||||
AeroMC ist urheberrechtlich geschützt. © M_Viper – Alle Rechte vorbehalten.
|
||||
|
||||
- ❌ Die Weitergabe dieses Launchers an Dritte ist strengstens untersagt
|
||||
- ❌ Das Verändern, Dekompilieren oder Bearbeiten des Codes ist nicht erlaubt
|
||||
- ❌ Eine Veröffentlichung – auch in veränderter Form – ist verboten
|
||||
- ❌ Der Verkauf oder die kommerzielle Nutzung ist nicht gestattet
|
||||
- ✅ Die Nutzung ist ausschließlich für den privaten, nicht-kommerziellen Gebrauch erlaubt
|
||||
|
||||
Wer gegen diese Bedingungen verstößt, macht sich strafbar und kann rechtlich belangt werden.
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package de.aeromc.capes;
|
||||
|
||||
import java.lang.instrument.Instrumentation;
|
||||
|
||||
/**
|
||||
* AeroMC-Agent.
|
||||
*
|
||||
* Wird vom Launcher per -javaagent an Minecraft angehängt (jede Version, jeder
|
||||
* Loader) und kann zwei Dinge, einzeln zuschaltbar:
|
||||
*
|
||||
* Umhänge: klinkt sich in Mojangs authlib ein – über sie lädt Minecraft für
|
||||
* jeden Spieler Skin und Umhang – und setzt dort den AeroMC-Umhang
|
||||
* des Spielers ein, falls er einen hat. Alles Weitere (Herunterladen,
|
||||
* Zwischenspeichern, Anzeigen, auch HD) erledigt Minecraft selbst.
|
||||
* Spieldesign: AeroMC-Ladebildschirm, Fenstertitel und -symbol (siehe Design).
|
||||
*
|
||||
* Argument, Teile durch "|" getrennt:
|
||||
* <Basisadresse des Umhang-Servers> (…/wp-json/aeromc/v1)
|
||||
* capes=0 Umhänge aus
|
||||
* design=<Ordner> Spieldesign an, Grafiken aus diesem Ordner
|
||||
* Ein Argument nur mit Adresse (ältere Launcher) heißt: nur Umhänge.
|
||||
*/
|
||||
public final class Agent {
|
||||
/** Unter diesem Schlüssel liegt der Nachschlage-Dienst in den System-Properties. */
|
||||
public static final String SCHLUESSEL = "aeromc.capes.hook";
|
||||
|
||||
private Agent() {}
|
||||
|
||||
public static void premain(String argument, Instrumentation inst) {
|
||||
starte(argument, inst);
|
||||
}
|
||||
|
||||
public static void agentmain(String argument, Instrumentation inst) {
|
||||
starte(argument, inst);
|
||||
}
|
||||
|
||||
private static void starte(String argument, Instrumentation inst) {
|
||||
String basis = Nachschlagen.STANDARD_BASIS;
|
||||
boolean umhaenge = true;
|
||||
String design = null;
|
||||
String mods = null;
|
||||
if (argument != null) {
|
||||
for (String teil : argument.split("\\|")) {
|
||||
if (teil.startsWith("http")) basis = teil;
|
||||
else if (teil.equals("capes=0")) umhaenge = false;
|
||||
else if (teil.startsWith("design=") && teil.length() > 7) design = teil.substring(7);
|
||||
else if (teil.startsWith("mods=") && teil.length() > 5) mods = teil.substring(5);
|
||||
}
|
||||
}
|
||||
if (mods != null) {
|
||||
try {
|
||||
Design.installiereMods(mods, inst);
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] Mod-Schalter konnte nicht starten: " + t);
|
||||
}
|
||||
}
|
||||
if (umhaenge) {
|
||||
try {
|
||||
// Die eingefügten Befehle in authlib holen sich den Dienst von hier – nur
|
||||
// über JDK-Typen, damit es mit jedem Classloader und Modulsystem klappt.
|
||||
System.getProperties().put(SCHLUESSEL, new Nachschlagen(basis));
|
||||
inst.addTransformer(new Umbau());
|
||||
System.out.println("[AeroMC] Umhang-Agent aktiv");
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] Umhang-Agent konnte nicht starten: " + t);
|
||||
}
|
||||
}
|
||||
if (design != null) {
|
||||
try {
|
||||
Design.installiere(design, inst);
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] Spieldesign konnte nicht starten: " + t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package de.aeromc.capes;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* AeroMC-Look im Spiel (Minecraft 1.14 … 26.x):
|
||||
*
|
||||
* – Ladebildschirm: AeroMC-Nachtblau statt Mojang-Rot, hellblauer Balken und
|
||||
* AeroMC-Logo statt Mojang-Logo. Das Logo lädt Minecraft am Ressourcenpaket
|
||||
* vorbei direkt aus dem Spiel – deshalb tauscht der Agent den Datenstrom aus.
|
||||
* – Fenster: Titel "AeroMC - Minecraft …" und AeroMC-Symbol.
|
||||
*
|
||||
* Titel-Logo, Untertitel, Sprüche und Knöpfe kommen über ein Ressourcenpaket,
|
||||
* das der Launcher anlegt (src/spieldesign.js).
|
||||
*
|
||||
* Die umgebauten Stellen im Spiel holen sich ihre Helfer – wie beim Umhang –
|
||||
* über die System-Properties und nur über JDK-Typen (java.util.function.Function),
|
||||
* damit es mit jedem Classloader klappt (Vanilla, Fabric, Forge, NeoForge).
|
||||
*/
|
||||
final class Design {
|
||||
static final String LOGO = "aeromc.design.logo";
|
||||
static final String LOGO_ALT = "aeromc.design.logoAlt";
|
||||
static final String TITEL = "aeromc.design.titel";
|
||||
static final String ICON = "aeromc.design.icon";
|
||||
static final String MODS = "aeromc.mods";
|
||||
|
||||
/** Hintergrund des Ladebildschirms (wie das AeroMC-Design im Launcher). */
|
||||
static final int HINTERGRUND = 0x0A0E1A;
|
||||
/** Fortschrittsbalken. */
|
||||
static final int BALKEN = 0x4FD2FF;
|
||||
/** Breite des Balkens als Anteil der Bildschirmbreite (das Logo ist jetzt bildschirmbreit). */
|
||||
static final float BALKEN_ANTEIL = 0.56f;
|
||||
|
||||
private static final int[] ICON_GROESSEN = {16, 32, 48, 64, 128, 256};
|
||||
/** Pixelspeicher der Symbole festhalten, bis GLFW sie übernommen hat. */
|
||||
private static final List<ByteBuffer> FESTHALTEN = new ArrayList<>();
|
||||
|
||||
private Design() {}
|
||||
|
||||
static void installiere(String ordner, Instrumentation inst) {
|
||||
File dir = new File(ordner);
|
||||
Properties p = System.getProperties();
|
||||
p.put(LOGO, strom(new File(dir, "ladelogo.png")));
|
||||
p.put(LOGO_ALT, strom(new File(dir, "ladelogo_alt.png")));
|
||||
p.put(TITEL, (Function<Object, Object>) Design::titel);
|
||||
p.put(ICON, (Function<Object, Object>) bilder -> symbol(dir, bilder));
|
||||
inst.addTransformer(new DesignUmbau());
|
||||
System.out.println("[AeroMC] Spieldesign aktiv");
|
||||
}
|
||||
|
||||
/**
|
||||
* Mod-Schalter: Klick auf „Mods verwalten“ im Pausemenü (Adresse aeromc://mods)
|
||||
* meldet sich beim Launcher, der dann das Mod-Fenster über dem Spiel öffnet.
|
||||
* Rückgabe TRUE = erledigt (Minecraft öffnet keinen Link), sonst FALSE.
|
||||
*/
|
||||
static void installiereMods(String adresse, Instrumentation inst) {
|
||||
System.getProperties().put(MODS, (Function<Object, Object>) link -> {
|
||||
if (link == null || !link.toString().startsWith("aeromc:")) return Boolean.FALSE;
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
java.net.HttpURLConnection c = (java.net.HttpURLConnection) new java.net.URL(adresse).openConnection();
|
||||
c.setConnectTimeout(2000);
|
||||
c.setReadTimeout(4000);
|
||||
c.getResponseCode();
|
||||
c.disconnect();
|
||||
} catch (Throwable e) {
|
||||
System.out.println("[AeroMC] Launcher fuer den Mod-Schalter nicht erreichbar: " + e);
|
||||
}
|
||||
}, "AeroMC-Mod-Schalter");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
return Boolean.TRUE;
|
||||
});
|
||||
inst.addTransformer(new ModsUmbau());
|
||||
System.out.println("[AeroMC] Mod-Schalter aktiv");
|
||||
}
|
||||
|
||||
/** Ersetzt den Datenstrom des Mojang-Logos durch das AeroMC-Logo (bei Fehlern bleibt das Original). */
|
||||
private static Function<Object, Object> strom(File datei) {
|
||||
return original -> {
|
||||
try {
|
||||
return new ByteArrayInputStream(Files.readAllBytes(datei.toPath()));
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] Ladelogo nicht lesbar: " + t);
|
||||
return original;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Object titel(Object original) {
|
||||
if (original == null) return null;
|
||||
String s = original.toString();
|
||||
return s.startsWith("AeroMC") ? original : "AeroMC - " + s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut aus den mitgelieferten Rohdaten (icon_N.rgba) einen neuen
|
||||
* GLFWImage.Buffer. LWJGL wird über den Classloader des übergebenen Buffers
|
||||
* angesprochen – der Agent selbst kennt LWJGL nicht.
|
||||
*/
|
||||
private static Object symbol(File dir, Object original) {
|
||||
if (original == null) return null;
|
||||
try {
|
||||
List<int[]> groessen = new ArrayList<>();
|
||||
List<byte[]> daten = new ArrayList<>();
|
||||
for (int n : ICON_GROESSEN) {
|
||||
File f = new File(dir, "icon_" + n + ".rgba");
|
||||
if (!f.isFile()) continue;
|
||||
byte[] b = Files.readAllBytes(f.toPath());
|
||||
if (b.length != n * n * 4) continue;
|
||||
groessen.add(new int[] {n, n});
|
||||
daten.add(b);
|
||||
}
|
||||
if (daten.isEmpty()) return original;
|
||||
ClassLoader cl = original.getClass().getClassLoader();
|
||||
Class<?> bild = Class.forName("org.lwjgl.glfw.GLFWImage", true, cl);
|
||||
Object puffer = bild.getMethod("malloc", int.class).invoke(null, daten.size());
|
||||
Method hole = puffer.getClass().getMethod("get", int.class);
|
||||
Method setze = bild.getMethod("set", int.class, int.class, ByteBuffer.class);
|
||||
for (int i = 0; i < daten.size(); i++) {
|
||||
ByteBuffer px = ByteBuffer.allocateDirect(daten.get(i).length);
|
||||
px.put(daten.get(i)).flip();
|
||||
synchronized (FESTHALTEN) { FESTHALTEN.add(px); }
|
||||
setze.invoke(hole.invoke(puffer, i), groessen.get(i)[0], groessen.get(i)[1], px);
|
||||
}
|
||||
return puffer;
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] Fenstersymbol nicht gesetzt: " + t);
|
||||
return original;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
package de.aeromc.capes;
|
||||
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.ProtectionDomain;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.InsnList;
|
||||
import org.objectweb.asm.tree.InsnNode;
|
||||
import org.objectweb.asm.tree.IntInsnNode;
|
||||
import org.objectweb.asm.tree.LdcInsnNode;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
import org.objectweb.asm.tree.TypeInsnNode;
|
||||
import org.objectweb.asm.tree.VarInsnNode;
|
||||
|
||||
/**
|
||||
* Baut beim Laden um (siehe Design):
|
||||
*
|
||||
* 1. Ladebildschirm – erkannt am Text "textures/gui/title/mojangstudios.png"
|
||||
* (ab 1.16) bzw. "…/mojang.png" (1.14/1.15), denn die Klassennamen sind je
|
||||
* Version und Loader anders (verschleiert, Intermediary, SRG, Mojang-Namen):
|
||||
* ab 1.16: color(255, 239, 50, 61) -> AeroMC-Nachtblau
|
||||
* color(alpha, 255, 255, 255) -> Balkenfarbe
|
||||
* 1.14/1.15: weiße Füllung (0xFFFFFF / -1) -> Nachtblau,
|
||||
* Balken-Rot (226, 40, 55) -> Balkenfarbe
|
||||
* 2. Logo-Textur – die innere Klasse des Ladebildschirms ("…$…") liest das
|
||||
* Logo mit NativeImage.read(InputStream); davor wird der Strom getauscht.
|
||||
* 3. LWJGL-GLFW – Fenstertitel (glfwCreateWindow/glfwSetWindowTitle) und
|
||||
* Fenstersymbol (glfwSetWindowIcon).
|
||||
*
|
||||
* Eingefügt bzw. ersetzt wird nur geradliniger Code ohne Sprünge mit gleicher
|
||||
* Stapelwirkung – die vorhandenen Stack-Map-Frames bleiben gültig.
|
||||
*/
|
||||
final class DesignUmbau implements ClassFileTransformer {
|
||||
private static final byte[] MERKMAL = "textures/gui/title/mojang".getBytes(StandardCharsets.UTF_8);
|
||||
private static final String LOGO_NEU = "textures/gui/title/mojangstudios.png";
|
||||
private static final String LOGO_ALT = "textures/gui/title/mojang.png";
|
||||
private static final byte[] TITEL_MERKMAL = "textures/gui/title/minecraft.png".getBytes(StandardCharsets.UTF_8);
|
||||
private static final int KEIN_WERT = Integer.MIN_VALUE;
|
||||
|
||||
/** Name der Ladebildschirm-Klasse, sobald sie gesehen wurde. */
|
||||
private volatile String ladeKlasse;
|
||||
private volatile boolean ladeAlt;
|
||||
|
||||
@Override
|
||||
public byte[] transform(ClassLoader loader, String name, Class<?> umdefiniert, ProtectionDomain pd, byte[] klasse) {
|
||||
if (name == null || umdefiniert != null) return null;
|
||||
try {
|
||||
if ("org/lwjgl/glfw/GLFW".equals(name)) return glfw(klasse);
|
||||
if (name.startsWith("java/") || name.startsWith("jdk/") || name.startsWith("sun/")
|
||||
|| name.startsWith("de/aeromc/") || name.startsWith("org/lwjgl/")) return null;
|
||||
String lk = ladeKlasse;
|
||||
if (lk != null && name.startsWith(lk + "$")) return logoStrom(klasse, ladeAlt);
|
||||
if (enthaelt(klasse, MERKMAL)) return ladeBildschirm(name, klasse);
|
||||
if (enthaelt(klasse, TITEL_MERKMAL)) return titelLogo(name, klasse);
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] Spieldesign: " + name + " nicht angepasst: " + t);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------- 1. Ladebildschirm ----------
|
||||
|
||||
private byte[] ladeBildschirm(String name, byte[] klasse) {
|
||||
ClassNode cn = lies(klasse);
|
||||
boolean neu = hatText(cn, LOGO_NEU), alt = hatText(cn, LOGO_ALT);
|
||||
if (!neu && !alt) return null;
|
||||
ladeKlasse = name;
|
||||
ladeAlt = alt && !neu;
|
||||
int r = Design.HINTERGRUND >> 16 & 255, g = Design.HINTERGRUND >> 8 & 255, b = Design.HINTERGRUND & 255;
|
||||
int br = Design.BALKEN >> 16 & 255, bg = Design.BALKEN >> 8 & 255, bb = Design.BALKEN & 255;
|
||||
int hintergrund = 0, balken = 0, vollbild = 0, balkenBreite = 0;
|
||||
for (MethodNode m : cn.methods) {
|
||||
List<AbstractInsnNode> a = befehle(m);
|
||||
for (int i = 0; i < a.size(); i++) {
|
||||
if (!ladeAlt) {
|
||||
// color(255, 239, 50, 61) – Mojang-Rot
|
||||
if (i + 4 < a.size() && wert(a.get(i)) == 255 && wert(a.get(i + 1)) == 239
|
||||
&& wert(a.get(i + 2)) == 50 && wert(a.get(i + 3)) == 61 && aufruf(a.get(i + 4), "(IIII)I")) {
|
||||
setze(m, a, i + 1, r); setze(m, a, i + 2, g); setze(m, a, i + 3, b);
|
||||
hintergrund++;
|
||||
}
|
||||
// color(alpha, 255, 255, 255) – weißer Balken
|
||||
if (i + 4 < a.size() && a.get(i).getOpcode() == Opcodes.ILOAD && wert(a.get(i + 1)) == 255
|
||||
&& wert(a.get(i + 2)) == 255 && wert(a.get(i + 3)) == 255 && aufruf(a.get(i + 4), "(IIII)I")) {
|
||||
setze(m, a, i + 1, br); setze(m, a, i + 2, bg); setze(m, a, i + 3, bb);
|
||||
balken++;
|
||||
}
|
||||
// Logo-Fläche auf Vollbild: Höhe = min(Breite·0,75; Höhe)·0,25 und Breite = 4·Höhe
|
||||
// wird zu Höhe = min(Breite·9/16; Höhe) und Breite = 16/9·Höhe (Bild im 16:9-Format)
|
||||
AbstractInsnNode n = a.get(i);
|
||||
if (n instanceof LdcInsnNode && ((LdcInsnNode) n).cst instanceof Double) {
|
||||
double d = (Double) ((LdcInsnNode) n).cst;
|
||||
double ersatzWert = d == 0.75 ? 9.0 / 16.0 : d == 0.25 ? 1.0 : d == 4.0 ? 16.0 / 9.0 : -1;
|
||||
if (ersatzWert > 0) {
|
||||
LdcInsnNode e = new LdcInsnNode(ersatzWert);
|
||||
m.instructions.set(n, e);
|
||||
a.set(i, e);
|
||||
vollbild++;
|
||||
}
|
||||
}
|
||||
// Fortschrittsbalken: Minecraft macht ihn so breit wie das Logo – jetzt also
|
||||
// bildschirmbreit; beim Aufruf auf einen Teil der Breite in der Mitte kürzen
|
||||
if (n instanceof MethodInsnNode && ((MethodInsnNode) n).owner.equals(name)
|
||||
&& ((MethodInsnNode) n).desc.endsWith("IIIIF)V")) {
|
||||
m.instructions.insertBefore(n, balkenKuerzen(m));
|
||||
balkenBreite++;
|
||||
}
|
||||
} else {
|
||||
AbstractInsnNode n = a.get(i);
|
||||
// weiße Füllung beim Ein-/Ausblenden: 0xFFFFFF | alpha << 24
|
||||
if (n instanceof LdcInsnNode && Integer.valueOf(0xFFFFFF).equals(((LdcInsnNode) n).cst)) {
|
||||
m.instructions.set(n, ersatz(Design.HINTERGRUND, a, i));
|
||||
hintergrund++;
|
||||
}
|
||||
// fill(0, 0, breite, hoehe, -1) – weißer Hintergrund
|
||||
if (i + 5 < a.size() && n.getOpcode() == Opcodes.ICONST_0 && a.get(i + 1).getOpcode() == Opcodes.ICONST_0
|
||||
&& a.get(i + 2).getOpcode() == Opcodes.ILOAD && a.get(i + 3).getOpcode() == Opcodes.ILOAD
|
||||
&& a.get(i + 4).getOpcode() == Opcodes.ICONST_M1 && aufruf(a.get(i + 5), "(IIIII)V")) {
|
||||
m.instructions.set(a.get(i + 4), ersatz(0xFF000000 | Design.HINTERGRUND, a, i + 4));
|
||||
hintergrund++;
|
||||
}
|
||||
// Balken-Rot (226, 40, 55) als Gleitkommazahlen
|
||||
if (n instanceof LdcInsnNode && ((LdcInsnNode) n).cst instanceof Float) {
|
||||
float f = (Float) ((LdcInsnNode) n).cst;
|
||||
float neuWert = f == 226f ? br : f == 40f ? bg : f == 55f ? bb : -1f;
|
||||
if (neuWert >= 0) {
|
||||
LdcInsnNode e = new LdcInsnNode(neuWert);
|
||||
m.instructions.set(n, e);
|
||||
a.set(i, e);
|
||||
balken++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.println("[AeroMC] Ladebildschirm angepasst (" + (ladeAlt ? "1.14/1.15" : "ab 1.16")
|
||||
+ ", Hintergrund " + hintergrund + ", Balken " + balken + ", Vollbild " + vollbild + ", Balkenbreite " + balkenBreite + ")");
|
||||
return schreibe(cn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vor dem Aufruf drawProgressBar(…, x1, y1, x2, y2, f): die vier Zahlen und f in
|
||||
* neue lokale Variablen legen, x1/x2 um die Mitte auf BALKEN_ANTEIL der Breite
|
||||
* zusammenziehen und alles wieder auf den Stapel legen.
|
||||
*/
|
||||
private static InsnList balkenKuerzen(MethodNode m) {
|
||||
int b = m.maxLocals;
|
||||
m.maxLocals += 7;
|
||||
int x1 = b, y1 = b + 1, x2 = b + 2, y2 = b + 3, f = b + 4, mitte = b + 5, halb = b + 6;
|
||||
InsnList l = new InsnList();
|
||||
l.add(new VarInsnNode(Opcodes.FSTORE, f));
|
||||
l.add(new VarInsnNode(Opcodes.ISTORE, y2));
|
||||
l.add(new VarInsnNode(Opcodes.ISTORE, x2));
|
||||
l.add(new VarInsnNode(Opcodes.ISTORE, y1));
|
||||
l.add(new VarInsnNode(Opcodes.ISTORE, x1));
|
||||
// mitte = (x1 + x2) / 2
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, x1));
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, x2));
|
||||
l.add(new InsnNode(Opcodes.IADD));
|
||||
l.add(new InsnNode(Opcodes.ICONST_2));
|
||||
l.add(new InsnNode(Opcodes.IDIV));
|
||||
l.add(new VarInsnNode(Opcodes.ISTORE, mitte));
|
||||
// halb = (int) ((x2 - x1) * anteil / 2)
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, x2));
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, x1));
|
||||
l.add(new InsnNode(Opcodes.ISUB));
|
||||
l.add(new InsnNode(Opcodes.I2F));
|
||||
l.add(new LdcInsnNode(Design.BALKEN_ANTEIL / 2f));
|
||||
l.add(new InsnNode(Opcodes.FMUL));
|
||||
l.add(new InsnNode(Opcodes.F2I));
|
||||
l.add(new VarInsnNode(Opcodes.ISTORE, halb));
|
||||
// (mitte - halb, y1, mitte + halb, y2, f)
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, mitte));
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, halb));
|
||||
l.add(new InsnNode(Opcodes.ISUB));
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, y1));
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, mitte));
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, halb));
|
||||
l.add(new InsnNode(Opcodes.IADD));
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, y2));
|
||||
l.add(new VarInsnNode(Opcodes.FLOAD, f));
|
||||
return l;
|
||||
}
|
||||
|
||||
// ---------- 1b. Titel-Logo im Hauptmenü ----------
|
||||
// Minecraft zeichnet vom Logo nur einen 44 Punkte hohen Streifen – zu knapp für
|
||||
// Pyramide, großen Schriftzug und Untertitel. Hier wird er auf 64 Punkte erweitert:
|
||||
// bis 1.19.4: blit(u 0, v 0, 155, 44) + blit(u 0, v 45, 155, 44) -> Höhe 64, zweites Stück ab v 65
|
||||
// ab 1.20: blit(…, 256, 44, Textur 256, 64) -> Höhe 64
|
||||
private byte[] titelLogo(String name, byte[] klasse) {
|
||||
ClassNode cn = lies(klasse);
|
||||
int n = 0;
|
||||
for (MethodNode m : cn.methods) {
|
||||
List<AbstractInsnNode> a = befehle(m);
|
||||
for (int i = 0; i + 4 < a.size(); i++) {
|
||||
int w0 = wert(a.get(i)), w1 = wert(a.get(i + 1)), w2 = wert(a.get(i + 2)), w3 = wert(a.get(i + 3));
|
||||
boolean aufrufDanach = a.get(i + 4) instanceof MethodInsnNode;
|
||||
if (w0 == 0 && w1 == 0 && w2 == 155 && w3 == 44 && aufrufDanach) {
|
||||
setze(m, a, i + 3, 64); n++;
|
||||
} else if (w0 == 0 && w1 == 45 && w2 == 155 && w3 == 44 && aufrufDanach) {
|
||||
setze(m, a, i + 1, 65); setze(m, a, i + 3, 64); n++;
|
||||
} else if (w0 == 256 && w1 == 44 && w2 == 256 && w3 == 64) {
|
||||
setze(m, a, i + 1, 64); n++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (n == 0) return null;
|
||||
System.out.println("[AeroMC] Titel-Logo vergroessert (" + n + ")");
|
||||
return schreibe(cn);
|
||||
}
|
||||
|
||||
// ---------- 2. Logo-Textur ----------
|
||||
|
||||
private byte[] logoStrom(byte[] klasse, boolean alt) {
|
||||
ClassNode cn = lies(klasse);
|
||||
int n = 0;
|
||||
for (MethodNode m : cn.methods) {
|
||||
for (AbstractInsnNode i : befehle(m)) {
|
||||
if (i.getOpcode() != Opcodes.INVOKESTATIC) continue;
|
||||
MethodInsnNode mi = (MethodInsnNode) i;
|
||||
if (!mi.desc.startsWith("(Ljava/io/InputStream;)L") || mi.owner.startsWith("java/")) continue;
|
||||
// Stapel: [strom] -> [strom, helfer] -> [helfer, strom] -> apply -> [neuer strom]
|
||||
InsnList neu = new InsnList();
|
||||
helfer(neu, alt ? Design.LOGO_ALT : Design.LOGO);
|
||||
neu.add(new InsnNode(Opcodes.SWAP));
|
||||
neu.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, "java/util/function/Function", "apply",
|
||||
"(Ljava/lang/Object;)Ljava/lang/Object;", true));
|
||||
neu.add(new TypeInsnNode(Opcodes.CHECKCAST, "java/io/InputStream"));
|
||||
m.instructions.insertBefore(i, neu);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
if (n == 0) return null;
|
||||
System.out.println("[AeroMC] Ladelogo ersetzt");
|
||||
return schreibe(cn);
|
||||
}
|
||||
|
||||
// ---------- 3. Fenstertitel und -symbol ----------
|
||||
|
||||
private byte[] glfw(byte[] klasse) {
|
||||
ClassNode cn = lies(klasse);
|
||||
int n = 0;
|
||||
for (MethodNode m : cn.methods) {
|
||||
String schluessel = null, typ = null;
|
||||
if ("glfwCreateWindow".equals(m.name) && "(IILjava/lang/CharSequence;JJ)J".equals(m.desc)) {
|
||||
schluessel = Design.TITEL; typ = "java/lang/CharSequence";
|
||||
} else if ("glfwSetWindowTitle".equals(m.name) && "(JLjava/lang/CharSequence;)V".equals(m.desc)) {
|
||||
schluessel = Design.TITEL; typ = "java/lang/CharSequence";
|
||||
} else if ("glfwSetWindowIcon".equals(m.name) && "(JLorg/lwjgl/glfw/GLFWImage$Buffer;)V".equals(m.desc)) {
|
||||
schluessel = Design.ICON; typ = "org/lwjgl/glfw/GLFWImage$Buffer";
|
||||
}
|
||||
if (schluessel == null) continue;
|
||||
// am Anfang: argument = helfer.apply(argument) (Argument liegt in beiden Fällen in Slot 2)
|
||||
InsnList neu = new InsnList();
|
||||
helfer(neu, schluessel);
|
||||
neu.add(new VarInsnNode(Opcodes.ALOAD, 2));
|
||||
neu.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, "java/util/function/Function", "apply",
|
||||
"(Ljava/lang/Object;)Ljava/lang/Object;", true));
|
||||
neu.add(new TypeInsnNode(Opcodes.CHECKCAST, typ));
|
||||
neu.add(new VarInsnNode(Opcodes.ASTORE, 2));
|
||||
m.instructions.insert(neu);
|
||||
n++;
|
||||
}
|
||||
if (n == 0) return null;
|
||||
System.out.println("[AeroMC] Fenstertitel und -symbol vorbereitet (" + n + ")");
|
||||
return schreibe(cn);
|
||||
}
|
||||
|
||||
// ---------- Hilfen ----------
|
||||
|
||||
/** legt (Function) System.getProperties().get(schluessel) auf den Stapel */
|
||||
private static void helfer(InsnList l, String schluessel) {
|
||||
l.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System", "getProperties", "()Ljava/util/Properties;", false));
|
||||
l.add(new LdcInsnNode(schluessel));
|
||||
l.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/util/Properties", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", false));
|
||||
l.add(new TypeInsnNode(Opcodes.CHECKCAST, "java/util/function/Function"));
|
||||
}
|
||||
|
||||
private static ClassNode lies(byte[] klasse) {
|
||||
ClassNode cn = new ClassNode();
|
||||
new ClassReader(klasse).accept(cn, 0);
|
||||
return cn;
|
||||
}
|
||||
|
||||
private static byte[] schreibe(ClassNode cn) {
|
||||
ClassWriter w = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
cn.accept(w);
|
||||
return w.toByteArray();
|
||||
}
|
||||
|
||||
/** echte Befehle eines Methodenrumpfs (ohne Labels, Zeilennummern, Frames) */
|
||||
private static List<AbstractInsnNode> befehle(MethodNode m) {
|
||||
List<AbstractInsnNode> l = new ArrayList<>();
|
||||
for (AbstractInsnNode i = m.instructions.getFirst(); i != null; i = i.getNext()) {
|
||||
if (i.getOpcode() >= 0) l.add(i);
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
private static boolean hatText(ClassNode cn, String text) {
|
||||
for (MethodNode m : cn.methods) {
|
||||
for (AbstractInsnNode i : befehle(m)) {
|
||||
if (i instanceof LdcInsnNode && text.equals(((LdcInsnNode) i).cst)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Zahlenwert eines Konstanten-Befehls, sonst KEIN_WERT */
|
||||
private static int wert(AbstractInsnNode i) {
|
||||
int op = i.getOpcode();
|
||||
if (op >= Opcodes.ICONST_M1 && op <= Opcodes.ICONST_5) return op - Opcodes.ICONST_0;
|
||||
if (op == Opcodes.BIPUSH || op == Opcodes.SIPUSH) return ((IntInsnNode) i).operand;
|
||||
if (i instanceof LdcInsnNode && ((LdcInsnNode) i).cst instanceof Integer) return (Integer) ((LdcInsnNode) i).cst;
|
||||
return KEIN_WERT;
|
||||
}
|
||||
|
||||
private static boolean aufruf(AbstractInsnNode i, String desc) {
|
||||
return (i.getOpcode() == Opcodes.INVOKESTATIC || i.getOpcode() == Opcodes.INVOKEVIRTUAL)
|
||||
&& desc.equals(((MethodInsnNode) i).desc);
|
||||
}
|
||||
|
||||
private static void setze(MethodNode m, List<AbstractInsnNode> a, int index, int v) {
|
||||
m.instructions.set(a.get(index), ersatz(v, a, index));
|
||||
}
|
||||
|
||||
/** neuer Konstanten-Befehl; ersetzt auch den Eintrag in der Befehlsliste */
|
||||
private static AbstractInsnNode ersatz(int v, List<AbstractInsnNode> a, int index) {
|
||||
AbstractInsnNode n = v >= -32768 && v <= 32767 ? new IntInsnNode(Opcodes.SIPUSH, v) : new LdcInsnNode(v);
|
||||
a.set(index, n);
|
||||
return n;
|
||||
}
|
||||
|
||||
private static boolean enthaelt(byte[] daten, byte[] muster) {
|
||||
outer:
|
||||
for (int i = 0, ende = daten.length - muster.length; i <= ende; i++) {
|
||||
for (int k = 0; k < muster.length; k++) {
|
||||
if (daten[i + k] != muster[k]) continue outer;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package de.aeromc.capes;
|
||||
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.ProtectionDomain;
|
||||
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.FieldInsnNode;
|
||||
import org.objectweb.asm.tree.FrameNode;
|
||||
import org.objectweb.asm.tree.InsnList;
|
||||
import org.objectweb.asm.tree.InsnNode;
|
||||
import org.objectweb.asm.tree.JumpInsnNode;
|
||||
import org.objectweb.asm.tree.LabelNode;
|
||||
import org.objectweb.asm.tree.LdcInsnNode;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
import org.objectweb.asm.tree.TypeInsnNode;
|
||||
import org.objectweb.asm.tree.VarInsnNode;
|
||||
|
||||
/**
|
||||
* Mod-Schalter im Pausemenü (Minecraft 1.14 … 26.x, jeder Loader):
|
||||
* Der Knopf „Feedback geben“ wird zum Knopf „Mods verwalten“.
|
||||
*
|
||||
* 1. Seine Adresse (aka.ms/javafeedback bzw. snapshotfeedback) wird zu
|
||||
* "aeromc://mods" – je nach Version steht sie im Pausemenü selbst
|
||||
* (bis 1.20) oder in CommonLinks (ab 1.21).
|
||||
* 2. Die Lambdas, über die Minecraft einen Link öffnet (im Pausemenü bzw. in
|
||||
* ConfirmLinkScreen), fragen vorher den Helfer: Ist es "aeromc://mods",
|
||||
* öffnet der Launcher das Mod-Fenster und Minecraft zeigt keine Rückfrage.
|
||||
* Alle anderen Links laufen unverändert weiter.
|
||||
*
|
||||
* Beschriftung bzw. Symbol des Knopfs kommen aus dem Ressourcenpaket.
|
||||
*/
|
||||
final class ModsUmbau implements ClassFileTransformer {
|
||||
static final String ZIEL = "aeromc://mods";
|
||||
private static final String[] ADRESSEN = {"https://aka.ms/javafeedback?ref=game", "https://aka.ms/snapshotfeedback?ref=game"};
|
||||
private static final byte[] MERKMAL_FEEDBACK = "aka.ms/".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] MERKMAL_LINK = "chat.link.confirm".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
@Override
|
||||
public byte[] transform(ClassLoader loader, String name, Class<?> umdefiniert, ProtectionDomain pd, byte[] klasse) {
|
||||
if (name == null || umdefiniert != null || name.startsWith("java/") || name.startsWith("jdk/")
|
||||
|| name.startsWith("sun/") || name.startsWith("de/aeromc/") || name.startsWith("org/lwjgl/")) return null;
|
||||
try {
|
||||
boolean feedback = enthaelt(klasse, MERKMAL_FEEDBACK);
|
||||
boolean link = enthaelt(klasse, MERKMAL_LINK);
|
||||
if (!feedback && !link) return null;
|
||||
ClassNode cn = new ClassNode();
|
||||
new ClassReader(klasse).accept(cn, 0);
|
||||
int adressen = 0, waechter = 0;
|
||||
for (MethodNode m : cn.methods) {
|
||||
for (AbstractInsnNode i = m.instructions.getFirst(); i != null; i = i.getNext()) {
|
||||
if (i instanceof LdcInsnNode && istFeedbackAdresse(((LdcInsnNode) i).cst)) {
|
||||
((LdcInsnNode) i).cst = ZIEL;
|
||||
adressen++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (adressen == 0 && !link) return null;
|
||||
for (MethodNode m : cn.methods) {
|
||||
if ((m.access & Opcodes.ACC_SYNTHETIC) == 0 || Type.getReturnType(m.desc) != Type.VOID_TYPE) continue;
|
||||
int slot = adressParameter(m);
|
||||
boolean konstante = slot < 0 && enthaeltZiel(m);
|
||||
if (slot < 0 && !konstante) continue;
|
||||
if (frameAmAnfang(m)) continue; // nicht sicher erweiterbar
|
||||
InsnList w = new InsnList();
|
||||
w.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System", "getProperties", "()Ljava/util/Properties;", false));
|
||||
w.add(new LdcInsnNode(Design.MODS));
|
||||
w.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/util/Properties", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", false));
|
||||
w.add(new TypeInsnNode(Opcodes.CHECKCAST, "java/util/function/Function"));
|
||||
if (slot >= 0) w.add(new VarInsnNode(Opcodes.ALOAD, slot)); else w.add(new LdcInsnNode(ZIEL));
|
||||
w.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, "java/util/function/Function", "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", true));
|
||||
w.add(new FieldInsnNode(Opcodes.GETSTATIC, "java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;"));
|
||||
LabelNode weiter = new LabelNode();
|
||||
w.add(new JumpInsnNode(Opcodes.IF_ACMPNE, weiter));
|
||||
w.add(new InsnNode(Opcodes.RETURN));
|
||||
w.add(weiter);
|
||||
w.add(new FrameNode(Opcodes.F_SAME, 0, null, 0, null));
|
||||
m.instructions.insert(w);
|
||||
waechter++;
|
||||
}
|
||||
if (adressen == 0 && waechter == 0) return null;
|
||||
System.out.println("[AeroMC] Mod-Schalter vorbereitet (Adressen " + adressen + ", Links " + waechter + ")");
|
||||
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
cn.accept(cw);
|
||||
return cw.toByteArray();
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] Mod-Schalter: " + name + " nicht angepasst: " + t);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean istFeedbackAdresse(Object cst) {
|
||||
for (String a : ADRESSEN) if (a.equals(cst)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Slot des ersten Parameters vom Typ String oder URI, sonst -1 */
|
||||
private static int adressParameter(MethodNode m) {
|
||||
int slot = (m.access & Opcodes.ACC_STATIC) != 0 ? 0 : 1;
|
||||
for (Type t : Type.getArgumentTypes(m.desc)) {
|
||||
String d = t.getDescriptor();
|
||||
if ("Ljava/lang/String;".equals(d) || "Ljava/net/URI;".equals(d)) return slot;
|
||||
slot += t.getSize();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static boolean enthaeltZiel(MethodNode m) {
|
||||
for (AbstractInsnNode i = m.instructions.getFirst(); i != null; i = i.getNext()) {
|
||||
if (i instanceof LdcInsnNode && ZIEL.equals(((LdcInsnNode) i).cst)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** steht vor dem ersten echten Befehl schon ein Frame (Sprungziel am Anfang)? */
|
||||
private static boolean frameAmAnfang(MethodNode m) {
|
||||
for (AbstractInsnNode i = m.instructions.getFirst(); i != null; i = i.getNext()) {
|
||||
if (i instanceof FrameNode) return true;
|
||||
if (i.getOpcode() >= 0) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean enthaelt(byte[] daten, byte[] muster) {
|
||||
outer:
|
||||
for (int i = 0, ende = daten.length - muster.length; i <= ende; i++) {
|
||||
for (int k = 0; k < muster.length; k++) if (daten[i + k] != muster[k]) continue outer;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
package de.aeromc.capes;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Wird aus authlib heraus mit (ergebnis, argument) aufgerufen und gibt das
|
||||
* Ergebnis zurück – mit eingesetztem AeroMC-Umhang, falls der Spieler einen hat.
|
||||
* Arbeitet nur über Reflexion, damit es zu jeder authlib-Fassung passt, und
|
||||
* fängt jeden Fehler ab: im schlimmsten Fall bleibt alles wie ohne Agent.
|
||||
*/
|
||||
public final class Nachschlagen implements BiFunction<Object, Object, Object> {
|
||||
public static final String STANDARD_BASIS = "https://viper-network.de/wp-json/aeromc/v1";
|
||||
private static final long GEFUNDEN_MS = 5 * 60 * 1000L;
|
||||
private static final long KEINER_MS = 2 * 60 * 1000L;
|
||||
private static final Pattern URL_MUSTER = Pattern.compile("\"url\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"");
|
||||
private static final Pattern ID_MUSTER = Pattern.compile("\"profileId\"\\s*:\\s*\"([0-9a-fA-F-]{32,36})\"");
|
||||
|
||||
private static final class Eintrag {
|
||||
final String url; // null = kein Umhang
|
||||
final long zeit;
|
||||
Eintrag(String url) { this.url = url; this.zeit = System.currentTimeMillis(); }
|
||||
boolean frisch() { return System.currentTimeMillis() - zeit < (url != null ? GEFUNDEN_MS : KEINER_MS); }
|
||||
}
|
||||
|
||||
private final String basis;
|
||||
private final Map<String, Eintrag> speicher = new ConcurrentHashMap<>();
|
||||
private final Map<String, Boolean> unterwegs = new ConcurrentHashMap<>();
|
||||
private final Map<String, Boolean> gemeldet = new ConcurrentHashMap<>();
|
||||
|
||||
public Nachschlagen(String basis) {
|
||||
this.basis = basis.endsWith("/") ? basis.substring(0, basis.length() - 1) : basis;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object apply(Object ergebnis, Object argument) {
|
||||
try {
|
||||
String id = spielerId(argument);
|
||||
if (id == null) return ergebnis;
|
||||
String url = umhangUrl(id);
|
||||
if (url == null) return ergebnis;
|
||||
Object neu = mitUmhang(ergebnis, url, argument);
|
||||
if (neu != ergebnis && gemeldet.putIfAbsent(id, Boolean.TRUE) == null) {
|
||||
System.out.println("[AeroMC] Umhang fuer " + id + " eingesetzt");
|
||||
}
|
||||
return neu;
|
||||
} catch (Throwable t) {
|
||||
return ergebnis;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Spieler erkennen ----------
|
||||
private static String spielerId(Object argument) throws Exception {
|
||||
if (argument == null) return null;
|
||||
String klasse = argument.getClass().getName();
|
||||
if (klasse.endsWith("GameProfile")) {
|
||||
Object id = rufe(argument, "getId", "id");
|
||||
return id instanceof UUID ? hex((UUID) id) : null;
|
||||
}
|
||||
if (klasse.endsWith("Property")) {
|
||||
Object wert = rufe(argument, "getValue", "value");
|
||||
if (!(wert instanceof String)) return null;
|
||||
String json = new String(Base64.getDecoder().decode(((String) wert).trim()), StandardCharsets.UTF_8);
|
||||
Matcher m = ID_MUSTER.matcher(json);
|
||||
return m.find() ? m.group(1).replace("-", "").toLowerCase() : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String hex(UUID u) {
|
||||
return u.toString().replace("-", "").toLowerCase();
|
||||
}
|
||||
|
||||
private static Object rufe(Object ziel, String... namen) throws Exception {
|
||||
for (String n : namen) {
|
||||
try {
|
||||
Method m = ziel.getClass().getMethod(n);
|
||||
return m.invoke(ziel);
|
||||
} catch (NoSuchMethodException ignoriert) {
|
||||
// nächster Name
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------- Umhang-Server ----------
|
||||
private String umhangUrl(String id) {
|
||||
Eintrag e = speicher.get(id);
|
||||
if (e != null && e.frisch()) return e.url;
|
||||
// Auf dem Haupt-/Render-Thread nie warten: dort nur im Hintergrund holen;
|
||||
// Minecraft fragt beim nächsten Laden des Spielers erneut
|
||||
String thread = Thread.currentThread().getName();
|
||||
boolean hauptThread = thread.equals("Render thread") || thread.equals("Client thread") || thread.equals("main");
|
||||
if (hauptThread) {
|
||||
if (unterwegs.putIfAbsent(id, Boolean.TRUE) == null) {
|
||||
Thread t = new Thread(() -> { try { hole(id); } finally { unterwegs.remove(id); } }, "AeroMC-Umhang");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
return e != null ? e.url : null;
|
||||
}
|
||||
return hole(id);
|
||||
}
|
||||
|
||||
private String hole(String id) {
|
||||
try {
|
||||
HttpURLConnection c = (HttpURLConnection) new URL(basis + "/capes?uuids=" + id).openConnection();
|
||||
c.setConnectTimeout(2000);
|
||||
c.setReadTimeout(3000);
|
||||
c.setRequestProperty("User-Agent", "AeroMC-Umhang/1.0");
|
||||
c.setRequestProperty("Accept", "application/json");
|
||||
if (c.getResponseCode() != 200) {
|
||||
speicher.put(id, new Eintrag(null));
|
||||
return null;
|
||||
}
|
||||
String json;
|
||||
try (InputStream in = c.getInputStream()) {
|
||||
ByteArrayOutputStream puffer = new ByteArrayOutputStream();
|
||||
byte[] b = new byte[4096];
|
||||
int n;
|
||||
while ((n = in.read(b)) > 0 && puffer.size() < 65536) puffer.write(b, 0, n);
|
||||
json = new String(puffer.toByteArray(), StandardCharsets.UTF_8);
|
||||
}
|
||||
String url = null;
|
||||
Matcher m = URL_MUSTER.matcher(json);
|
||||
if (json.contains("\"" + id + "\"") && m.find()) {
|
||||
url = m.group(1).replace("\\/", "/");
|
||||
if (!url.startsWith("https://")) url = null;
|
||||
}
|
||||
speicher.put(id, new Eintrag(url));
|
||||
return url;
|
||||
} catch (Throwable t) {
|
||||
// Server nicht erreichbar: kurz merken, damit nicht jeder Spieler hängt
|
||||
speicher.put(id, new Eintrag(null));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Umhang einsetzen ----------
|
||||
private static Object textur(ClassLoader cl, String url) throws Exception {
|
||||
Class<?> k = Class.forName("com.mojang.authlib.minecraft.MinecraftProfileTexture", false, cl);
|
||||
Constructor<?> ctor = k.getConstructor(String.class, Map.class);
|
||||
return ctor.newInstance(url, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private static Object mitUmhang(Object ergebnis, String url, Object argument) throws Exception {
|
||||
ClassLoader cl = argument.getClass().getClassLoader();
|
||||
if (ergebnis instanceof Map) {
|
||||
// authlib 1–4: Map<MinecraftProfileTexture.Type, MinecraftProfileTexture>
|
||||
Class<?> typ = Class.forName("com.mojang.authlib.minecraft.MinecraftProfileTexture$Type", false, cl);
|
||||
Object cape = Enum.valueOf((Class<Enum>) typ, "CAPE");
|
||||
Map neu = new HashMap((Map) ergebnis);
|
||||
neu.put(cape, textur(cl, url));
|
||||
return neu;
|
||||
}
|
||||
if (ergebnis == null) return null;
|
||||
// authlib 5+: record MinecraftProfileTextures(skin, cape, elytra, signatureState)
|
||||
Class<?> k = ergebnis.getClass();
|
||||
Object skin = k.getMethod("skin").invoke(ergebnis);
|
||||
Object elytra = k.getMethod("elytra").invoke(ergebnis);
|
||||
Object signatur = k.getMethod("signatureState").invoke(ergebnis);
|
||||
for (Constructor<?> ctor : k.getConstructors()) {
|
||||
if (ctor.getParameterCount() == 4) {
|
||||
return ctor.newInstance(skin, textur(cl, url), elytra, signatur);
|
||||
}
|
||||
}
|
||||
return ergebnis;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package de.aeromc.capes;
|
||||
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.security.ProtectionDomain;
|
||||
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
/**
|
||||
* Baut YggdrasilMinecraftSessionService beim Laden um: vor jedem "return" der
|
||||
* Textur-Methode wird das Ergebnis an den Nachschlage-Dienst gereicht, der den
|
||||
* AeroMC-Umhang einsetzt.
|
||||
*
|
||||
* authlib 5+ (Minecraft 1.20.2 … 26.x):
|
||||
* MinecraftProfileTextures unpackTextures(Property)
|
||||
* authlib 1–4 (Minecraft 1.8 … 1.20.1):
|
||||
* Map<Type, MinecraftProfileTexture> getTextures(GameProfile, boolean)
|
||||
*
|
||||
* Eingefügt wird nur geradliniger Code ohne Sprünge – die vorhandenen
|
||||
* Stack-Map-Frames bleiben gültig, es muss nichts neu berechnet werden.
|
||||
*/
|
||||
final class Umbau implements ClassFileTransformer {
|
||||
static final String ZIEL = "com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService";
|
||||
|
||||
@Override
|
||||
public byte[] transform(ClassLoader loader, String name, Class<?> umdefiniert, ProtectionDomain pd, byte[] klasse) {
|
||||
if (!ZIEL.equals(name) || umdefiniert != null) return null;
|
||||
try {
|
||||
ClassReader leser = new ClassReader(klasse);
|
||||
ClassWriter schreiber = new ClassWriter(leser, ClassWriter.COMPUTE_MAXS);
|
||||
final int[] umgebaut = {0};
|
||||
leser.accept(new ClassVisitor(Opcodes.ASM9, schreiber) {
|
||||
@Override
|
||||
public MethodVisitor visitMethod(int zugriff, String mname, String desc, String sig, String[] ausn) {
|
||||
MethodVisitor mv = super.visitMethod(zugriff, mname, desc, sig, ausn);
|
||||
if ((zugriff & Opcodes.ACC_STATIC) != 0) return mv;
|
||||
String rueckgabe = null;
|
||||
if ("unpackTextures".equals(mname) && desc.startsWith("(Lcom/mojang/authlib/properties/Property;)L")) {
|
||||
rueckgabe = desc.substring(desc.indexOf(")L") + 2, desc.length() - 1);
|
||||
} else if ("getTextures".equals(mname) && desc.equals("(Lcom/mojang/authlib/GameProfile;Z)Ljava/util/Map;")) {
|
||||
rueckgabe = "java/util/Map";
|
||||
}
|
||||
if (rueckgabe == null) return mv;
|
||||
umgebaut[0]++;
|
||||
final String typ = rueckgabe;
|
||||
return new MethodVisitor(Opcodes.ASM9, mv) {
|
||||
@Override
|
||||
public void visitInsn(int op) {
|
||||
if (op == Opcodes.ARETURN) {
|
||||
// Stapel: [ergebnis] -> [dienst, ergebnis, argument] -> apply -> [neu]
|
||||
super.visitVarInsn(Opcodes.ALOAD, 1);
|
||||
super.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "getProperties", "()Ljava/util/Properties;", false);
|
||||
super.visitLdcInsn(Agent.SCHLUESSEL);
|
||||
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/util/Properties", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", false);
|
||||
super.visitTypeInsn(Opcodes.CHECKCAST, "java/util/function/BiFunction");
|
||||
super.visitInsn(Opcodes.DUP_X2);
|
||||
super.visitInsn(Opcodes.POP);
|
||||
super.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/function/BiFunction", "apply",
|
||||
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", true);
|
||||
super.visitTypeInsn(Opcodes.CHECKCAST, typ);
|
||||
}
|
||||
super.visitInsn(op);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, 0);
|
||||
if (umgebaut[0] == 0) return null;
|
||||
System.out.println("[AeroMC] authlib fuer Umhaenge vorbereitet (" + umgebaut[0] + " Methode)");
|
||||
return schreiber.toByteArray();
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] authlib konnte nicht angepasst werden: " + t);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Enumeration;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.commons.ClassRemapper;
|
||||
import org.objectweb.asm.commons.Remapper;
|
||||
|
||||
/**
|
||||
* Baut die Agent-Datei: eigene Klassen + ASM, dabei org/objectweb/asm nach
|
||||
* de/aeromc/capes/asm umbenannt. So kann das ASM von Fabric/Forge (das auf
|
||||
* demselben Klassenpfad liegt) nie mit unserem verwechselt werden.
|
||||
*
|
||||
* Aufruf: java Shade <ziel.jar> <klassenordner> <asm.jar> [weitere ASM-Jars, z. B. asm-tree]
|
||||
*/
|
||||
public class Shade {
|
||||
static final Remapper UMBENENNEN = new Remapper() {
|
||||
@Override
|
||||
public String map(String name) {
|
||||
return name.startsWith("org/objectweb/asm/") ? "de/aeromc/capes/asm/" + name.substring(18) : name;
|
||||
}
|
||||
};
|
||||
|
||||
static byte[] umbenennen(byte[] klasse) {
|
||||
ClassReader r = new ClassReader(klasse);
|
||||
ClassWriter w = new ClassWriter(0);
|
||||
r.accept(new ClassRemapper(w, UMBENENNEN), 0);
|
||||
return w.toByteArray();
|
||||
}
|
||||
|
||||
static byte[] lies(InputStream in) throws Exception {
|
||||
ByteArrayOutputStream b = new ByteArrayOutputStream();
|
||||
byte[] p = new byte[8192];
|
||||
int n;
|
||||
while ((n = in.read(p)) > 0) b.write(p, 0, n);
|
||||
return b.toByteArray();
|
||||
}
|
||||
|
||||
public static void main(String[] a) throws Exception {
|
||||
Manifest mf = new Manifest();
|
||||
Attributes at = mf.getMainAttributes();
|
||||
at.put(Attributes.Name.MANIFEST_VERSION, "1.0");
|
||||
at.putValue("Premain-Class", "de.aeromc.capes.Agent");
|
||||
at.putValue("Agent-Class", "de.aeromc.capes.Agent");
|
||||
at.putValue("Can-Retransform-Classes", "false");
|
||||
at.putValue("Implementation-Title", "AeroMC Agent (Umhaenge, Spieldesign)");
|
||||
try (JarOutputStream out = new JarOutputStream(new FileOutputStream(a[0]), mf)) {
|
||||
// eigene Klassen
|
||||
File basis = new File(a[1]);
|
||||
for (File f : Files.walk(basis.toPath()).map(p -> p.toFile()).filter(f -> f.getName().endsWith(".class")).toArray(File[]::new)) {
|
||||
String name = basis.toPath().relativize(f.toPath()).toString().replace('\\', '/');
|
||||
out.putNextEntry(new JarEntry(name));
|
||||
out.write(umbenennen(Files.readAllBytes(f.toPath())));
|
||||
out.closeEntry();
|
||||
}
|
||||
// ASM (umbenannt), ohne Modul-Infos und Signaturen
|
||||
for (int j = 2; j < a.length; j++) try (JarFile asm = new JarFile(a[j])) {
|
||||
Enumeration<JarEntry> e = asm.entries();
|
||||
while (e.hasMoreElements()) {
|
||||
JarEntry je = e.nextElement();
|
||||
String n = je.getName();
|
||||
if (!n.startsWith("org/objectweb/asm/") || !n.endsWith(".class")) continue;
|
||||
byte[] neu = umbenennen(lies(asm.getInputStream(je)));
|
||||
out.putNextEntry(new JarEntry("de/aeromc/capes/asm/" + n.substring(18)));
|
||||
out.write(neu);
|
||||
out.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 169 KiB |
@@ -0,0 +1,49 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<!-- .NET Framework 4.8 ist in jedem Windows 10/11 enthalten → der Installer läuft ohne Zusatzinstallation -->
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<UseWPF>true</UseWPF>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<AssemblyName>AeroMC-Setup</AssemblyName>
|
||||
<RootNamespace>AeroMCSetup</RootNamespace>
|
||||
<ApplicationIcon>..\src\assets\icon.ico</ApplicationIcon>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Product>AeroMC Setup</Product>
|
||||
<AssemblyTitle>AeroMC Setup</AssemblyTitle>
|
||||
<Description>Installiert AeroMC</Description>
|
||||
<Company>M_Viper</Company>
|
||||
<Authors>M_Viper</Authors>
|
||||
<Copyright>Copyright (c) M_Viper. Alle Rechte vorbehalten.</Copyright>
|
||||
<NeutralLanguage>de-DE</NeutralLanguage>
|
||||
<!-- Version kommt beim Bauen aus package.json (scripts/build-setup.js) -->
|
||||
<Version Condition="'$(Version)' == ''">0.0.0</Version>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<!-- WinForms nur für den Ordner-Dialog; nicht global, sonst Namenskonflikte mit WPF -->
|
||||
<Using Remove="System.Windows.Forms" />
|
||||
<Using Remove="System.Drawing" />
|
||||
<Reference Include="Microsoft.VisualBasic" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.Web" />
|
||||
<Reference Include="System.Web.Extensions" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!-- Themes\*.xaml (gleiches Design wie der DeskCore-Installer) bindet das SDK automatisch als Page ein -->
|
||||
<Resource Include="..\src\assets\icon.ico" Link="app.ico" />
|
||||
<Resource Include="..\src\assets\logo.png" Link="app.png" />
|
||||
<EmbeddedResource Include="..\LICENSE" LogicalName="LICENSE.txt" />
|
||||
</ItemGroup>
|
||||
<!--
|
||||
Die eigentliche App (entpackter Electron-Ordner als ZIP) wird NICHT hier eingebettet,
|
||||
sondern nach dem Bauen hinten an die .exe angehängt (scripts/build-setup.js, siehe Payload.cs).
|
||||
So muss der Compiler keine ~150 MB als Ressource verarbeiten, und das Deinstallationsprogramm
|
||||
ist einfach der vordere Teil der Datei ohne Anhang.
|
||||
-->
|
||||
</Project>
|
||||
@@ -0,0 +1,19 @@
|
||||
<Application x:Class="AeroMCSetup.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<!-- [0] wird je nach Hell/Dunkel ersetzt -->
|
||||
<ResourceDictionary Source="Themes/Colors.Light.xaml"/>
|
||||
<ResourceDictionary Source="Themes/Controls.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<!-- AeroMC-Blau (wie Logo und Webseite) -->
|
||||
<SolidColorBrush x:Key="AccentBrush" Color="#2F6FDB"/>
|
||||
<SolidColorBrush x:Key="AccentHoverBrush" Color="#4380E6"/>
|
||||
<SolidColorBrush x:Key="OnAccentBrush" Color="White"/>
|
||||
<SolidColorBrush x:Key="AccentSubtleBrush" Color="#222F6FDB"/>
|
||||
<SolidColorBrush x:Key="AccentSubtleStrokeBrush" Color="#552F6FDB"/>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,97 @@
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace AeroMCSetup;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
private static Mutex? _single;
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
base.OnStartup(e);
|
||||
bool has(string arg) => e.Args.Any(a => a.Equals(arg, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// Das Deinstallationsprogramm ist das Setup ohne angehängte App → ohne Anhang immer Deinstallation
|
||||
bool uninstall = has("--uninstall") || !Payload.Exists;
|
||||
bool silent = has("/S");
|
||||
bool update = has("--update");
|
||||
bool launch = has("/launch");
|
||||
var dirArg = e.Args.FirstOrDefault(a => a.StartsWith("/D=", StringComparison.OrdinalIgnoreCase))?.Substring(3);
|
||||
|
||||
_single = new Mutex(true, "AeroMC-Setup", out bool first);
|
||||
if (!first)
|
||||
{
|
||||
Shutdown(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stille Installation/Deinstallation für Skripte: AeroMC-Setup.exe /S [/D=Ordner] [/launch] bzw. --uninstall /S
|
||||
if (silent)
|
||||
{
|
||||
var progress = new Progress<(double, string)>(_ => { });
|
||||
try
|
||||
{
|
||||
if (uninstall) Installer.Uninstall(removeData: false, progress);
|
||||
else
|
||||
{
|
||||
var dir = dirArg ?? Installer.InstalledDir ?? Installer.DefaultDir;
|
||||
Installer.Install(dir, desktopLink: Installer.InstalledDir is null || Installer.DesktopLinkExists,
|
||||
progress, waitForExit: true);
|
||||
if (launch) System.Diagnostics.Process.Start(Path.Combine(dir, Installer.ExeName));
|
||||
}
|
||||
Shutdown(0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// ohne Oberfläche: Fehler für Skripte nachvollziehbar ablegen
|
||||
try { File.WriteAllText(Path.Combine(Path.GetTempPath(), "AeroMC-Setup.log"), $"[{DateTime.Now}] {ex}"); } catch { }
|
||||
Shutdown(1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyTheme();
|
||||
var mode = uninstall ? SetupMode.Uninstall : update ? SetupMode.Update : SetupMode.Install;
|
||||
new MainWindow(mode).Show();
|
||||
}
|
||||
|
||||
protected override void OnExit(ExitEventArgs e)
|
||||
{
|
||||
try { _single?.ReleaseMutex(); } catch { }
|
||||
base.OnExit(e);
|
||||
}
|
||||
|
||||
/// <summary>Hell/Dunkel wie in Windows eingestellt.</summary>
|
||||
private void ApplyTheme()
|
||||
{
|
||||
bool light = ReadDword(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme") != 0;
|
||||
Resources.MergedDictionaries[0] = new ResourceDictionary
|
||||
{
|
||||
Source = new Uri($"pack://application:,,,/Themes/Colors.{(light ? "Light" : "Dark")}.xaml"),
|
||||
};
|
||||
if (!light)
|
||||
{
|
||||
// im Dunkeln etwas heller, damit Text und Fortschrittsbalken gut lesbar bleiben
|
||||
var a = Color.FromRgb(0x4A, 0x86, 0xEA);
|
||||
Resources["AccentBrush"] = new SolidColorBrush(a);
|
||||
Resources["AccentHoverBrush"] = new SolidColorBrush(Color.FromRgb(0x5E, 0x94, 0xEE));
|
||||
Resources["AccentSubtleBrush"] = new SolidColorBrush(Color.FromArgb(0x30, a.R, a.G, a.B));
|
||||
Resources["AccentSubtleStrokeBrush"] = new SolidColorBrush(Color.FromArgb(0x60, a.R, a.G, a.B));
|
||||
}
|
||||
}
|
||||
|
||||
private static int ReadDword(string key, string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var k = Registry.CurrentUser.OpenSubKey(key);
|
||||
return k?.GetValue(name) is int i ? i : -1;
|
||||
}
|
||||
catch { return -1; }
|
||||
}
|
||||
}
|
||||
|
||||
public enum SetupMode { Install, Update, Uninstall }
|
||||
@@ -0,0 +1,529 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace AeroMCSetup;
|
||||
|
||||
/// <summary>Installation/Deinstallation – nur für den aktuellen Benutzer, ohne Administratorrechte.</summary>
|
||||
public static class Installer
|
||||
{
|
||||
public const string AppName = "AeroMC";
|
||||
public const string Publisher = "M_Viper";
|
||||
public const string ExeName = "AeroMC.exe";
|
||||
// Gleicher Name wie beim früheren NSIS-Setup: der Launcher erkennt daran eine Setup-Installation
|
||||
// und bietet Updates als Setup an (src/updater.js → hasUninstaller).
|
||||
public const string UninstallerName = "Uninstall AeroMC.exe";
|
||||
// wie "appId" in package.json – gleiche Taskleisten-/Benachrichtigungs-Zuordnung wie bisher
|
||||
public const string AppUserModelId = "com.aeromc.launcher";
|
||||
|
||||
// Schlüssel vom alten electron-builder-Setup übernommen → in "Apps" bleibt es ein einziger Eintrag
|
||||
private const string KeyId = "12814222-87ee-5ae1-9e10-8eb2741fedd1";
|
||||
private const string UninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall\" + KeyId;
|
||||
private const string LegacyAppKey = @"Software\" + KeyId;
|
||||
private const string RunKey = @"Software\Microsoft\Windows\CurrentVersion\Run";
|
||||
private const string FileListName = "install-files.txt";
|
||||
|
||||
public static string Version
|
||||
{
|
||||
get
|
||||
{
|
||||
var info = typeof(Installer).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
|
||||
if (!string.IsNullOrEmpty(info)) return info!.Split('+')[0];
|
||||
var v = typeof(Installer).Assembly.GetName().Version!;
|
||||
return $"{v.Major}.{v.Minor}.{v.Build}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Pfad dieses Setups (bzw. des Deinstallationsprogramms).</summary>
|
||||
public static string SelfPath => Assembly.GetExecutingAssembly().Location;
|
||||
|
||||
public static string DefaultDir =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", AppName);
|
||||
|
||||
/// <summary>Launcher-Daten: Einstellungen, Konten, Java und (standardmäßig) die Instanzen.</summary>
|
||||
public static string DataDir =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "aeromc");
|
||||
|
||||
private static string StartMenuLink =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Programs), AppName + ".lnk");
|
||||
|
||||
private static string DesktopLink =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), AppName + ".lnk");
|
||||
|
||||
// ---------- vorhandene Installation ----------
|
||||
|
||||
private static string? RegValue(string name)
|
||||
{
|
||||
using var k = Registry.CurrentUser.OpenSubKey(UninstallKey);
|
||||
return k?.GetValue(name) as string;
|
||||
}
|
||||
|
||||
/// <summary>Ordner einer vorhandenen Installation (dieses Setups oder des alten NSIS-Setups), oder null.</summary>
|
||||
public static string? InstalledDir
|
||||
{
|
||||
get
|
||||
{
|
||||
var dir = RegValue("InstallLocation");
|
||||
if (string.IsNullOrWhiteSpace(dir))
|
||||
{
|
||||
// altes NSIS-Setup trägt keinen InstallLocation ein → aus dem Deinstallationsbefehl ablesen
|
||||
var m = Regex.Match(RegValue("UninstallString") ?? "", "^\"([^\"]+)\"");
|
||||
dir = m.Success ? Path.GetDirectoryName(m.Groups[1].Value) : null;
|
||||
}
|
||||
return dir is not null && File.Exists(Path.Combine(dir, ExeName)) ? dir : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Stammt die vorhandene Installation noch vom alten NSIS-Setup?</summary>
|
||||
public static bool IsLegacyInstall =>
|
||||
InstalledDir is not null && (RegValue("UninstallString") ?? "").IndexOf("--uninstall", StringComparison.OrdinalIgnoreCase) < 0;
|
||||
|
||||
public static string? InstalledVersion => RegValue("DisplayVersion");
|
||||
|
||||
public static bool DesktopLinkExists => File.Exists(DesktopLink);
|
||||
|
||||
public static bool IsRunning(string? dir) => AppProcesses(dir, InstalledDir).Any();
|
||||
|
||||
public static string LicenseText
|
||||
{
|
||||
get
|
||||
{
|
||||
using var s = typeof(Installer).Assembly.GetManifestResourceStream("LICENSE.txt");
|
||||
if (s is null) return "";
|
||||
using var r = new StreamReader(s);
|
||||
return r.ReadToEnd().Trim();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Eigener Instanzen-Ordner außerhalb der Launcher-Daten (falls in den Einstellungen gesetzt), sonst null.</summary>
|
||||
public static string? CustomInstancesDir
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(Path.Combine(DataDir, "settings.json"));
|
||||
var m = Regex.Match(json, "\"instancesDir\"\\s*:\\s*\"((?:[^\"\\\\]|\\\\.)*)\"");
|
||||
if (!m.Success) return null;
|
||||
var dir = Regex.Unescape(m.Groups[1].Value).Trim();
|
||||
// liegt er doch in den Launcher-Daten (z. B. ausdrücklich der Standardordner), ist er nicht "eigen"
|
||||
return dir.Length > 0 && !IsUnder(dir, DataDir) && !SamePath(dir, DataDir) ? dir : null;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Installieren ----------
|
||||
|
||||
/// <param name="waitForExit">nach einem Update aus dem Launcher: ihm erst Zeit geben, sich selbst zu beenden</param>
|
||||
public static void Install(string dir, bool desktopLink, IProgress<(double, string)> progress, bool waitForExit = false)
|
||||
{
|
||||
if (!Environment.Is64BitOperatingSystem)
|
||||
throw new InvalidOperationException("AeroMC benötigt ein 64-Bit-Windows (Windows 10 oder 11).");
|
||||
|
||||
var oldDir = InstalledDir;
|
||||
progress.Report((0.02, "AeroMC wird beendet …"));
|
||||
CloseRunningApp(waitForExit ? 20000 : 0, dir, oldDir);
|
||||
|
||||
using var zip = Payload.OpenArchive();
|
||||
var topLevel = TopLevelNames(zip);
|
||||
|
||||
progress.Report((0.05, "Alte Version wird entfernt …"));
|
||||
Directory.CreateDirectory(dir);
|
||||
RemoveAppFiles(dir, topLevel);
|
||||
if (oldDir is not null && !SamePath(oldDir, dir))
|
||||
{
|
||||
// Installation wurde in einen anderen Ordner verlegt → alten Ordner aufräumen
|
||||
var oldExe = Path.Combine(oldDir, ExeName);
|
||||
DeleteIfPointsTo(StartMenuLink, oldExe);
|
||||
DeleteIfPointsTo(DesktopLink, oldExe);
|
||||
RemoveAppFiles(oldDir, topLevel);
|
||||
try { Directory.Delete(oldDir); } catch { } // nur wenn leer
|
||||
}
|
||||
|
||||
Extract(zip, dir, p => progress.Report((0.08 + p * 0.8, "Dateien werden kopiert …")));
|
||||
|
||||
progress.Report((0.9, "Deinstallationsprogramm wird angelegt …"));
|
||||
var exe = Path.Combine(dir, ExeName);
|
||||
var uninstaller = Path.Combine(dir, UninstallerName);
|
||||
if (!SamePath(SelfPath, uninstaller))
|
||||
{
|
||||
MoveAside(uninstaller);
|
||||
CopyStub(uninstaller);
|
||||
}
|
||||
File.WriteAllLines(Path.Combine(dir, FileListName), topLevel.Concat([UninstallerName, FileListName]));
|
||||
|
||||
progress.Report((0.94, "Verknüpfungen werden angelegt …"));
|
||||
Shortcut.Create(StartMenuLink, exe, "Minecraft-Launcher", AppUserModelId);
|
||||
if (desktopLink) Shortcut.Create(DesktopLink, exe, "Minecraft-Launcher", AppUserModelId);
|
||||
else DeleteIfPointsTo(DesktopLink, exe);
|
||||
|
||||
progress.Report((0.97, "Windows wird eingerichtet …"));
|
||||
Registry.CurrentUser.DeleteSubKeyTree(UninstallKey, throwOnMissingSubKey: false); // Reste des alten Setups
|
||||
Registry.CurrentUser.DeleteSubKeyTree(LegacyAppKey, throwOnMissingSubKey: false);
|
||||
using (var k = Registry.CurrentUser.CreateSubKey(UninstallKey))
|
||||
{
|
||||
long size = DirSize(dir) / 1024;
|
||||
k.SetValue("DisplayName", AppName);
|
||||
k.SetValue("DisplayVersion", Version);
|
||||
k.SetValue("Publisher", Publisher);
|
||||
k.SetValue("DisplayIcon", $"{exe},0");
|
||||
k.SetValue("InstallLocation", dir);
|
||||
k.SetValue("UninstallString", $"\"{uninstaller}\" --uninstall");
|
||||
k.SetValue("QuietUninstallString", $"\"{uninstaller}\" --uninstall /S");
|
||||
k.SetValue("URLInfoAbout", "https://m-viper.de");
|
||||
k.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd"));
|
||||
k.SetValue("EstimatedSize", (int)Math.Min(int.MaxValue, size), RegistryValueKind.DWord);
|
||||
k.SetValue("NoModify", 1, RegistryValueKind.DWord);
|
||||
k.SetValue("NoRepair", 1, RegistryValueKind.DWord);
|
||||
}
|
||||
|
||||
progress.Report((0.98, "Installation wird abgeschlossen …"));
|
||||
Vorwaermen(exe);
|
||||
|
||||
progress.Report((1.0, "Fertig"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Startet AeroMC einmal kurz mit "--aeromc-vorwaermen" – die App beendet sich
|
||||
/// damit sofort wieder (siehe src/main.js). Windows Defender prüft eine neue,
|
||||
/// noch unbekannte EXE beim ersten Start mehrere Sekunden lang (gemessen: ~8 s);
|
||||
/// das passiert so während des Fortschrittsbalkens und nicht erst beim Klick
|
||||
/// auf "Fertigstellen". Das Ergebnis merkt sich Windows, der echte Start ist
|
||||
/// danach sofort da.
|
||||
/// </summary>
|
||||
private static void Vorwaermen(string exe)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var p = Process.Start(new ProcessStartInfo(exe, "--aeromc-vorwaermen")
|
||||
{
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
WorkingDirectory = Path.GetDirectoryName(exe)!,
|
||||
});
|
||||
if (p != null && !p.WaitForExit(30000)) { try { p.Kill(); } catch { } }
|
||||
}
|
||||
catch { /* nur eine Beschleunigung – Fehler sind egal */ }
|
||||
}
|
||||
|
||||
private static List<string> TopLevelNames(ZipArchive zip) =>
|
||||
zip.Entries
|
||||
.Select(e => e.FullName.Replace('\\', '/').Split('/')[0])
|
||||
.Where(n => n.Length > 0)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
private static void Extract(ZipArchive zip, string dir, Action<double> report)
|
||||
{
|
||||
var root = Path.GetFullPath(dir).TrimEnd('\\') + "\\";
|
||||
long total = Math.Max(1, zip.Entries.Sum(e => e.Length)), done = 0;
|
||||
var buffer = new byte[1 << 20];
|
||||
int lastPercent = -1;
|
||||
|
||||
foreach (var entry in zip.Entries)
|
||||
{
|
||||
var target = Path.GetFullPath(Path.Combine(dir, entry.FullName));
|
||||
if (!target.StartsWith(root, StringComparison.OrdinalIgnoreCase)) continue; // nie außerhalb des Ordners schreiben
|
||||
if (entry.Name.Length == 0)
|
||||
{
|
||||
Directory.CreateDirectory(target);
|
||||
continue;
|
||||
}
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
|
||||
long before = done;
|
||||
Retry(target, () =>
|
||||
{
|
||||
done = before;
|
||||
MoveAside(target);
|
||||
using var src = entry.Open();
|
||||
using var dst = File.Create(target);
|
||||
int n;
|
||||
while ((n = src.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
dst.Write(buffer, 0, n);
|
||||
done += n;
|
||||
int percent = (int)(done * 100 / total);
|
||||
if (percent != lastPercent)
|
||||
{
|
||||
lastPercent = percent;
|
||||
report(Math.Min(1.0, (double)done / total));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
report(1.0);
|
||||
}
|
||||
|
||||
/// <summary>Setup ohne angehängte App als Deinstallationsprogramm ablegen (nur ein paar hundert KB).</summary>
|
||||
private static void CopyStub(string path)
|
||||
{
|
||||
long length = Payload.StubLength;
|
||||
using var src = File.OpenRead(SelfPath);
|
||||
using var dst = File.Create(path);
|
||||
var buffer = new byte[81920];
|
||||
long left = length;
|
||||
while (left > 0)
|
||||
{
|
||||
int n = src.Read(buffer, 0, (int)Math.Min(buffer.Length, left));
|
||||
if (n <= 0) break;
|
||||
dst.Write(buffer, 0, n);
|
||||
left -= n;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Deinstallieren ----------
|
||||
|
||||
public static void Uninstall(bool removeData, IProgress<(double, string)> progress)
|
||||
{
|
||||
// Nur im eingetragenen Installationsordner löschen – nie dort, wo das Setup zufällig liegt (z. B. Downloads)
|
||||
var dir = InstalledDir;
|
||||
var exe = dir is null ? "" : Path.Combine(dir, ExeName);
|
||||
|
||||
progress.Report((0.1, "AeroMC wird beendet …"));
|
||||
CloseRunningApp(0, dir);
|
||||
|
||||
progress.Report((0.3, "Verknüpfungen werden entfernt …"));
|
||||
DeleteIfPointsTo(StartMenuLink, exe);
|
||||
DeleteIfPointsTo(DesktopLink, exe);
|
||||
|
||||
progress.Report((0.45, "Windows-Einträge werden entfernt …"));
|
||||
RemoveAutostart(exe);
|
||||
Registry.CurrentUser.DeleteSubKeyTree(UninstallKey, throwOnMissingSubKey: false);
|
||||
Registry.CurrentUser.DeleteSubKeyTree(LegacyAppKey, throwOnMissingSubKey: false);
|
||||
// Name/Logo für Windows-Benachrichtigungen (legt der Launcher selbst an)
|
||||
Registry.CurrentUser.DeleteSubKeyTree(@"Software\Classes\AppUserModelId\" + AppUserModelId, throwOnMissingSubKey: false);
|
||||
|
||||
if (removeData && Directory.Exists(DataDir))
|
||||
{
|
||||
progress.Report((0.6, "Instanzen, Konten und Einstellungen werden in den Papierkorb gelegt …"));
|
||||
try
|
||||
{
|
||||
Microsoft.VisualBasic.FileIO.FileSystem.DeleteDirectory(DataDir,
|
||||
Microsoft.VisualBasic.FileIO.UIOption.OnlyErrorDialogs,
|
||||
Microsoft.VisualBasic.FileIO.RecycleOption.SendToRecycleBin);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
progress.Report((0.8, "Dateien werden entfernt …"));
|
||||
if (dir is null) { progress.Report((1.0, "Fertig")); return; }
|
||||
RemoveAppFiles(dir, null);
|
||||
|
||||
// Das laufende Deinstallationsprogramm kann sich nicht selbst löschen → kurz nach dem Beenden per cmd.
|
||||
// rmdir ohne /s entfernt den Ordner nur, wenn er danach leer ist.
|
||||
var self = SelfPath;
|
||||
if (IsUnder(self, dir))
|
||||
{
|
||||
Process.Start(new ProcessStartInfo("cmd.exe",
|
||||
$"/c ping 127.0.0.1 -n 3 > nul & del /f /q \"{self}\" & del /f /q \"{Path.Combine(dir, "*.old")}\" & rmdir \"{dir}\"")
|
||||
{
|
||||
CreateNoWindow = true, UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
TryDelete(Path.Combine(dir, UninstallerName));
|
||||
try { Directory.Delete(dir); } catch { } // nur wenn leer
|
||||
}
|
||||
progress.Report((1.0, "Fertig"));
|
||||
}
|
||||
|
||||
/// <summary>Autostart-Einträge entfernen, die der Launcher selbst gesetzt hat (Name vergibt Electron).</summary>
|
||||
private static void RemoveAutostart(string exe)
|
||||
{
|
||||
if (exe.Length == 0) return;
|
||||
using var run = Registry.CurrentUser.OpenSubKey(RunKey, writable: true);
|
||||
if (run is null) return;
|
||||
foreach (var name in run.GetValueNames())
|
||||
{
|
||||
if (run.GetValue(name) is string cmd && cmd.IndexOf(exe, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
run.DeleteValue(name, throwOnMissingValue: false);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Dateien ----------
|
||||
|
||||
/// <summary>
|
||||
/// Entfernt die App-Dateien aus einem Installationsordner. Gelöscht wird nur, was nachweislich zu AeroMC gehört:
|
||||
/// Einträge aus der Dateiliste der letzten Installation bzw. die Namen aus dem neuen Paket –
|
||||
/// Ordner nur, wenn sie eindeutig von Electron stammen. Fremde Dateien im Ordner bleiben unangetastet.
|
||||
/// </summary>
|
||||
private static void RemoveAppFiles(string dir, IEnumerable<string>? extraNames)
|
||||
{
|
||||
if (!Directory.Exists(dir)) return;
|
||||
CleanupOldFiles(dir);
|
||||
var listFile = Path.Combine(dir, FileListName);
|
||||
var fromList = File.Exists(listFile) ? File.ReadAllLines(listFile).Where(l => l.Trim().Length > 0).ToList() : [];
|
||||
var names = fromList.Concat(extraNames ?? []).Concat([UninstallerName, FileListName])
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var name in names)
|
||||
{
|
||||
if (name.IndexOfAny(['\\', '/', ':']) >= 0 || name is "." or "..") continue;
|
||||
var path = Path.Combine(dir, name);
|
||||
if (SamePath(path, SelfPath)) continue; // das laufende Deinstallationsprogramm räumt cmd weg
|
||||
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
bool ours = fromList.Contains(name, StringComparer.OrdinalIgnoreCase)
|
||||
|| (name.Equals("resources", StringComparison.OrdinalIgnoreCase) && File.Exists(Path.Combine(path, "app.asar")))
|
||||
|| (name.Equals("locales", StringComparison.OrdinalIgnoreCase) && File.Exists(Path.Combine(path, "en-US.pak")));
|
||||
if (ours) Retry(path, () => Directory.Delete(path, recursive: true));
|
||||
}
|
||||
else if (File.Exists(path))
|
||||
{
|
||||
Retry(path, () => MoveAside(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Längster Pfad im Paket. Windows erlaubt (ohne Sonderfreigabe) nur 259 Zeichen pro Pfad –
|
||||
/// der Installationsordner darf deshalb höchstens <see cref="MaxDirLength"/> Zeichen lang sein.
|
||||
/// </summary>
|
||||
private static int? _longestEntry;
|
||||
public static int MaxDirLength
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_longestEntry is null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var zip = Payload.OpenArchive();
|
||||
_longestEntry = zip.Entries.Count == 0 ? 0 : zip.Entries.Max(e => e.FullName.Length);
|
||||
}
|
||||
catch { _longestEntry = 0; }
|
||||
}
|
||||
return 259 - 1 - _longestEntry.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private static void Retry(string path, Action action)
|
||||
{
|
||||
for (int attempt = 1; ; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
catch (PathTooLongException ex)
|
||||
{
|
||||
throw new IOException($"Der Installationsordner ist zu lang. Bitte einen kürzeren Ordner wählen (höchstens {MaxDirLength} Zeichen).", ex);
|
||||
}
|
||||
catch (Exception ex) when ((ex is IOException || ex is UnauthorizedAccessException) && attempt < 10)
|
||||
{
|
||||
Thread.Sleep(500);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException)
|
||||
{
|
||||
throw new IOException($"„{Path.GetFileName(path)}“ wird noch verwendet. Bitte AeroMC beenden " +
|
||||
"(auch das Symbol im Infobereich neben der Uhr) und erneut versuchen.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Vorhandene Datei löschen oder – falls sie noch geöffnet ist (Explorer, Virenscanner) – beiseite umbenennen.
|
||||
/// Umbenennen klappt unter Windows auch bei geöffneten Dateien, überschreiben nicht.
|
||||
/// </summary>
|
||||
private static void MoveAside(string path)
|
||||
{
|
||||
if (!File.Exists(path)) return;
|
||||
try { File.Delete(path); return; } catch { }
|
||||
File.Move(path, $"{path}.{Guid.NewGuid():N}.old");
|
||||
}
|
||||
|
||||
private static void CleanupOldFiles(string dir)
|
||||
{
|
||||
if (!Directory.Exists(dir)) return;
|
||||
foreach (var f in Directory.GetFiles(dir, "*.old", SearchOption.AllDirectories))
|
||||
try { File.Delete(f); } catch { }
|
||||
}
|
||||
|
||||
private static void TryDelete(string path)
|
||||
{
|
||||
try { if (File.Exists(path)) File.Delete(path); } catch { }
|
||||
}
|
||||
|
||||
private static void DeleteIfPointsTo(string link, string target)
|
||||
{
|
||||
if (!File.Exists(link) || target.Length == 0) return;
|
||||
if (!SamePath(Shortcut.Target(link) ?? "", target)) return;
|
||||
// Explorer/Indexdienst halten Verknüpfungen manchmal kurz fest → mehrmals versuchen
|
||||
for (int i = 0; i < 10 && File.Exists(link); i++)
|
||||
{
|
||||
try { File.Delete(link); }
|
||||
catch { Thread.Sleep(300); }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Prozesse ----------
|
||||
|
||||
private static IEnumerable<Process> AppProcesses(params string?[] dirs)
|
||||
{
|
||||
var roots = dirs.Where(d => !string.IsNullOrEmpty(d)).Select(d => d!).ToList();
|
||||
if (roots.Count == 0) yield break;
|
||||
// Launcher (mehrere Electron-Prozesse) und ein evtl. laufendes ffmpeg der Aufzeichnung
|
||||
foreach (var p in Process.GetProcessesByName("AeroMC").Concat(Process.GetProcessesByName("ffmpeg")))
|
||||
{
|
||||
string? file = null;
|
||||
try { file = p.MainModule?.FileName; } catch { }
|
||||
if (file is not null && roots.Any(r => IsUnder(file, r))) yield return p;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CloseRunningApp(int waitMs, params string?[] dirs)
|
||||
{
|
||||
var until = DateTime.Now.AddMilliseconds(waitMs);
|
||||
while (waitMs > 0 && DateTime.Now < until && AppProcesses(dirs).Any())
|
||||
Thread.Sleep(300);
|
||||
|
||||
foreach (var p in AppProcesses(dirs).ToList())
|
||||
{
|
||||
try
|
||||
{
|
||||
p.Kill();
|
||||
p.WaitForExit(10000);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Helfer ----------
|
||||
|
||||
private static bool SamePath(string a, string b)
|
||||
{
|
||||
try { return string.Equals(Path.GetFullPath(a).TrimEnd('\\'), Path.GetFullPath(b).TrimEnd('\\'), StringComparison.OrdinalIgnoreCase); }
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private static bool IsUnder(string path, string dir) =>
|
||||
Path.GetFullPath(path).StartsWith(Path.GetFullPath(dir).TrimEnd('\\') + "\\", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static long DirSize(string dir)
|
||||
{
|
||||
long sum = 0;
|
||||
try
|
||||
{
|
||||
foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories))
|
||||
try { sum += new FileInfo(f).Length; } catch { }
|
||||
}
|
||||
catch { }
|
||||
return sum;
|
||||
}
|
||||
|
||||
public static long DataSize() => Directory.Exists(DataDir) ? DirSize(DataDir) : 0;
|
||||
|
||||
public static string FormatSize(long bytes) => bytes switch
|
||||
{
|
||||
< 1024 * 1024 => $"{Math.Max(1, bytes / 1024)} KB",
|
||||
< 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024:0.#} MB",
|
||||
_ => $"{bytes / 1024.0 / 1024 / 1024:0.##} GB",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Web.Script.Serialization;
|
||||
|
||||
namespace AeroMCSetup;
|
||||
|
||||
/// <summary>Ein gefundener anderer Launcher, dessen Instanzen AeroMC übernehmen kann.</summary>
|
||||
/// <param name="kind">Kennung wie im Import-Dialog des Launchers: mc, cf, mr, mmc, technic</param>
|
||||
public sealed class FoundLauncher(string kind, string name, string path, int count, string unit)
|
||||
{
|
||||
public string Kind { get; } = kind;
|
||||
public string Name { get; } = name;
|
||||
public string Path { get; } = path;
|
||||
public int Count { get; } = count;
|
||||
public string Unit { get; } = unit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sucht andere Launcher an ihren üblichen Orten – nur lesend, es wird nichts verändert.
|
||||
/// Übernommen wird später im Launcher selbst (gleicher Import wie unter „Importieren“):
|
||||
/// das Setup legt dafür nur pending-import.json ab, AeroMC öffnet beim ersten Start
|
||||
/// den Import-Dialog damit fertig durchsucht.
|
||||
/// </summary>
|
||||
public static class LauncherImport
|
||||
{
|
||||
private static string AppData => Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
private static string Home => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
|
||||
private static string PendingFile => System.IO.Path.Combine(Installer.DataDir, "pending-import.json");
|
||||
|
||||
public static List<FoundLauncher> Detect()
|
||||
{
|
||||
var found = new List<FoundLauncher>();
|
||||
void Add(Func<FoundLauncher?> probe)
|
||||
{
|
||||
try { if (probe() is { Count: > 0 } f) found.Add(f); } catch { /* unlesbar → einfach nicht anbieten */ }
|
||||
}
|
||||
Add(DetectMinecraftLauncher);
|
||||
Add(DetectCurseForge);
|
||||
Add(DetectModrinthApp);
|
||||
Add(() => DetectMultiMc("Prism Launcher", System.IO.Path.Combine(AppData, "PrismLauncher")));
|
||||
Add(() => DetectMultiMc("PolyMC", System.IO.Path.Combine(AppData, "PolyMC")));
|
||||
// MultiMC (und portable Prism/PolyMC) liegen irgendwo – über ihre Verknüpfungen finden
|
||||
foreach (var (name, root) in PortableMultiMcFromShortcuts())
|
||||
{
|
||||
if (found.Any(f => f.Kind == "mmc" && SamePath(f.Path, root))) continue;
|
||||
Add(() => DetectMultiMc(name, root));
|
||||
}
|
||||
Add(DetectTechnic);
|
||||
return found;
|
||||
}
|
||||
|
||||
// ---------- offizieller Minecraft Launcher ----------
|
||||
|
||||
private static readonly string[] ProfileFiles =
|
||||
["launcher_profiles.json", "launcher_profiles_microsoft_store.json", "launcher_profiles_microsoft_store_2.json"];
|
||||
|
||||
private static FoundLauncher? DetectMinecraftLauncher()
|
||||
{
|
||||
var root = System.IO.Path.Combine(AppData, ".minecraft");
|
||||
var file = ProfileFiles.Select(f => System.IO.Path.Combine(root, f)).FirstOrDefault(File.Exists);
|
||||
if (file is null) return null;
|
||||
|
||||
var json = new JavaScriptSerializer { MaxJsonLength = int.MaxValue }
|
||||
.Deserialize<Dictionary<string, object>>(File.ReadAllText(file, Encoding.UTF8).TrimStart(''));
|
||||
if (json?.TryGetValue("profiles", out var p) != true || p is not Dictionary<string, object> profiles) return null;
|
||||
|
||||
// wie isImportableProfile in src/mcimport.js: die automatischen "Neueste Version"-Profile zählen nicht
|
||||
int count = profiles.Values.OfType<Dictionary<string, object>>().Count(profile =>
|
||||
{
|
||||
var type = (Get(profile, "type") ?? "").Trim().ToLowerInvariant();
|
||||
if (type is "latest-release" or "latest-snapshot") return false;
|
||||
return !string.IsNullOrWhiteSpace(Get(profile, "name")) || !string.IsNullOrWhiteSpace(Get(profile, "lastVersionId"));
|
||||
});
|
||||
return new FoundLauncher("mc", "Minecraft Launcher", root, count, count == 1 ? "Profil" : "Profile");
|
||||
}
|
||||
|
||||
private static string? Get(Dictionary<string, object> d, string key) => d.TryGetValue(key, out var v) ? v as string : null;
|
||||
|
||||
// ---------- CurseForge ----------
|
||||
|
||||
private static FoundLauncher? DetectCurseForge()
|
||||
{
|
||||
var dir = new[]
|
||||
{
|
||||
System.IO.Path.Combine(Home, "curseforge", "minecraft", "Instances"),
|
||||
System.IO.Path.Combine(Home, "CurseForge", "minecraft", "Instances"),
|
||||
}.FirstOrDefault(Directory.Exists);
|
||||
if (dir is null) return null;
|
||||
|
||||
string[] markers = ["minecraftinstance.json", "instance.json", "mods", "config", "resourcepacks", "shaderpacks", "saves", "options.txt"];
|
||||
int count = Directory.GetDirectories(dir).Count(d => markers.Any(m => Exists(d, m)));
|
||||
return new FoundLauncher("cf", "CurseForge", dir, count, count == 1 ? "Instanz" : "Instanzen");
|
||||
}
|
||||
|
||||
// ---------- Modrinth App ----------
|
||||
|
||||
private static FoundLauncher? DetectModrinthApp()
|
||||
{
|
||||
// neuere Fassungen: ModrinthApp, sehr alte: com.modrinth.theseus (siehe src/mrimport.js)
|
||||
var root = new[]
|
||||
{
|
||||
System.IO.Path.Combine(AppData, "ModrinthApp"),
|
||||
System.IO.Path.Combine(AppData, "com.modrinth.theseus"),
|
||||
}.FirstOrDefault(d => Directory.Exists(System.IO.Path.Combine(d, "profiles")));
|
||||
if (root is null) return null;
|
||||
|
||||
string[] markers = ["profile.json", "mods", "config", "resourcepacks", "shaderpacks", "saves", "options.txt"];
|
||||
int count = Directory.GetDirectories(System.IO.Path.Combine(root, "profiles")).Count(d => markers.Any(m => Exists(d, m)));
|
||||
return new FoundLauncher("mr", "Modrinth App", root, count, count == 1 ? "Instanz" : "Instanzen");
|
||||
}
|
||||
|
||||
// ---------- Prism Launcher / PolyMC (MultiMC-Format) ----------
|
||||
|
||||
private static FoundLauncher? DetectMultiMc(string name, string root)
|
||||
{
|
||||
var instances = System.IO.Path.Combine(root, "instances");
|
||||
if (!Directory.Exists(instances)) return null;
|
||||
int count = Directory.GetDirectories(instances).Count(d => File.Exists(System.IO.Path.Combine(d, "instance.cfg")));
|
||||
return new FoundLauncher("mmc", name, root, count, count == 1 ? "Instanz" : "Instanzen");
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, string> MultiMcExes = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["MultiMC.exe"] = "MultiMC",
|
||||
["prismlauncher.exe"] = "Prism Launcher",
|
||||
["polymc.exe"] = "PolyMC",
|
||||
};
|
||||
|
||||
/// <summary>Ordner von MultiMC-artigen Launchern, auf die eine Verknüpfung (Desktop/Startmenü) zeigt.</summary>
|
||||
private static IEnumerable<(string Name, string Root)> PortableMultiMcFromShortcuts()
|
||||
{
|
||||
var places = new[]
|
||||
{
|
||||
(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), SearchOption.TopDirectoryOnly),
|
||||
(Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory), SearchOption.TopDirectoryOnly),
|
||||
(Environment.GetFolderPath(Environment.SpecialFolder.Programs), SearchOption.AllDirectories),
|
||||
(Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms), SearchOption.AllDirectories),
|
||||
};
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var (dir, option) in places)
|
||||
{
|
||||
string[] links;
|
||||
try { links = string.IsNullOrEmpty(dir) || !Directory.Exists(dir) ? [] : Directory.GetFiles(dir, "*.lnk", option); }
|
||||
catch { continue; }
|
||||
foreach (var link in links)
|
||||
{
|
||||
var target = Shortcut.Target(link);
|
||||
if (string.IsNullOrEmpty(target) || !MultiMcExes.TryGetValue(System.IO.Path.GetFileName(target), out var name)) continue;
|
||||
var root = System.IO.Path.GetDirectoryName(target);
|
||||
if (root is null || !Directory.Exists(System.IO.Path.Combine(root, "instances")) || !seen.Add(root)) continue;
|
||||
yield return (name, root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool SamePath(string a, string b) =>
|
||||
string.Equals(System.IO.Path.GetFullPath(a).TrimEnd('\\'), System.IO.Path.GetFullPath(b).TrimEnd('\\'), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
// ---------- Technic ----------
|
||||
|
||||
private static FoundLauncher? DetectTechnic()
|
||||
{
|
||||
var modpacks = System.IO.Path.Combine(AppData, ".technic", "modpacks");
|
||||
if (!Directory.Exists(modpacks)) return null;
|
||||
string[] markers = ["bin", "mods", "config", "saves", "resourcepacks", "options.txt"];
|
||||
int count = Directory.GetDirectories(modpacks).Count(d => markers.Any(m => Exists(d, m)));
|
||||
return new FoundLauncher("technic", "Technic Launcher", modpacks, count, count == 1 ? "Modpack" : "Modpacks");
|
||||
}
|
||||
|
||||
private static bool Exists(string dir, string name)
|
||||
{
|
||||
var p = System.IO.Path.Combine(dir, name);
|
||||
return File.Exists(p) || Directory.Exists(p);
|
||||
}
|
||||
|
||||
// ---------- Übergabe an den Launcher ----------
|
||||
|
||||
/// <summary>Merkt die ausgewählten Launcher für den ersten Start von AeroMC vor (oder räumt eine alte Vormerkung weg).</summary>
|
||||
public static void WritePending(IReadOnlyCollection<FoundLauncher> selected)
|
||||
{
|
||||
if (selected.Count == 0)
|
||||
{
|
||||
try { if (File.Exists(PendingFile)) File.Delete(PendingFile); } catch { }
|
||||
return;
|
||||
}
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
["sources"] = selected.Select(s => new Dictionary<string, string> { ["kind"] = s.Kind, ["path"] = s.Path }).ToList(),
|
||||
};
|
||||
Directory.CreateDirectory(Installer.DataDir);
|
||||
File.WriteAllText(PendingFile, new JavaScriptSerializer().Serialize(data), new UTF8Encoding(false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
<Window x:Class="AeroMCSetup.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="AeroMC Setup" Icon="app.ico" Width="780" Height="520" ResizeMode="CanMinimize"
|
||||
Background="{DynamicResource LayerBrush}" Foreground="{DynamicResource TextBrush}"
|
||||
FontFamily="{DynamicResource UiFont}" FontSize="14"
|
||||
TextOptions.TextFormattingMode="Display" UseLayoutRounding="True"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
|
||||
<WindowChrome.WindowChrome>
|
||||
<WindowChrome CaptionHeight="36" ResizeBorderThickness="0" GlassFrameThickness="0,0,0,1"
|
||||
CornerRadius="0" UseAeroCaptionButtons="False"/>
|
||||
</WindowChrome.WindowChrome>
|
||||
|
||||
<Window.Resources>
|
||||
<!-- Markenfarben der Seitenleiste (in Hell und Dunkel gleich) -->
|
||||
<SolidColorBrush x:Key="SideText" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="SideTextSecondary" Color="#B9C8E8"/>
|
||||
<SolidColorBrush x:Key="SideTextMuted" Color="#7F95C2"/>
|
||||
<LinearGradientBrush x:Key="BrandGradient" StartPoint="0,0" EndPoint="1,0">
|
||||
<GradientStop Color="#2F6FDB" Offset="0"/>
|
||||
<GradientStop Color="#4FB8F0" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
|
||||
<Style x:Key="PageTitle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{DynamicResource DisplayFont}"/>
|
||||
<Setter Property="FontSize" Value="26"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||
</Style>
|
||||
<Style x:Key="Body" TargetType="TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
<Setter Property="LineHeight" Value="21"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextSecondaryBrush}"/>
|
||||
</Style>
|
||||
<Style x:Key="StepDot" TargetType="Border">
|
||||
<Setter Property="Width" Value="26"/>
|
||||
<Setter Property="Height" Value="26"/>
|
||||
<Setter Property="CornerRadius" Value="13"/>
|
||||
<Setter Property="BorderThickness" Value="1.5"/>
|
||||
</Style>
|
||||
<Style x:Key="StepNum" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
<Style x:Key="StepLabel" TargetType="TextBlock">
|
||||
<Setter Property="Margin" Value="12,0,0,0"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="FontSize" Value="13.5"/>
|
||||
</Style>
|
||||
<Style x:Key="StepLine" TargetType="Border">
|
||||
<Setter Property="Width" Value="2"/>
|
||||
<Setter Property="Height" Value="18"/>
|
||||
<Setter Property="Margin" Value="12,3,0,3"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||
<Setter Property="CornerRadius" Value="1"/>
|
||||
</Style>
|
||||
<Style x:Key="FeatureIcon" TargetType="Border">
|
||||
<Setter Property="Width" Value="38"/>
|
||||
<Setter Property="Height" Value="38"/>
|
||||
<Setter Property="CornerRadius" Value="10"/>
|
||||
<Setter Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||
<Setter Property="VerticalAlignment" Value="Top"/>
|
||||
</Style>
|
||||
<Style x:Key="FeatureGlyph" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{DynamicResource IconFont}"/>
|
||||
<Setter Property="FontSize" Value="17"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource AccentBrush}"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
<Style x:Key="FeatureTitle" TargetType="TextBlock">
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="FontSize" Value="13.5"/>
|
||||
</Style>
|
||||
<Style x:Key="FeatureText" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
<Setter Property="Margin" Value="0,2,0,0"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextSecondaryBrush}"/>
|
||||
</Style>
|
||||
<Style x:Key="Card" TargetType="Border">
|
||||
<Setter Property="Padding" Value="16,14"/>
|
||||
<Setter Property="CornerRadius" Value="8"/>
|
||||
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="36"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="250"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- ===== Seitenleiste im AeroMC-Look ===== -->
|
||||
<Border Grid.RowSpan="3" ClipToBounds="True">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="0.6,1">
|
||||
<GradientStop Color="#0C1630" Offset="0"/>
|
||||
<GradientStop Color="#112452" Offset="0.55"/>
|
||||
<GradientStop Color="#173A86" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
<Grid>
|
||||
<!-- Lichtschein hinter dem Logo und unten -->
|
||||
<Ellipse Width="320" Height="320" Margin="-120,-110,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" IsHitTestVisible="False">
|
||||
<Ellipse.Fill>
|
||||
<RadialGradientBrush>
|
||||
<GradientStop Color="#664A86EA" Offset="0"/>
|
||||
<GradientStop Color="#004A86EA" Offset="1"/>
|
||||
</RadialGradientBrush>
|
||||
</Ellipse.Fill>
|
||||
</Ellipse>
|
||||
<Ellipse Width="300" Height="300" Margin="0,0,-150,-150" HorizontalAlignment="Right" VerticalAlignment="Bottom" IsHitTestVisible="False">
|
||||
<Ellipse.Fill>
|
||||
<RadialGradientBrush>
|
||||
<GradientStop Color="#444FB8F0" Offset="0"/>
|
||||
<GradientStop Color="#004FB8F0" Offset="1"/>
|
||||
</RadialGradientBrush>
|
||||
</Ellipse.Fill>
|
||||
</Ellipse>
|
||||
|
||||
<StackPanel Margin="30,64,24,0">
|
||||
<Image Source="app.png" Width="68" Height="68" HorizontalAlignment="Left" RenderOptions.BitmapScalingMode="HighQuality">
|
||||
<Image.Effect>
|
||||
<DropShadowEffect Color="#4A86EA" BlurRadius="28" ShadowDepth="0" Opacity="0.75"/>
|
||||
</Image.Effect>
|
||||
</Image>
|
||||
<TextBlock Text="AeroMC" FontFamily="{DynamicResource DisplayFont}" FontSize="26" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource SideText}" Margin="0,16,0,0"/>
|
||||
<TextBlock x:Name="VersionText" Foreground="{StaticResource SideTextSecondary}" FontSize="12" Margin="0,2,0,0"/>
|
||||
|
||||
<StackPanel x:Name="Steps" Margin="0,38,0,0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border x:Name="Dot0" Style="{StaticResource StepDot}"><TextBlock x:Name="Num0" Text="1" Style="{StaticResource StepNum}"/></Border>
|
||||
<TextBlock x:Name="Label0" Text="Willkommen" Style="{StaticResource StepLabel}"/>
|
||||
</StackPanel>
|
||||
<Border x:Name="Line0" Style="{StaticResource StepLine}"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border x:Name="Dot1" Style="{StaticResource StepDot}"><TextBlock x:Name="Num1" Text="2" Style="{StaticResource StepNum}"/></Border>
|
||||
<TextBlock x:Name="Label1" Text="Lizenz" Style="{StaticResource StepLabel}"/>
|
||||
</StackPanel>
|
||||
<Border x:Name="Line1" Style="{StaticResource StepLine}"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border x:Name="Dot2" Style="{StaticResource StepDot}"><TextBlock x:Name="Num2" Text="3" Style="{StaticResource StepNum}"/></Border>
|
||||
<TextBlock x:Name="Label2" Text="Optionen" Style="{StaticResource StepLabel}"/>
|
||||
</StackPanel>
|
||||
<Border x:Name="Line2" Style="{StaticResource StepLine}"/>
|
||||
<!-- Import: nur sichtbar, wenn andere Launcher gefunden wurden -->
|
||||
<StackPanel x:Name="ImportStepRow" Orientation="Horizontal">
|
||||
<Border x:Name="Dot3" Style="{StaticResource StepDot}"><TextBlock x:Name="Num3" Text="4" Style="{StaticResource StepNum}"/></Border>
|
||||
<TextBlock x:Name="Label3" Text="Import" Style="{StaticResource StepLabel}"/>
|
||||
</StackPanel>
|
||||
<Border x:Name="Line3" Style="{StaticResource StepLine}"/>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border x:Name="Dot4" Style="{StaticResource StepDot}"><TextBlock x:Name="Num4" Text="5" Style="{StaticResource StepNum}"/></Border>
|
||||
<TextBlock x:Name="Label4" Text="Installation" Style="{StaticResource StepLabel}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="© M_Viper · m-viper.de" FontSize="11" Foreground="{StaticResource SideTextMuted}"
|
||||
VerticalAlignment="Bottom" Margin="30,0,0,20"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Titelleiste -->
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="14,0,0,0">
|
||||
<Image Source="app.ico" Width="16" Height="16" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
<TextBlock x:Name="WindowTitle" Text="AeroMC Setup" FontSize="12" Margin="10,0,0,0" VerticalAlignment="Center"
|
||||
Foreground="{StaticResource SideTextSecondary}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top">
|
||||
<Button Style="{StaticResource CaptionButton}" Content="" Click="Minimize_Click"
|
||||
WindowChrome.IsHitTestVisibleInChrome="True"/>
|
||||
<Button x:Name="CloseButton" Style="{StaticResource CloseCaptionButton}" Content="" Click="Cancel_Click"
|
||||
WindowChrome.IsHitTestVisibleInChrome="True"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- ===== Inhalt ===== -->
|
||||
<Grid Grid.Row="1" Grid.Column="1" Margin="40,18,40,0">
|
||||
|
||||
<!-- 0: Willkommen -->
|
||||
<StackPanel x:Name="PageWelcome">
|
||||
<TextBlock Text="Willkommen bei AeroMC" Style="{StaticResource PageTitle}"/>
|
||||
<TextBlock Style="{StaticResource Body}"
|
||||
Text="Der Assistent richtet AeroMC in wenigen Schritten ein – nur für dein Benutzerkonto und ohne Administratorrechte."/>
|
||||
|
||||
<Grid Margin="0,24,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="20"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="18"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<DockPanel>
|
||||
<Border Style="{StaticResource FeatureIcon}" DockPanel.Dock="Left"><TextBlock Text="" Style="{StaticResource FeatureGlyph}"/></Border>
|
||||
<StackPanel Margin="12,0,0,0">
|
||||
<TextBlock Text="Getrennte Instanzen" Style="{StaticResource FeatureTitle}"/>
|
||||
<TextBlock Text="Mods und Modpacks von Modrinth und CurseForge" Style="{StaticResource FeatureText}"/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
<DockPanel Grid.Column="2">
|
||||
<Border Style="{StaticResource FeatureIcon}" DockPanel.Dock="Left"><TextBlock Text="" Style="{StaticResource FeatureGlyph}"/></Border>
|
||||
<StackPanel Margin="12,0,0,0">
|
||||
<TextBlock Text="Spielaufzeichnung" Style="{StaticResource FeatureTitle}"/>
|
||||
<TextBlock Text="Als MP4, ganz ohne Zusatzprogramm" Style="{StaticResource FeatureText}"/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
<DockPanel Grid.Row="2">
|
||||
<Border Style="{StaticResource FeatureIcon}" DockPanel.Dock="Left"><TextBlock Text="" Style="{StaticResource FeatureGlyph}"/></Border>
|
||||
<StackPanel Margin="12,0,0,0">
|
||||
<TextBlock Text="Freunde" Style="{StaticResource FeatureTitle}"/>
|
||||
<TextBlock Text="Online-Status und Meldungen im Spiel" Style="{StaticResource FeatureText}"/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
<DockPanel Grid.Row="2" Grid.Column="2">
|
||||
<Border Style="{StaticResource FeatureIcon}" DockPanel.Dock="Left"><TextBlock Text="" Style="{StaticResource FeatureGlyph}"/></Border>
|
||||
<StackPanel Margin="12,0,0,0">
|
||||
<TextBlock Text="Microsoft-Konten" Style="{StaticResource FeatureTitle}"/>
|
||||
<TextBlock Text="Mehrere Konten, Skins direkt ändern" Style="{StaticResource FeatureText}"/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
|
||||
<Border x:Name="UpdateHint" Margin="0,26,0,0" Padding="14,10" CornerRadius="8" Visibility="Collapsed"
|
||||
Background="{DynamicResource AccentSubtleBrush}" BorderBrush="{DynamicResource AccentSubtleStrokeBrush}" BorderThickness="1">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"
|
||||
Foreground="{DynamicResource AccentBrush}" Margin="0,2,12,0" VerticalAlignment="Top"/>
|
||||
<TextBlock x:Name="UpdateHintText" TextWrapping="Wrap" LineHeight="20" FontSize="13"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<Border x:Name="RunningHint" Margin="0,10,0,0" Padding="14,10" CornerRadius="8" Visibility="Collapsed"
|
||||
Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"
|
||||
Foreground="{DynamicResource AccentBrush}" Margin="0,2,12,0" VerticalAlignment="Top"/>
|
||||
<TextBlock TextWrapping="Wrap" LineHeight="20" FontSize="13"
|
||||
Text="AeroMC läuft gerade und wird für die Installation automatisch beendet."/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 1: Lizenz -->
|
||||
<DockPanel x:Name="PageLicense" Visibility="Collapsed">
|
||||
<TextBlock DockPanel.Dock="Top" Text="Lizenz" Style="{StaticResource PageTitle}"/>
|
||||
<TextBlock DockPanel.Dock="Top" Style="{StaticResource Body}" Margin="0,0,0,14"
|
||||
Text="Bitte lies die Nutzungsbedingungen. AeroMC ist kostenlos für den privaten Gebrauch."/>
|
||||
<CheckBox x:Name="AcceptLicense" DockPanel.Dock="Bottom" Margin="0,14,0,0"
|
||||
Checked="AcceptLicense_Click" Unchecked="AcceptLicense_Click"
|
||||
Content="Ich akzeptiere die Lizenz- und Nutzungsbedingungen"/>
|
||||
<Border Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrokeBrush}"
|
||||
BorderThickness="1" CornerRadius="8">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<!-- wird aus LICENSE aufgebaut: Überschriften, Absätze, Aufzählungen (siehe ZeigeLizenz) -->
|
||||
<StackPanel x:Name="LicensePanel" Margin="16,12,16,14"/>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</DockPanel>
|
||||
|
||||
<!-- 2: Optionen -->
|
||||
<StackPanel x:Name="PageOptions" Visibility="Collapsed">
|
||||
<TextBlock Text="Optionen" Style="{StaticResource PageTitle}"/>
|
||||
<TextBlock Style="{StaticResource Body}" Text="Wohin soll AeroMC installiert werden?"/>
|
||||
|
||||
<Border Style="{StaticResource Card}" Margin="0,18,0,0">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Installationsordner" FontWeight="SemiBold" FontSize="13" Margin="0,0,0,10"/>
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Content="Ändern …" Margin="8,0,0,0" Click="ChangeDir_Click"/>
|
||||
<TextBox x:Name="DirBox"/>
|
||||
</DockPanel>
|
||||
<TextBlock Style="{StaticResource Body}" FontSize="12" LineHeight="18" Margin="0,10,0,0"
|
||||
Text="Benötigt ca. 400 MB. Deine Instanzen, Welten und Einstellungen liegen getrennt davon und bleiben bei Updates erhalten."/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Style="{StaticResource Card}" Margin="0,12,0,0">
|
||||
<StackPanel>
|
||||
<CheckBox x:Name="DesktopLinkBox" Content="Verknüpfung auf dem Desktop" IsChecked="True"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 3: Import aus anderen Launchern (optional) -->
|
||||
<StackPanel x:Name="PageImport" Visibility="Collapsed">
|
||||
<TextBlock Text="Import" Style="{StaticResource PageTitle}"/>
|
||||
<TextBlock Style="{StaticResource Body}"
|
||||
Text="Optional: Diese Launcher wurden auf deinem PC gefunden. Was du ankreuzt, bietet AeroMC beim ersten Start zum Übernehmen an – dort wählst du die einzelnen Instanzen aus."/>
|
||||
<Border Style="{StaticResource Card}" Margin="0,18,0,0" Padding="16,6">
|
||||
<StackPanel x:Name="ImportList"/>
|
||||
</Border>
|
||||
<TextBlock Style="{StaticResource Body}" FontSize="12" LineHeight="18" Margin="0,12,0,0"
|
||||
Text="In den anderen Launchern wird nichts verändert – AeroMC legt Kopien an. Später geht das jederzeit über „Importieren“ in der Toolleiste."/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 4: Fortschritt / Fertig -->
|
||||
<StackPanel x:Name="PageProgress" Visibility="Collapsed">
|
||||
<Border x:Name="SuccessIcon" Width="64" Height="64" CornerRadius="32" HorizontalAlignment="Left" Margin="0,4,0,20"
|
||||
Background="{StaticResource BrandGradient}" RenderTransformOrigin="0.5,0.5" Visibility="Collapsed">
|
||||
<Border.RenderTransform><ScaleTransform x:Name="SuccessScale" ScaleX="1" ScaleY="1"/></Border.RenderTransform>
|
||||
<Border.Effect><DropShadowEffect Color="#4A86EA" BlurRadius="24" ShadowDepth="0" Opacity="0.55"/></Border.Effect>
|
||||
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="26" Foreground="White"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock x:Name="ProgressTitle" Text="AeroMC wird installiert …" Style="{StaticResource PageTitle}"/>
|
||||
|
||||
<StackPanel x:Name="ProgressBarPanel" Margin="0,18,0,0">
|
||||
<TextBlock x:Name="PercentText" Text="0 %" FontFamily="{DynamicResource DisplayFont}" FontSize="44" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AccentBrush}"/>
|
||||
<Border x:Name="ProgressTrack" SizeChanged="ProgressTrack_SizeChanged" Height="8" CornerRadius="4" Margin="0,10,0,0" Background="{DynamicResource SubtleSelectedBrush}">
|
||||
<Border x:Name="ProgressFill" HorizontalAlignment="Left" Width="0" CornerRadius="4" Background="{StaticResource BrandGradient}"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<TextBlock x:Name="ProgressText" Style="{StaticResource Body}" Margin="0,12,0,0" Text="Einen Moment bitte."/>
|
||||
|
||||
<StackPanel x:Name="DonePanel" Visibility="Collapsed">
|
||||
<CheckBox x:Name="LaunchBox" Content="AeroMC jetzt starten" IsChecked="True" Margin="0,24,0,0"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Deinstallation -->
|
||||
<StackPanel x:Name="PageUninstall" Visibility="Collapsed">
|
||||
<TextBlock Text="AeroMC deinstallieren" Style="{StaticResource PageTitle}"/>
|
||||
<TextBlock Style="{StaticResource Body}"
|
||||
Text="AeroMC wird von deinem PC entfernt: Programm, Verknüpfungen, Autostart und der Eintrag in den Windows-Einstellungen."/>
|
||||
<Border Style="{StaticResource Card}" Margin="0,22,0,0">
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="15" Foreground="{DynamicResource AccentBrush}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock Text="Deine Instanzen und Welten bleiben erhalten" FontWeight="SemiBold" Margin="10,0,0,0" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<TextBlock x:Name="DataInfo" Style="{StaticResource Body}" FontSize="12.5" Margin="0,6,0,14"/>
|
||||
<CheckBox x:Name="RemoveDataBox" Content="Auch Instanzen, Welten, Konten und Einstellungen löschen"
|
||||
Checked="RemoveDataBox_Click" Unchecked="RemoveDataBox_Click"/>
|
||||
<TextBlock x:Name="RemoveDataWarning" Visibility="Collapsed" Margin="30,8,0,0" TextWrapping="Wrap" FontSize="12.5"
|
||||
Foreground="{DynamicResource DangerBrush}"
|
||||
Text="Achtung: Damit wandern alle Welten und Spielstände aus AeroMC in den Papierkorb. Aufnahmen (Videos) und Backups (Dokumente) bleiben erhalten."/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Fußleiste -->
|
||||
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,14,40,24">
|
||||
<Button x:Name="BackButton" Content="Zurück" Width="110" Margin="0,0,8,0" Click="Back_Click"/>
|
||||
<Button x:Name="NextButton" Content="Weiter" Width="140" Margin="0,0,8,0" Style="{StaticResource AccentButton}" IsDefault="True" Click="Next_Click"/>
|
||||
<Button x:Name="CancelButton" Content="Abbrechen" Width="110" Click="Cancel_Click"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Eigene Rückfrage im Setup-Look statt Windows-Meldungsfenster (siehe Frage()) -->
|
||||
<Grid x:Name="DialogOverlay" Grid.RowSpan="3" Grid.ColumnSpan="2" Visibility="Collapsed" Background="#99000000">
|
||||
<Border x:Name="DialogCard" Width="420" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Background="{DynamicResource LayerBrush}" BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1"
|
||||
CornerRadius="10" Padding="26,22,22,18" Effect="{StaticResource DialogShadow}" RenderTransformOrigin="0.5,0.5">
|
||||
<Border.RenderTransform><ScaleTransform x:Name="DialogScale" ScaleX="1" ScaleY="1"/></Border.RenderTransform>
|
||||
<StackPanel>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border Width="34" Height="34" CornerRadius="17" Background="{DynamicResource AccentSubtleBrush}" VerticalAlignment="Top">
|
||||
<TextBlock x:Name="DialogIcon" Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"
|
||||
Foreground="{DynamicResource AccentBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock x:Name="DialogTitle" FontFamily="{DynamicResource DisplayFont}" FontSize="18" FontWeight="SemiBold"
|
||||
VerticalAlignment="Center" Margin="12,0,0,0" TextWrapping="Wrap" MaxWidth="320"/>
|
||||
</StackPanel>
|
||||
<TextBlock x:Name="DialogText" TextWrapping="Wrap" LineHeight="20" Margin="46,8,0,0"
|
||||
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,22,0,0">
|
||||
<Button x:Name="DialogSecondary" MinWidth="120" Margin="0,0,8,0" Click="DialogSecondary_Click"/>
|
||||
<Button x:Name="DialogPrimary" MinWidth="130" Style="{StaticResource AccentButton}" Click="DialogPrimary_Click"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,620 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace AeroMCSetup;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly SetupMode _mode;
|
||||
private int _page;
|
||||
private bool _busy;
|
||||
private bool _done;
|
||||
private string? _installedExe;
|
||||
// andere Launcher für den optionalen Import (leer → Import-Schritt entfällt)
|
||||
private List<FoundLauncher> _found = [];
|
||||
private bool HasImport => _found.Count > 0;
|
||||
|
||||
private const int PageNumImport = 3;
|
||||
private const int PageNumProgress = 4;
|
||||
|
||||
public MainWindow(SetupMode mode)
|
||||
{
|
||||
InitializeComponent();
|
||||
_mode = mode;
|
||||
VersionText.Text = $"Version {Installer.Version}";
|
||||
ZeigeLizenz(Installer.LicenseText);
|
||||
|
||||
var installed = Installer.InstalledDir;
|
||||
DirBox.Text = installed ?? Installer.DefaultDir;
|
||||
DesktopLinkBox.IsChecked = installed is null || Installer.DesktopLinkExists;
|
||||
|
||||
if (installed is not null)
|
||||
{
|
||||
UpdateHint.Visibility = Visibility.Visible;
|
||||
UpdateHintText.Text = Installer.InstalledVersion == Installer.Version
|
||||
? $"AeroMC {Installer.Version} ist bereits installiert und wird neu installiert. Deine Instanzen, Welten und Einstellungen bleiben erhalten."
|
||||
: $"Die vorhandene Installation{(Installer.InstalledVersion is { } v ? $" (Version {v})" : "")} wird durch Version {Installer.Version} ersetzt. Deine Instanzen, Welten und Einstellungen bleiben erhalten.";
|
||||
}
|
||||
if (Installer.IsRunning(installed)) RunningHint.Visibility = Visibility.Visible;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case SetupMode.Uninstall:
|
||||
Title = WindowTitle.Text = "AeroMC deinstallieren";
|
||||
Steps.Visibility = Visibility.Collapsed;
|
||||
ShowOnly(PageUninstall);
|
||||
NextButton.Content = "Deinstallieren";
|
||||
BackButton.Visibility = Visibility.Collapsed;
|
||||
LoadDataInfo();
|
||||
break;
|
||||
case SetupMode.Update:
|
||||
// aus dem Launcher heraus: ohne Rückfragen direkt aktualisieren und danach wieder starten
|
||||
Title = WindowTitle.Text = "AeroMC wird aktualisiert";
|
||||
Steps.Visibility = Visibility.Collapsed;
|
||||
Loaded += async (_, _) => await RunInstall(DirBox.Text, DesktopLinkBox.IsChecked == true, update: true);
|
||||
ShowOnly(PageProgress);
|
||||
ProgressTitle.Text = "AeroMC wird aktualisiert …";
|
||||
BackButton.Visibility = Visibility.Hidden;
|
||||
break;
|
||||
default:
|
||||
SetupImportPage();
|
||||
ShowPage(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Lizenztext ----------
|
||||
|
||||
/// <summary>
|
||||
/// LICENSE ist für die Textansicht auf ~80 Zeichen umbrochen und eingerückt. Hier wird das
|
||||
/// wieder zu fließendem Text zusammengesetzt, damit der Umbruch zur Fensterbreite passt:
|
||||
/// Leerzeilen trennen Abschnitte, "1. …" sind Überschriften, "- …" Aufzählungspunkte
|
||||
/// (Folgezeilen gehören zum Punkt), alles andere ist Absatztext.
|
||||
/// </summary>
|
||||
private void ZeigeLizenz(string text)
|
||||
{
|
||||
LicensePanel.Children.Clear();
|
||||
var abschnitte = new List<List<string>>();
|
||||
var aktuell = new List<string>();
|
||||
foreach (var zeile in text.Replace("\r", "").Split('\n'))
|
||||
{
|
||||
if (zeile.Trim().Length == 0)
|
||||
{
|
||||
if (aktuell.Count > 0) { abschnitte.Add(aktuell); aktuell = []; }
|
||||
}
|
||||
else aktuell.Add(zeile);
|
||||
}
|
||||
if (aktuell.Count > 0) abschnitte.Add(aktuell);
|
||||
|
||||
for (int a = 0; a < abschnitte.Count; a++)
|
||||
{
|
||||
var abschnitt = abschnitte[a];
|
||||
int i = 0;
|
||||
if (a == 0)
|
||||
{
|
||||
LicensePanel.Children.Add(LizenzText(abschnitt[0].Trim(), 14, FontWeights.SemiBold, "TextBrush", 0));
|
||||
i = 1;
|
||||
}
|
||||
else if (System.Text.RegularExpressions.Regex.IsMatch(abschnitt[0], @"^\s*\d+\.\s"))
|
||||
{
|
||||
LicensePanel.Children.Add(LizenzText(abschnitt[0].Trim(), 13, FontWeights.SemiBold, "TextBrush", 14));
|
||||
i = 1;
|
||||
}
|
||||
|
||||
// nach einer Überschrift eng anschließen, sonst Abstand zum vorigen Abschnitt
|
||||
double absatzAbstand = i == 1 && a > 0 ? 4 : 10;
|
||||
var absatz = new System.Text.StringBuilder();
|
||||
string? punkt = null;
|
||||
void AbsatzFertig()
|
||||
{
|
||||
if (absatz.Length == 0) return;
|
||||
LicensePanel.Children.Add(LizenzText(absatz.ToString(), 12.5, FontWeights.Normal, "TextSecondaryBrush", absatzAbstand));
|
||||
absatz.Clear();
|
||||
}
|
||||
void PunktFertig()
|
||||
{
|
||||
if (punkt is null) return;
|
||||
LicensePanel.Children.Add(LizenzPunkt(punkt));
|
||||
punkt = null;
|
||||
}
|
||||
|
||||
for (; i < abschnitt.Count; i++)
|
||||
{
|
||||
var t = abschnitt[i].Trim();
|
||||
if (t.StartsWith("- "))
|
||||
{
|
||||
AbsatzFertig();
|
||||
PunktFertig();
|
||||
punkt = t.Substring(2).Trim();
|
||||
}
|
||||
else if (punkt is not null) punkt += " " + t;
|
||||
else absatz.Append(absatz.Length > 0 ? " " : "").Append(t);
|
||||
}
|
||||
AbsatzFertig();
|
||||
PunktFertig();
|
||||
}
|
||||
}
|
||||
|
||||
private TextBlock LizenzText(string text, double groesse, FontWeight gewicht, string farbe, double abstandOben)
|
||||
{
|
||||
var tb = new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
FontSize = groesse,
|
||||
FontWeight = gewicht,
|
||||
LineHeight = groesse + 6,
|
||||
Margin = new Thickness(0, abstandOben, 0, 0),
|
||||
};
|
||||
tb.SetResourceReference(TextBlock.ForegroundProperty, farbe);
|
||||
return tb;
|
||||
}
|
||||
|
||||
// Aufzählungspunkt mit hängendem Einzug (umbrochene Zeilen beginnen unter dem Text, nicht unter dem Punkt)
|
||||
private Grid LizenzPunkt(string text)
|
||||
{
|
||||
var grid = new Grid { Margin = new Thickness(4, 4, 0, 0) };
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(16) });
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
var punkt = LizenzText("•", 12.5, FontWeights.Normal, "AccentBrush", 0);
|
||||
var inhalt = LizenzText(text, 12.5, FontWeights.Normal, "TextSecondaryBrush", 0);
|
||||
Grid.SetColumn(inhalt, 1);
|
||||
grid.Children.Add(punkt);
|
||||
grid.Children.Add(inhalt);
|
||||
return grid;
|
||||
}
|
||||
|
||||
// ---------- Seiten ----------
|
||||
|
||||
private UIElement? _shownPage;
|
||||
|
||||
/// <summary>Seite wechseln – mit kurzem Einblenden von rechts.</summary>
|
||||
private void ShowOnly(UIElement page)
|
||||
{
|
||||
foreach (var p in new UIElement[] { PageWelcome, PageLicense, PageOptions, PageImport, PageProgress, PageUninstall })
|
||||
p.Visibility = p == page ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (page == _shownPage) return;
|
||||
_shownPage = page;
|
||||
|
||||
var ease = new CubicEase { EasingMode = EasingMode.EaseOut };
|
||||
var slide = new TranslateTransform(18, 0);
|
||||
page.RenderTransform = slide;
|
||||
slide.BeginAnimation(TranslateTransform.XProperty, new DoubleAnimation(18, 0, TimeSpan.FromMilliseconds(260)) { EasingFunction = ease });
|
||||
page.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(220)));
|
||||
}
|
||||
|
||||
// Farben der Schritt-Anzeige in der (immer dunkelblauen) Seitenleiste
|
||||
private static readonly Brush StepDoneFill = Frozen(Color.FromRgb(0x4A, 0x86, 0xEA));
|
||||
private static readonly Brush StepCurrentFill = Brushes.White;
|
||||
private static readonly Brush StepCurrentText = Frozen(Color.FromRgb(0x1C, 0x4E, 0xB8));
|
||||
private static readonly Brush StepUpcoming = Frozen(Color.FromRgb(0x7F, 0x95, 0xC2));
|
||||
private static readonly Brush StepUpcomingStroke = Frozen(Color.FromRgb(0x46, 0x5E, 0x92));
|
||||
private static readonly Brush StepDoneText = Frozen(Color.FromRgb(0xB9, 0xC8, 0xE8));
|
||||
private static readonly Brush StepLineIdle = Frozen(Color.FromRgb(0x2E, 0x45, 0x78));
|
||||
|
||||
private static Brush Frozen(Color c)
|
||||
{
|
||||
var b = new SolidColorBrush(c);
|
||||
b.Freeze();
|
||||
return b;
|
||||
}
|
||||
|
||||
private void UpdateSteps(int page)
|
||||
{
|
||||
var dots = new[] { Dot0, Dot1, Dot2, Dot3, Dot4 };
|
||||
var nums = new[] { Num0, Num1, Num2, Num3, Num4 };
|
||||
var labels = new[] { Label0, Label1, Label2, Label3, Label4 };
|
||||
var lines = new[] { Line0, Line1, Line2, Line3 };
|
||||
var iconFont = (FontFamily)FindResource("IconFont");
|
||||
var uiFont = (FontFamily)FindResource("UiFont");
|
||||
|
||||
// ohne gefundene Launcher fällt der Import-Schritt weg → Nummern rücken auf
|
||||
ImportStepRow.Visibility = Line3.Visibility = HasImport ? Visibility.Visible : Visibility.Collapsed;
|
||||
int number = 0;
|
||||
for (int i = 0; i < dots.Length; i++)
|
||||
{
|
||||
if (i == PageNumImport && !HasImport) continue;
|
||||
number++;
|
||||
bool done = i < page || (_done && i == page);
|
||||
bool current = i == page && !done;
|
||||
dots[i].Background = done ? StepDoneFill : current ? StepCurrentFill : Brushes.Transparent;
|
||||
dots[i].BorderBrush = done ? StepDoneFill : current ? StepCurrentFill : StepUpcomingStroke;
|
||||
nums[i].Text = done ? "" : number.ToString();
|
||||
nums[i].FontFamily = done ? iconFont : uiFont;
|
||||
nums[i].FontSize = done ? 11 : 12;
|
||||
nums[i].Foreground = done ? Brushes.White : current ? StepCurrentText : StepUpcoming;
|
||||
labels[i].Foreground = current ? Brushes.White : done ? StepDoneText : StepUpcoming;
|
||||
labels[i].FontWeight = current ? FontWeights.SemiBold : FontWeights.Normal;
|
||||
}
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
lines[i].Background = i < page ? StepDoneFill : StepLineIdle;
|
||||
}
|
||||
|
||||
private void ShowPage(int page)
|
||||
{
|
||||
_page = page;
|
||||
ShowOnly(page switch { 0 => PageWelcome, 1 => PageLicense, 2 => PageOptions, PageNumImport => PageImport, _ => PageProgress });
|
||||
UpdateSteps(page);
|
||||
|
||||
BackButton.Visibility = page is 1 or 2 or PageNumImport ? Visibility.Visible : Visibility.Hidden;
|
||||
NextButton.Content = page switch
|
||||
{
|
||||
2 => HasImport ? "Weiter" : "Installieren",
|
||||
PageNumImport => "Installieren",
|
||||
PageNumProgress => "Fertigstellen",
|
||||
_ => "Weiter",
|
||||
};
|
||||
NextButton.IsEnabled = page != 1 || AcceptLicense.IsChecked == true;
|
||||
}
|
||||
|
||||
private void AcceptLicense_Click(object sender, RoutedEventArgs e) => NextButton.IsEnabled = AcceptLicense.IsChecked == true;
|
||||
|
||||
private void Back_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!_busy && _page > 0) ShowPage(_page - 1);
|
||||
}
|
||||
|
||||
private async void Next_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_busy) return;
|
||||
|
||||
if (_mode == SetupMode.Uninstall)
|
||||
{
|
||||
if (_done) { Close(); return; }
|
||||
await RunUninstall();
|
||||
return;
|
||||
}
|
||||
if (_mode == SetupMode.Update)
|
||||
{
|
||||
Close(); // nur nach einem Fehler erreichbar
|
||||
return;
|
||||
}
|
||||
|
||||
switch (_page)
|
||||
{
|
||||
case 0: ShowPage(1); break;
|
||||
case 1 when AcceptLicense.IsChecked == true: ShowPage(2); break;
|
||||
case 2:
|
||||
var dir = DirBox.Text.Trim();
|
||||
if (!IsFullPath(dir) || dir.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
|
||||
{
|
||||
await Frage("Ungültiger Ordner", "Bitte einen vollständigen Ordner angeben, z. B. C:\\Spiele\\AeroMC.", "OK");
|
||||
return;
|
||||
}
|
||||
if (dir.TrimEnd('\\', '/').Length > Installer.MaxDirLength)
|
||||
{
|
||||
await Frage("Ordnerpfad zu lang",
|
||||
$"Der Pfad hat {dir.TrimEnd('\\', '/').Length} Zeichen, Windows erlaubt hier höchstens {Installer.MaxDirLength}. " +
|
||||
"Bitte einen kürzeren Ordner wählen, z. B. C:\\Spiele\\AeroMC.", "OK");
|
||||
return;
|
||||
}
|
||||
if (HasImport) ShowPage(PageNumImport);
|
||||
else await RunInstall(dir, DesktopLinkBox.IsChecked == true, update: false);
|
||||
break;
|
||||
case PageNumImport:
|
||||
await RunInstall(DirBox.Text.Trim(), DesktopLinkBox.IsChecked == true, update: false);
|
||||
break;
|
||||
case PageNumProgress:
|
||||
// Start im Hintergrund: Process.Start kann blockieren, solange Windows
|
||||
// die EXE prüft – das Fenster darf dabei nie einfrieren
|
||||
if (LaunchBox.IsChecked == true)
|
||||
{
|
||||
_busy = true;
|
||||
NextButton.IsEnabled = false;
|
||||
NextButton.Content = "AeroMC startet …";
|
||||
await Task.Run(StartApp);
|
||||
_busy = false;
|
||||
}
|
||||
Close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Import aus anderen Launchern ----------
|
||||
|
||||
private void SetupImportPage()
|
||||
{
|
||||
try { _found = LauncherImport.Detect(); } catch { _found = []; }
|
||||
ImportList.Children.Clear();
|
||||
for (int i = 0; i < _found.Count; i++)
|
||||
{
|
||||
var f = _found[i];
|
||||
var text = new StackPanel { Margin = new Thickness(4, 0, 0, 0) };
|
||||
text.Children.Add(new TextBlock { Text = f.Name, FontWeight = FontWeights.SemiBold });
|
||||
var detail = new TextBlock
|
||||
{
|
||||
Text = $"{f.Count} {f.Unit} gefunden · {f.Path}",
|
||||
FontSize = 12,
|
||||
Margin = new Thickness(0, 2, 0, 0),
|
||||
TextTrimming = TextTrimming.CharacterEllipsis,
|
||||
ToolTip = f.Path,
|
||||
};
|
||||
detail.SetResourceReference(TextBlock.ForegroundProperty, "TextSecondaryBrush");
|
||||
text.Children.Add(detail);
|
||||
|
||||
var box = new CheckBox { Content = text, Tag = f, Margin = new Thickness(0, 10, 0, 10), IsChecked = false };
|
||||
if (i > 0)
|
||||
{
|
||||
var line = new Border { Height = 1 };
|
||||
line.SetResourceReference(Border.BackgroundProperty, "ControlStrokeBrush");
|
||||
ImportList.Children.Add(line);
|
||||
}
|
||||
ImportList.Children.Add(box);
|
||||
}
|
||||
}
|
||||
|
||||
private List<FoundLauncher> SelectedImports() =>
|
||||
ImportList.Children.OfType<CheckBox>()
|
||||
.Where(c => c.IsChecked == true && c.Tag is FoundLauncher)
|
||||
.Select(c => (FoundLauncher)c.Tag)
|
||||
.ToList();
|
||||
|
||||
private void ChangeDir_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
using var dlg = new System.Windows.Forms.FolderBrowserDialog
|
||||
{
|
||||
Description = "Installationsordner wählen",
|
||||
SelectedPath = Directory.Exists(DirBox.Text) ? DirBox.Text : Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
ShowNewFolderButton = true,
|
||||
};
|
||||
if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return;
|
||||
var chosen = dlg.SelectedPath;
|
||||
// wie bei üblichen Installern: Unterordner "AeroMC" anhängen
|
||||
DirBox.Text = Path.GetFileName(chosen.TrimEnd('\\')).Equals("AeroMC", StringComparison.OrdinalIgnoreCase)
|
||||
? chosen : Path.Combine(chosen, "AeroMC");
|
||||
}
|
||||
|
||||
// ---------- Installation ----------
|
||||
|
||||
private async Task RunInstall(string dir, bool desktop, bool update)
|
||||
{
|
||||
_busy = true;
|
||||
if (!update) ShowPage(PageNumProgress);
|
||||
SetButtons(false);
|
||||
var progress = new Progress<(double Value, string Text)>(p => SetProgress(p.Value, p.Text));
|
||||
try
|
||||
{
|
||||
await Task.Run(() => Installer.Install(dir, desktop, progress, waitForExit: update));
|
||||
_installedExe = Path.Combine(dir, Installer.ExeName);
|
||||
_done = true;
|
||||
|
||||
if (update)
|
||||
{
|
||||
ShowSuccess("Update abgeschlossen", $"AeroMC {Installer.Version} wird gestartet …");
|
||||
StartApp();
|
||||
_busy = false;
|
||||
var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1.5) };
|
||||
timer.Tick += (_, _) => { timer.Stop(); Close(); };
|
||||
timer.Start();
|
||||
return;
|
||||
}
|
||||
|
||||
// ausgewählte Launcher für den ersten Start vormerken (Fehler hier sollen die Installation nicht kippen)
|
||||
var selected = SelectedImports();
|
||||
try { LauncherImport.WritePending(selected); } catch { selected = []; }
|
||||
|
||||
ShowSuccess("Alles bereit!", "AeroMC ist installiert. Du findest es im Startmenü" + (desktop ? " und auf dem Desktop." : "."));
|
||||
if (selected.Count > 0)
|
||||
ProgressText.Text += $" Beim ersten Start bietet AeroMC dir die Instanzen aus {string.Join(" und ", selected.Select(f => f.Name))} zum Übernehmen an.";
|
||||
DonePanel.Visibility = Visibility.Visible;
|
||||
ShowPage(PageNumProgress);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ProgressTitle.Text = update ? "Update fehlgeschlagen" : "Installation fehlgeschlagen";
|
||||
ProgressBarPanel.Visibility = Visibility.Collapsed;
|
||||
ProgressText.Text = ex.Message;
|
||||
ProgressText.Foreground = (Brush)FindResource("DangerBrush");
|
||||
NextButton.Content = "Schließen";
|
||||
_installedExe = null;
|
||||
}
|
||||
_busy = false;
|
||||
NextButton.IsEnabled = true;
|
||||
CloseButton.IsEnabled = true;
|
||||
BackButton.Visibility = Visibility.Hidden;
|
||||
CancelButton.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
/// <summary>Fortschritt ausblenden und den Haken mit kurzem "Aufploppen" zeigen.</summary>
|
||||
private void ShowSuccess(string title, string text)
|
||||
{
|
||||
ProgressBarPanel.Visibility = Visibility.Collapsed;
|
||||
ProgressTitle.Text = title;
|
||||
ProgressText.Text = text;
|
||||
SuccessIcon.Visibility = Visibility.Visible;
|
||||
var pop = new DoubleAnimation(0.4, 1, TimeSpan.FromMilliseconds(420)) { EasingFunction = new BackEase { EasingMode = EasingMode.EaseOut, Amplitude = 0.5 } };
|
||||
SuccessScale.BeginAnimation(ScaleTransform.ScaleXProperty, pop);
|
||||
SuccessScale.BeginAnimation(ScaleTransform.ScaleYProperty, pop);
|
||||
SuccessIcon.BeginAnimation(OpacityProperty, new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(200)));
|
||||
}
|
||||
|
||||
private void StartApp()
|
||||
{
|
||||
if (_installedExe is null || !File.Exists(_installedExe)) return;
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(_installedExe)
|
||||
{
|
||||
UseShellExecute = true,
|
||||
WorkingDirectory = Path.GetDirectoryName(_installedExe)!,
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// ---------- Deinstallation ----------
|
||||
|
||||
private async void LoadDataInfo()
|
||||
{
|
||||
var root = Installer.DataDir;
|
||||
var custom = Installer.CustomInstancesDir;
|
||||
var customNote = custom is null ? "" : $" Instanzen in deinem eigenen Ordner {custom} werden nie angefasst.";
|
||||
if (!Directory.Exists(root))
|
||||
{
|
||||
DataInfo.Text = "Es wurden keine Launcher-Daten gefunden." + customNote;
|
||||
RemoveDataBox.IsEnabled = false;
|
||||
return;
|
||||
}
|
||||
DataInfo.Text = $"Sie liegen in {root}. Größe wird berechnet …";
|
||||
var size = await Task.Run(Installer.DataSize);
|
||||
DataInfo.Text = $"Sie liegen in {root} ({Installer.FormatSize(size)}) und stehen wieder bereit, falls du AeroMC später neu installierst." + customNote;
|
||||
}
|
||||
|
||||
private void RemoveDataBox_Click(object sender, RoutedEventArgs e) =>
|
||||
RemoveDataWarning.Visibility = RemoveDataBox.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
private async Task RunUninstall()
|
||||
{
|
||||
_busy = true;
|
||||
bool removeData = RemoveDataBox.IsChecked == true;
|
||||
ShowOnly(PageProgress);
|
||||
ProgressTitle.Text = "AeroMC wird entfernt …";
|
||||
LaunchBox.Visibility = Visibility.Collapsed;
|
||||
SetButtons(false);
|
||||
var progress = new Progress<(double Value, string Text)>(p => SetProgress(p.Value, p.Text));
|
||||
try
|
||||
{
|
||||
await Task.Run(() => Installer.Uninstall(removeData, progress));
|
||||
ShowSuccess("AeroMC wurde entfernt", removeData
|
||||
? "AeroMC, deine Instanzen, Konten und Einstellungen wurden entfernt (die Daten liegen im Papierkorb)."
|
||||
: "Deine Instanzen und Einstellungen liegen weiterhin in " + Installer.DataDir + " – falls du AeroMC später wieder installierst.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ProgressTitle.Text = "Deinstallation fehlgeschlagen";
|
||||
ProgressBarPanel.Visibility = Visibility.Collapsed;
|
||||
ProgressText.Text = ex.Message;
|
||||
}
|
||||
_done = true;
|
||||
_busy = false;
|
||||
CloseButton.IsEnabled = true;
|
||||
NextButton.Content = "Schließen";
|
||||
NextButton.IsEnabled = true;
|
||||
CancelButton.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
// ---------- Helfer ----------
|
||||
|
||||
private void SetButtons(bool enabled)
|
||||
{
|
||||
NextButton.IsEnabled = enabled;
|
||||
BackButton.IsEnabled = enabled;
|
||||
CancelButton.IsEnabled = enabled;
|
||||
CloseButton.IsEnabled = enabled;
|
||||
}
|
||||
|
||||
private void SetProgress(double value, string text)
|
||||
{
|
||||
if (_done) return;
|
||||
ProgressText.Text = text;
|
||||
PercentText.Text = $"{(int)Math.Round(value * 100)} %";
|
||||
_progress = value;
|
||||
AnimateProgress();
|
||||
}
|
||||
|
||||
private double _progress;
|
||||
|
||||
private void AnimateProgress()
|
||||
{
|
||||
var target = Math.Max(0, ProgressTrack.ActualWidth * _progress);
|
||||
ProgressFill.BeginAnimation(WidthProperty,
|
||||
new DoubleAnimation(target, TimeSpan.FromMilliseconds(250)) { EasingFunction = new CubicEase() });
|
||||
}
|
||||
|
||||
// Kommt der erste Wert, bevor die Seite fertig angeordnet ist (Breite 0), nachträglich richtig setzen
|
||||
private void ProgressTrack_SizeChanged(object sender, SizeChangedEventArgs e) => AnimateProgress();
|
||||
|
||||
private static bool IsFullPath(string p) =>
|
||||
p.Length >= 3 && char.IsLetter(p[0]) && p[1] == ':' && (p[2] == '\\' || p[2] == '/');
|
||||
|
||||
private void Minimize_Click(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
|
||||
|
||||
private async void Cancel_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_busy || _dialog is not null) return;
|
||||
if (!_done && _mode == SetupMode.Install && _page > 0)
|
||||
{
|
||||
bool abbrechen = await Frage("Installation abbrechen?",
|
||||
"AeroMC wird nicht installiert. Du kannst das Setup später jederzeit erneut starten.",
|
||||
"Ja, abbrechen", "Weiter installieren", "\uE897");
|
||||
if (!abbrechen) return;
|
||||
}
|
||||
Close();
|
||||
}
|
||||
|
||||
// ---------- eigene Rückfragen im Setup-Look ----------
|
||||
|
||||
private TaskCompletionSource<bool>? _dialog;
|
||||
|
||||
/// <summary>
|
||||
/// Zeigt eine Rückfrage über dem Fenster. Mit <paramref name="zurueck"/>: zwei Knöpfe –
|
||||
/// der hervorgehobene ist der sichere Weg zurück (liefert false), der andere bestätigt (true).
|
||||
/// Ohne: nur ein Hinweis mit einem Knopf (liefert true).
|
||||
/// </summary>
|
||||
private Task<bool> Frage(string titel, string text, string bestaetigen, string? zurueck = null, string symbol = "\uE7BA")
|
||||
{
|
||||
_dialog?.TrySetResult(false);
|
||||
_dialog = new TaskCompletionSource<bool>();
|
||||
DialogTitle.Text = titel;
|
||||
DialogText.Text = text;
|
||||
DialogIcon.Text = symbol;
|
||||
if (zurueck is null)
|
||||
{
|
||||
DialogSecondary.Visibility = Visibility.Collapsed;
|
||||
DialogPrimary.Content = bestaetigen;
|
||||
}
|
||||
else
|
||||
{
|
||||
DialogSecondary.Visibility = Visibility.Visible;
|
||||
DialogSecondary.Content = bestaetigen;
|
||||
DialogPrimary.Content = zurueck;
|
||||
}
|
||||
|
||||
DialogOverlay.Visibility = Visibility.Visible;
|
||||
var ein = new DoubleAnimation(0, 1, TimeSpan.FromMilliseconds(160));
|
||||
DialogOverlay.BeginAnimation(OpacityProperty, ein);
|
||||
var groesse = new DoubleAnimation(0.94, 1, TimeSpan.FromMilliseconds(220)) { EasingFunction = new CubicEase { EasingMode = EasingMode.EaseOut } };
|
||||
DialogScale.BeginAnimation(ScaleTransform.ScaleXProperty, groesse);
|
||||
DialogScale.BeginAnimation(ScaleTransform.ScaleYProperty, groesse);
|
||||
DialogPrimary.Focus();
|
||||
return _dialog.Task;
|
||||
}
|
||||
|
||||
private void SchliesseFrage(bool ergebnis)
|
||||
{
|
||||
if (_dialog is null) return;
|
||||
var tcs = _dialog;
|
||||
_dialog = null;
|
||||
DialogOverlay.Visibility = Visibility.Collapsed;
|
||||
tcs.TrySetResult(ergebnis);
|
||||
}
|
||||
|
||||
// hervorgehobener Knopf: bei Rückfragen "zurück" (false), beim reinen Hinweis "OK" (true)
|
||||
private void DialogPrimary_Click(object sender, RoutedEventArgs e) =>
|
||||
SchliesseFrage(DialogSecondary.Visibility != Visibility.Visible);
|
||||
|
||||
private void DialogSecondary_Click(object sender, RoutedEventArgs e) => SchliesseFrage(true);
|
||||
|
||||
protected override void OnPreviewKeyDown(System.Windows.Input.KeyEventArgs e)
|
||||
{
|
||||
// solange die Rückfrage offen ist: Enter = hervorgehobener Knopf, Esc = zurück
|
||||
if (_dialog is not null)
|
||||
{
|
||||
if (e.Key == System.Windows.Input.Key.Escape) { SchliesseFrage(DialogSecondary.Visibility != Visibility.Visible); e.Handled = true; return; }
|
||||
if (e.Key == System.Windows.Input.Key.Enter) { DialogPrimary_Click(this, new RoutedEventArgs()); e.Handled = true; return; }
|
||||
}
|
||||
base.OnPreviewKeyDown(e);
|
||||
}
|
||||
|
||||
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
if (_busy) e.Cancel = true; // während der Installation nicht schließen
|
||||
_dialog?.TrySetResult(false);
|
||||
base.OnClosing(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Text;
|
||||
|
||||
namespace AeroMCSetup;
|
||||
|
||||
/// <summary>
|
||||
/// Die App liegt als ZIP hinten an der Setup-.exe angehängt:
|
||||
/// [Setup.exe][ZIP][8 Byte: ZIP-Länge][8 Byte: "AEROMCPK"]
|
||||
/// Windows ignoriert angehängte Daten beim Starten einer .exe.
|
||||
/// </summary>
|
||||
public static class Payload
|
||||
{
|
||||
private static readonly byte[] Magic = Encoding.ASCII.GetBytes("AEROMCPK");
|
||||
private const int FooterSize = 16;
|
||||
|
||||
private static (long Offset, long Length)? _info;
|
||||
private static bool _checked;
|
||||
|
||||
/// <summary>Position und Länge des ZIPs in dieser .exe, oder null (z. B. beim Deinstallationsprogramm).</summary>
|
||||
public static (long Offset, long Length)? Info
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_checked) return _info;
|
||||
_checked = true;
|
||||
try
|
||||
{
|
||||
using var f = File.OpenRead(Installer.SelfPath);
|
||||
if (f.Length < FooterSize) return null;
|
||||
f.Seek(-FooterSize, SeekOrigin.End);
|
||||
var footer = new byte[FooterSize];
|
||||
if (f.Read(footer, 0, FooterSize) != FooterSize) return null;
|
||||
for (int i = 0; i < Magic.Length; i++)
|
||||
if (footer[8 + i] != Magic[i]) return null;
|
||||
long length = BitConverter.ToInt64(footer, 0);
|
||||
long offset = f.Length - FooterSize - length;
|
||||
if (length <= 0 || offset <= 0) return null;
|
||||
_info = (offset, length);
|
||||
}
|
||||
catch { _info = null; }
|
||||
return _info;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Exists => Info is not null;
|
||||
|
||||
/// <summary>Länge des eigentlichen Setup-Programms ohne Anhang.</summary>
|
||||
public static long StubLength => Info?.Offset ?? new FileInfo(Installer.SelfPath).Length;
|
||||
|
||||
public static ZipArchive OpenArchive()
|
||||
{
|
||||
var info = Info ?? throw new InvalidOperationException("Die Installationsdaten fehlen in dieser Datei. Bitte AeroMC-Setup.exe neu herunterladen.");
|
||||
var file = File.OpenRead(Installer.SelfPath);
|
||||
return new ZipArchive(new SubStream(file, info.Offset, info.Length), ZipArchiveMode.Read, leaveOpen: false);
|
||||
}
|
||||
|
||||
/// <summary>Nur-Lese-Ausschnitt einer Datei, damit ZipArchive den Anhang wie eine eigene Datei sieht.</summary>
|
||||
private sealed class SubStream(Stream inner, long start, long length) : Stream
|
||||
{
|
||||
private long _pos;
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => true;
|
||||
public override bool CanWrite => false;
|
||||
public override long Length => length;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => _pos;
|
||||
set => _pos = Math.Max(0, Math.Min(value, length));
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
long left = length - _pos;
|
||||
if (left <= 0) return 0;
|
||||
if (count > left) count = (int)left;
|
||||
inner.Position = start + _pos;
|
||||
int n = inner.Read(buffer, offset, count);
|
||||
_pos += n;
|
||||
return n;
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
Position = origin switch
|
||||
{
|
||||
SeekOrigin.Begin => offset,
|
||||
SeekOrigin.Current => _pos + offset,
|
||||
_ => length + offset,
|
||||
};
|
||||
return _pos;
|
||||
}
|
||||
|
||||
public override void Flush() { }
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing) inner.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.ComTypes;
|
||||
using System.Text;
|
||||
|
||||
namespace AeroMCSetup;
|
||||
|
||||
/// <summary>
|
||||
/// Verknüpfungen (.lnk) über die Shell-Schnittstellen – inklusive AppUserModelID, damit
|
||||
/// Taskleiste und Windows-Benachrichtigungen AeroMC wie bisher zuordnen (wie beim alten NSIS-Setup).
|
||||
/// </summary>
|
||||
public static class Shortcut
|
||||
{
|
||||
public static void Create(string link, string target, string description, string appUserModelId)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(link)!);
|
||||
var shellLink = (IShellLinkW)new CShellLink();
|
||||
try
|
||||
{
|
||||
shellLink.SetPath(target);
|
||||
shellLink.SetWorkingDirectory(Path.GetDirectoryName(target)!);
|
||||
shellLink.SetIconLocation(target, 0);
|
||||
shellLink.SetDescription(description);
|
||||
|
||||
var store = (IPropertyStore)shellLink;
|
||||
var key = new PropertyKey(new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"), 5); // PKEY_AppUserModel_ID
|
||||
var value = new PropVariant { vt = 31 /* VT_LPWSTR */, p = Marshal.StringToCoTaskMemUni(appUserModelId) };
|
||||
try
|
||||
{
|
||||
store.SetValue(ref key, ref value);
|
||||
store.Commit();
|
||||
}
|
||||
finally { Marshal.FreeCoTaskMem(value.p); }
|
||||
|
||||
((IPersistFile)shellLink).Save(link, true);
|
||||
}
|
||||
finally { Marshal.ReleaseComObject(shellLink); }
|
||||
}
|
||||
|
||||
/// <summary>Ziel einer vorhandenen Verknüpfung, oder null.</summary>
|
||||
public static string? Target(string link)
|
||||
{
|
||||
if (!File.Exists(link)) return null;
|
||||
var shellLink = (IShellLinkW)new CShellLink();
|
||||
try
|
||||
{
|
||||
((IPersistFile)shellLink).Load(link, 0);
|
||||
var sb = new StringBuilder(1024);
|
||||
shellLink.GetPath(sb, sb.Capacity, IntPtr.Zero, 0);
|
||||
return sb.ToString();
|
||||
}
|
||||
catch { return null; }
|
||||
finally { Marshal.ReleaseComObject(shellLink); }
|
||||
}
|
||||
|
||||
// ---------- COM ----------
|
||||
|
||||
[ComImport, Guid("00021401-0000-0000-C000-000000000046")]
|
||||
private class CShellLink { }
|
||||
|
||||
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")]
|
||||
private interface IShellLinkW
|
||||
{
|
||||
void GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, IntPtr pfd, uint fFlags);
|
||||
void GetIDList(out IntPtr ppidl);
|
||||
void SetIDList(IntPtr pidl);
|
||||
void GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
|
||||
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
|
||||
void GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
|
||||
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
|
||||
void GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
|
||||
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
|
||||
void GetHotkey(out short pwHotkey);
|
||||
void SetHotkey(short wHotkey);
|
||||
void GetShowCmd(out int piShowCmd);
|
||||
void SetShowCmd(int iShowCmd);
|
||||
void GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int piIcon);
|
||||
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
|
||||
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, uint dwReserved);
|
||||
void Resolve(IntPtr hwnd, uint fFlags);
|
||||
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
|
||||
}
|
||||
|
||||
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
|
||||
private interface IPropertyStore
|
||||
{
|
||||
void GetCount(out uint cProps);
|
||||
void GetAt(uint iProp, out PropertyKey pkey);
|
||||
void GetValue(ref PropertyKey key, out PropVariant pv);
|
||||
void SetValue(ref PropertyKey key, ref PropVariant pv);
|
||||
void Commit();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
private struct PropertyKey(Guid fmtid, uint pid)
|
||||
{
|
||||
public Guid fmtid = fmtid;
|
||||
public uint pid = pid;
|
||||
}
|
||||
|
||||
// PROPVARIANT: 16 Byte (32 Bit) bzw. 24 Byte (64 Bit)
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct PropVariant
|
||||
{
|
||||
public ushort vt;
|
||||
public ushort r1, r2, r3;
|
||||
public IntPtr p;
|
||||
public IntPtr p2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="BaseBrush" Color="#202020"/>
|
||||
<SolidColorBrush x:Key="LayerBrush" Color="#282828"/>
|
||||
<SolidColorBrush x:Key="LayerStrokeBrush" Color="#1A1A1A"/>
|
||||
<SolidColorBrush x:Key="SubtleHoverBrush" Color="#0FFFFFFF"/>
|
||||
<SolidColorBrush x:Key="SubtlePressedBrush" Color="#0AFFFFFF"/>
|
||||
<SolidColorBrush x:Key="SubtleSelectedBrush" Color="#14FFFFFF"/>
|
||||
<SolidColorBrush x:Key="ControlFillBrush" Color="#2D2D2D"/>
|
||||
<SolidColorBrush x:Key="ControlHoverBrush" Color="#323232"/>
|
||||
<SolidColorBrush x:Key="ControlPressedBrush" Color="#272727"/>
|
||||
<SolidColorBrush x:Key="ControlStrokeBrush" Color="#353535"/>
|
||||
<SolidColorBrush x:Key="ControlStrongStrokeBrush" Color="#8A8A8A"/>
|
||||
<SolidColorBrush x:Key="InputFocusBrush" Color="#1F1F1F"/>
|
||||
<SolidColorBrush x:Key="TextBrush" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#C8C8C8"/>
|
||||
<SolidColorBrush x:Key="TextTertiaryBrush" Color="#9A9A9A"/>
|
||||
<SolidColorBrush x:Key="FlyoutBrush" Color="#2C2C2C"/>
|
||||
<SolidColorBrush x:Key="FlyoutStrokeBrush" Color="#404040"/>
|
||||
<SolidColorBrush x:Key="DialogFooterBrush" Color="#202020"/>
|
||||
<SolidColorBrush x:Key="ScrollThumbBrush" Color="#9F9F9F"/>
|
||||
<SolidColorBrush x:Key="OverlayBrush" Color="#66000000"/>
|
||||
<SolidColorBrush x:Key="SuccessBrush" Color="#4CAF50"/>
|
||||
<SolidColorBrush x:Key="DangerBrush" Color="#FF99A4"/>
|
||||
<SolidColorBrush x:Key="FenceBrush" Color="#F21C1C1C"/>
|
||||
<Color x:Key="ShadowColor">#000000</Color>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,27 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<SolidColorBrush x:Key="BaseBrush" Color="#F3F3F3"/>
|
||||
<SolidColorBrush x:Key="LayerBrush" Color="#FAFAFA"/>
|
||||
<SolidColorBrush x:Key="LayerStrokeBrush" Color="#E5E5E5"/>
|
||||
<SolidColorBrush x:Key="SubtleHoverBrush" Color="#0A000000"/>
|
||||
<SolidColorBrush x:Key="SubtlePressedBrush" Color="#06000000"/>
|
||||
<SolidColorBrush x:Key="SubtleSelectedBrush" Color="#0F000000"/>
|
||||
<SolidColorBrush x:Key="ControlFillBrush" Color="#FDFDFD"/>
|
||||
<SolidColorBrush x:Key="ControlHoverBrush" Color="#F6F6F6"/>
|
||||
<SolidColorBrush x:Key="ControlPressedBrush" Color="#F2F2F2"/>
|
||||
<SolidColorBrush x:Key="ControlStrokeBrush" Color="#E3E3E3"/>
|
||||
<SolidColorBrush x:Key="ControlStrongStrokeBrush" Color="#C4C4C4"/>
|
||||
<SolidColorBrush x:Key="InputFocusBrush" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="TextBrush" Color="#1B1B1B"/>
|
||||
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#5D5D5D"/>
|
||||
<SolidColorBrush x:Key="TextTertiaryBrush" Color="#8B8B8B"/>
|
||||
<SolidColorBrush x:Key="FlyoutBrush" Color="#F9F9F9"/>
|
||||
<SolidColorBrush x:Key="FlyoutStrokeBrush" Color="#DCDCDC"/>
|
||||
<SolidColorBrush x:Key="DialogFooterBrush" Color="#F3F3F3"/>
|
||||
<SolidColorBrush x:Key="ScrollThumbBrush" Color="#8A8A8A"/>
|
||||
<SolidColorBrush x:Key="OverlayBrush" Color="#4D000000"/>
|
||||
<SolidColorBrush x:Key="SuccessBrush" Color="#0F7B0F"/>
|
||||
<SolidColorBrush x:Key="DangerBrush" Color="#C42B1C"/>
|
||||
<SolidColorBrush x:Key="FenceBrush" Color="#F5F7F7F7"/>
|
||||
<Color x:Key="ShadowColor">#000000</Color>
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,651 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- Schriften: Windows 11 nutzt "Segoe UI Variable" / "Segoe Fluent Icons", Windows 10 fällt auf Segoe UI / MDL2 zurück -->
|
||||
<FontFamily x:Key="UiFont">Segoe UI Variable Text, Segoe UI</FontFamily>
|
||||
<FontFamily x:Key="DisplayFont">Segoe UI Variable Display, Segoe UI</FontFamily>
|
||||
<FontFamily x:Key="IconFont">Segoe Fluent Icons, Segoe MDL2 Assets</FontFamily>
|
||||
|
||||
<DropShadowEffect x:Key="FlyoutShadow" BlurRadius="24" ShadowDepth="6" Direction="270" Opacity="0.18"
|
||||
Color="Black"/>
|
||||
<DropShadowEffect x:Key="DialogShadow" BlurRadius="40" ShadowDepth="12" Direction="270" Opacity="0.28"
|
||||
Color="Black"/>
|
||||
|
||||
<!-- ===== Buttons ===== -->
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="14,5"/>
|
||||
<Setter Property="MinHeight" Value="32"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4" Padding="{TemplateBinding Padding}"
|
||||
SnapsToDevicePixels="True">
|
||||
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="Center" RecognizesAccessKey="True"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlHoverBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlPressedBrush}"/>
|
||||
<Setter Property="Opacity" Value="0.85"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource TextBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.4"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="AccentButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="Foreground" Value="{DynamicResource OnAccentBrush}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="1" CornerRadius="4" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="True">
|
||||
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||
VerticalAlignment="Center" RecognizesAccessKey="True"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentHoverBrush}"/>
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentHoverBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter Property="Opacity" Value="0.8"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource TextBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.4"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Transparenter Button (Symbolleisten, Sidebar) -->
|
||||
<Style x:Key="SubtleButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="9,6"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="4" Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtlePressedBrush}"/>
|
||||
<Setter Property="Opacity" Value="0.75"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.35"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Titelleisten-Buttons -->
|
||||
<Style x:Key="CaptionButton" TargetType="Button">
|
||||
<Setter Property="Width" Value="46"/>
|
||||
<Setter Property="Height" Value="32"/>
|
||||
<Setter Property="FontFamily" Value="{DynamicResource IconFont}"/>
|
||||
<Setter Property="FontSize" Value="10"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="IsTabStop" Value="False"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" Background="{TemplateBinding Background}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtlePressedBrush}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="CloseCaptionButton" TargetType="Button" BasedOn="{StaticResource CaptionButton}">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" Background="Transparent">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#C42B1C"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#C83C30"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ===== TextBox ===== -->
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="CaretBrush" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="SelectionBrush" Value="{DynamicResource AccentBrush}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
|
||||
<Setter Property="Padding" Value="10,6"/>
|
||||
<Setter Property="MinHeight" Value="32"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TextBox">
|
||||
<Grid>
|
||||
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="4"
|
||||
BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1"/>
|
||||
<!-- Unterstrich wie bei Windows 11 -->
|
||||
<Border x:Name="Line" Height="1" VerticalAlignment="Bottom" Margin="3,0" CornerRadius="0,0,4,4"
|
||||
Background="{DynamicResource ControlStrongStrokeBrush}"/>
|
||||
<ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}" VerticalAlignment="Center"
|
||||
Focusable="False" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlHoverBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource InputFocusBrush}"/>
|
||||
<Setter TargetName="Line" Property="Height" Value="2"/>
|
||||
<Setter TargetName="Line" Property="Margin" Value="1,0"/>
|
||||
<Setter TargetName="Line" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ===== Scrollbar (schmal, wird beim Überfahren breiter) ===== -->
|
||||
<Style x:Key="ScrollThumb" TargetType="Thumb">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Thumb">
|
||||
<Border Background="{DynamicResource ScrollThumbBrush}" CornerRadius="3"/>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ScrollBar">
|
||||
<Setter Property="Width" Value="12"/>
|
||||
<Setter Property="MinWidth" Value="12"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ScrollBar">
|
||||
<Grid Background="Transparent">
|
||||
<Track x:Name="PART_Track" IsDirectionReversed="True">
|
||||
<Track.Thumb>
|
||||
<Thumb x:Name="Th" Style="{StaticResource ScrollThumb}" Width="3" HorizontalAlignment="Center" Opacity="0.7"/>
|
||||
</Track.Thumb>
|
||||
</Track>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Th" Property="Width" Value="6"/>
|
||||
<Setter TargetName="Th" Property="Opacity" Value="1"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="Orientation" Value="Horizontal">
|
||||
<Setter Property="Width" Value="Auto"/>
|
||||
<Setter Property="MinWidth" Value="0"/>
|
||||
<Setter Property="Height" Value="12"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ScrollBar">
|
||||
<Grid Background="Transparent">
|
||||
<Track x:Name="PART_Track">
|
||||
<Track.Thumb>
|
||||
<Thumb x:Name="Th" Style="{StaticResource ScrollThumb}" Height="3" VerticalAlignment="Center" Opacity="0.7"/>
|
||||
</Track.Thumb>
|
||||
</Track>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Th" Property="Height" Value="6"/>
|
||||
<Setter TargetName="Th" Property="Opacity" Value="1"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- ===== ToolTip ===== -->
|
||||
<Style TargetType="ToolTip">
|
||||
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="HasDropShadow" Value="False"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ToolTip">
|
||||
<Border Margin="8,4,8,12" Padding="9,6" CornerRadius="4" MaxWidth="320"
|
||||
Background="{DynamicResource FlyoutBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}" BorderThickness="1"
|
||||
Effect="{StaticResource FlyoutShadow}">
|
||||
<ContentPresenter>
|
||||
<ContentPresenter.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
</Style>
|
||||
</ContentPresenter.Resources>
|
||||
</ContentPresenter>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ===== Kontextmenü ===== -->
|
||||
<Style TargetType="ContextMenu">
|
||||
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="HasDropShadow" Value="False"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ContextMenu">
|
||||
<Border Margin="12,4,12,16" Padding="0,4" CornerRadius="8" MinWidth="200"
|
||||
Background="{DynamicResource FlyoutBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}" BorderThickness="1"
|
||||
Effect="{StaticResource FlyoutShadow}">
|
||||
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="Separator">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Separator">
|
||||
<Border Height="1" Margin="0,4" Background="{DynamicResource FlyoutStrokeBrush}"/>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="MenuItem">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="MenuItem">
|
||||
<Border x:Name="Bd" Background="Transparent" CornerRadius="4" Margin="4,1" Padding="11,0" Height="34">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="28"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<ContentPresenter ContentSource="Icon" VerticalAlignment="Center" HorizontalAlignment="Left"
|
||||
TextElement.FontFamily="{DynamicResource IconFont}" TextElement.FontSize="16"/>
|
||||
<ContentPresenter Grid.Column="1" ContentSource="Header" VerticalAlignment="Center" RecognizesAccessKey="True"/>
|
||||
<TextBlock x:Name="Gesture" Grid.Column="2" Text="{TemplateBinding InputGestureText}" FontSize="12"
|
||||
VerticalAlignment="Center" Margin="24,0,0,0" Foreground="{DynamicResource TextTertiaryBrush}"/>
|
||||
<TextBlock x:Name="Arrow" Grid.Column="2" Text="" FontFamily="{DynamicResource IconFont}" FontSize="11"
|
||||
VerticalAlignment="Center" Margin="24,0,0,0" Visibility="Collapsed"
|
||||
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
<Popup x:Name="PART_Popup" Placement="Right" HorizontalOffset="-4" VerticalOffset="-12"
|
||||
IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
AllowsTransparency="True" Focusable="False" PopupAnimation="Fade">
|
||||
<Border Margin="12,4,12,16" Padding="0,4" CornerRadius="8" MinWidth="180"
|
||||
Background="{DynamicResource FlyoutBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}"
|
||||
BorderThickness="1" Effect="{StaticResource FlyoutShadow}">
|
||||
<ScrollViewer MaxHeight="420" VerticalScrollBarVisibility="Auto">
|
||||
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle"/>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsHighlighted" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="Role" Value="SubmenuHeader">
|
||||
<Setter TargetName="Arrow" Property="Visibility" Value="Visible"/>
|
||||
<Setter TargetName="Gesture" Property="Visibility" Value="Collapsed"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.4"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ===== Listen ===== -->
|
||||
<Style TargetType="ListBox">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBox">
|
||||
<ScrollViewer Focusable="False" Padding="{TemplateBinding Padding}">
|
||||
<ItemsPresenter/>
|
||||
</ScrollViewer>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Navigationseintrag in der Seitenleiste -->
|
||||
<Style x:Key="NavItem" TargetType="ListBoxItem">
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Height" Value="38"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Grid Margin="4,1" Background="Transparent">
|
||||
<Border x:Name="Bd" CornerRadius="4" BorderThickness="1" BorderBrush="Transparent" Background="Transparent"/>
|
||||
<Rectangle x:Name="Pill" Width="3" Height="16" RadiusX="1.5" RadiusY="1.5" HorizontalAlignment="Left"
|
||||
Fill="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
|
||||
<ContentPresenter Margin="14,0,12,0" VerticalAlignment="Center"/>
|
||||
<!-- Einfügelinie beim Umsortieren -->
|
||||
<Border x:Name="InsertTop" Height="2" CornerRadius="1" VerticalAlignment="Top" Margin="6,-2,6,0"
|
||||
Background="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
|
||||
<Border x:Name="InsertBottom" Height="2" CornerRadius="1" VerticalAlignment="Bottom" Margin="6,0,6,-2"
|
||||
Background="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleSelectedBrush}"/>
|
||||
<Setter TargetName="Pill" Property="Visibility" Value="Visible"/>
|
||||
</Trigger>
|
||||
<Trigger Property="Tag" Value="before">
|
||||
<Setter TargetName="InsertTop" Property="Visibility" Value="Visible"/>
|
||||
</Trigger>
|
||||
<Trigger Property="Tag" Value="after">
|
||||
<Setter TargetName="InsertBottom" Property="Visibility" Value="Visible"/>
|
||||
</Trigger>
|
||||
<Trigger Property="Tag" Value="drop">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Icon-Kachel -->
|
||||
<Style x:Key="TileItem" TargetType="ListBoxItem">
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Border x:Name="Bd" Margin="2" CornerRadius="6" BorderThickness="1" BorderBrush="Transparent" Background="Transparent">
|
||||
<ContentPresenter/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentSubtleStrokeBrush}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ===== Listenansicht-Zeile ===== -->
|
||||
<Style x:Key="RowItem" TargetType="ListBoxItem">
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Grid Margin="0,1">
|
||||
<Border x:Name="Bd" CornerRadius="4" Background="Transparent"/>
|
||||
<Rectangle x:Name="Pill" Width="3" Height="16" RadiusX="1.5" RadiusY="1.5" HorizontalAlignment="Left"
|
||||
Fill="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
|
||||
<ContentPresenter/>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||
<Setter TargetName="Pill" Property="Visibility" Value="Visible"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ===== Karte (Einstellungen) ===== -->
|
||||
<Style x:Key="Card" TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="6"/>
|
||||
<Setter Property="Padding" Value="18,14"/>
|
||||
<Setter Property="Margin" Value="0,0,0,4"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SectionTitle" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Margin" Value="2,24,0,8"/>
|
||||
</Style>
|
||||
|
||||
<!-- ===== Kippschalter (Windows 11) ===== -->
|
||||
<Style x:Key="ToggleSwitch" TargetType="CheckBox">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="CheckBox">
|
||||
<StackPanel Orientation="Horizontal" Background="Transparent">
|
||||
<TextBlock x:Name="State" Text="Aus" VerticalAlignment="Center" Margin="0,0,12,0" MinWidth="24" TextAlignment="Right"/>
|
||||
<Grid Width="40" Height="20">
|
||||
<Border x:Name="Track" CornerRadius="10" BorderThickness="1"
|
||||
Background="{DynamicResource SubtleSelectedBrush}" BorderBrush="{DynamicResource ControlStrongStrokeBrush}"/>
|
||||
<Ellipse x:Name="Knob" Width="12" Height="12" HorizontalAlignment="Left" Margin="4,0,0,0"
|
||||
Fill="{DynamicResource TextSecondaryBrush}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Track" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||
<Setter TargetName="Track" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||
<Setter TargetName="Knob" Property="Fill" Value="{DynamicResource OnAccentBrush}"/>
|
||||
<Setter TargetName="Knob" Property="HorizontalAlignment" Value="Right"/>
|
||||
<Setter TargetName="Knob" Property="Margin" Value="0,0,4,0"/>
|
||||
<Setter TargetName="State" Property="Text" Value="Ein"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Knob" Property="Width" Value="14"/>
|
||||
<Setter TargetName="Knob" Property="Height" Value="14"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.4"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ===== Kontrollkästchen ===== -->
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="CheckBox">
|
||||
<StackPanel Orientation="Horizontal" Background="Transparent">
|
||||
<Border x:Name="Box" Width="20" Height="20" CornerRadius="4" BorderThickness="1" VerticalAlignment="Center"
|
||||
Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrongStrokeBrush}">
|
||||
<TextBlock x:Name="Check" Text="" FontFamily="{DynamicResource IconFont}" FontSize="12"
|
||||
Foreground="{DynamicResource OnAccentBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Visibility="Collapsed"/>
|
||||
</Border>
|
||||
<ContentPresenter Margin="10,0,0,0" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Box" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||
<Setter TargetName="Box" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||
<Setter TargetName="Check" Property="Visibility" Value="Visible"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Box" Property="Opacity" Value="0.85"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter Property="Opacity" Value="0.4"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ===== Segment-Auswahl (z. B. Hell | Dunkel | System) ===== -->
|
||||
<Style x:Key="SegmentRadio" TargetType="RadioButton">
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Padding" Value="14,6"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="RadioButton">
|
||||
<Border x:Name="Bd" CornerRadius="4" Padding="{TemplateBinding Padding}" Margin="2" Background="Transparent">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource OnAccentBrush}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SegmentGroup" TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource SubtlePressedBrush}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="6"/>
|
||||
<Setter Property="Padding" Value="1"/>
|
||||
</Style>
|
||||
|
||||
<!-- ===== Farb- und Symbolauswahl ===== -->
|
||||
<Style x:Key="SwatchRadio" TargetType="RadioButton">
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="RadioButton">
|
||||
<Grid Width="34" Height="34" Margin="0,0,6,6" Background="Transparent">
|
||||
<Ellipse x:Name="Ring" Stroke="{DynamicResource TextBrush}" StrokeThickness="2" Visibility="Collapsed"/>
|
||||
<Ellipse Margin="5" Fill="{TemplateBinding Background}" Stroke="{DynamicResource ControlStrokeBrush}"/>
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Ring" Property="Visibility" Value="Visible"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="GlyphRadio" TargetType="RadioButton">
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||
<Setter Property="FontFamily" Value="{DynamicResource IconFont}"/>
|
||||
<Setter Property="FontSize" Value="16"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="RadioButton">
|
||||
<Border x:Name="Bd" Width="36" Height="36" Margin="0,0,4,4" CornerRadius="4" BorderThickness="1"
|
||||
BorderBrush="Transparent" Background="Transparent">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="AeroMC.Setup"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<!-- Installation nur für den aktuellen Benutzer → keine Administratorrechte -->
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"format": 1,
|
||||
"restore": {
|
||||
"C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj": {}
|
||||
},
|
||||
"projects": {
|
||||
"C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj": {
|
||||
"version": "1.2.3",
|
||||
"restore": {
|
||||
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
||||
"projectName": "AeroMC-Setup",
|
||||
"projectPath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
||||
"packagesPath": "C:\\Users\\Viper\\.nuget\\packages\\",
|
||||
"outputPath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\Viper\\AppData\\Roaming\\NuGet\\NuGet.Config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net48"
|
||||
],
|
||||
"sources": {
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net48": {
|
||||
"targetAlias": "net48",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.100"
|
||||
},
|
||||
"frameworks": {
|
||||
"net48": {
|
||||
"targetAlias": "net48",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"suppressParent": "All",
|
||||
"target": "Package",
|
||||
"version": "[1.0.3, )",
|
||||
"autoReferenced": true
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.112\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\Viper\.nuget\packages\</NuGetPackageFolders>
|
||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<SourceRoot Include="C:\Users\Viper\.nuget\packages\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<Import Project="$(NuGetPackageRoot)microsoft.netframework.referenceassemblies.net48\1.0.3\build\Microsoft.NETFramework.ReferenceAssemblies.net48.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.netframework.referenceassemblies.net48\1.0.3\build\Microsoft.NETFramework.ReferenceAssemblies.net48.targets')" />
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||
</startup>
|
||||
</configuration>
|
||||
@@ -0,0 +1,20 @@
|
||||
AeroMC-Setup
|
||||
0.0.0.0
|
||||
|
||||
winexe
|
||||
C#
|
||||
.cs
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Debug\net48\
|
||||
AeroMCSetup
|
||||
none
|
||||
false
|
||||
TRACE;DEBUG;NETFRAMEWORK;NET48;NET30_OR_GREATER;NET35_OR_GREATER;NET40_OR_GREATER;NET45_OR_GREATER;NET451_OR_GREATER;NET452_OR_GREATER;NET46_OR_GREATER;NET461_OR_GREATER;NET462_OR_GREATER;NET47_OR_GREATER;NET471_OR_GREATER;NET472_OR_GREATER;NET48_OR_GREATER
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\App.xaml
|
||||
41549299691
|
||||
|
||||
919373854
|
||||
251587687494
|
||||
MainWindow.xaml;Themes\Colors.Dark.xaml;Themes\Colors.Light.xaml;Themes\Controls.xaml;
|
||||
|
||||
False
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("M_Viper")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper. Alle Rechte vorbehalten.")]
|
||||
[assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert AeroMC")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("0.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("AeroMC Setup")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("AeroMC Setup")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("0.0.0.0")]
|
||||
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
|
||||
|
||||
// Von der MSBuild WriteCodeFragment-Klasse generiert.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
f169547649332c2f1c7da272087eb95fe510c6a52616eac752d180c8c6fe1d73
|
||||
@@ -0,0 +1,14 @@
|
||||
is_global = true
|
||||
build_property.ApplicationManifest = app.manifest
|
||||
build_property.StartupObject =
|
||||
build_property.ApplicationDefaultFont =
|
||||
build_property.ApplicationHighDpiMode =
|
||||
build_property.ApplicationUseCompatibleTextRendering =
|
||||
build_property.ApplicationVisualStyles =
|
||||
build_property.RootNamespace = AeroMCSetup
|
||||
build_property.ProjectDir = C:\Users\Viper\Desktop\AeroMC-Launcher\installer\
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.CsWinRTUseWindowsUIXamlProjections = false
|
||||
build_property.EffectiveAnalysisLevelStyle =
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
@@ -0,0 +1,6 @@
|
||||
// <auto-generated/>
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Linq;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma checksum "..\..\..\App.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "85CCB5F529C7F3695DF076BCDBCCA84D8F4ED0DE"
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Automation;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Controls.Ribbon;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Forms.Integration;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Effects;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Media.Media3D;
|
||||
using System.Windows.Media.TextFormatting;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Shell;
|
||||
|
||||
|
||||
namespace AeroMCSetup {
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// App
|
||||
/// </summary>
|
||||
public partial class App : System.Windows.Application {
|
||||
|
||||
private bool _contentLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// InitializeComponent
|
||||
/// </summary>
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||
public void InitializeComponent() {
|
||||
if (_contentLoaded) {
|
||||
return;
|
||||
}
|
||||
_contentLoaded = true;
|
||||
System.Uri resourceLocater = new System.Uri("/AeroMC-Setup;V0.0.0.0;component/app.xaml", System.UriKind.Relative);
|
||||
|
||||
#line 1 "..\..\..\App.xaml"
|
||||
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Application Entry Point.
|
||||
/// </summary>
|
||||
[System.STAThreadAttribute()]
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||
public static void Main() {
|
||||
AeroMCSetup.App app = new AeroMCSetup.App();
|
||||
app.InitializeComponent();
|
||||
app.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,842 @@
|
||||
#pragma checksum "..\..\..\MainWindow.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "8CB5D6EC1BE0A9A19A04D9A92B2B0961370843E0"
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Automation;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Controls.Ribbon;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Forms.Integration;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Effects;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Media.Media3D;
|
||||
using System.Windows.Media.TextFormatting;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Shell;
|
||||
|
||||
|
||||
namespace AeroMCSetup {
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// MainWindow
|
||||
/// </summary>
|
||||
public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector {
|
||||
|
||||
|
||||
#line 140 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock VersionText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 142 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel Steps;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 144 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Dot0;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 144 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Num0;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 145 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Label0;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 147 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Line0;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 149 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Dot1;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 149 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Num1;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 150 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Label1;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 152 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Line1;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 154 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Dot2;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 154 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Num2;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 155 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Label2;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 157 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Line2;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 159 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel ImportStepRow;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 160 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Dot3;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 160 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Num3;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 161 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Label3;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 163 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Line3;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 165 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Dot4;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 165 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Num4;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 166 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Label4;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 179 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock WindowTitle;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 185 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button CloseButton;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 193 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageWelcome;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 240 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border UpdateHint;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 245 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock UpdateHintText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 248 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border RunningHint;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 260 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.DockPanel PageLicense;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 264 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox AcceptLicense;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 271 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel LicensePanel;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 277 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageOptions;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 286 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBox DirBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 295 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox DesktopLinkBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 301 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageImport;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 306 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel ImportList;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 313 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageProgress;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 314 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border SuccessIcon;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 316 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Media.ScaleTransform SuccessScale;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 321 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock ProgressTitle;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 323 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel ProgressBarPanel;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 324 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock PercentText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 326 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border ProgressTrack;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 327 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border ProgressFill;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 330 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock ProgressText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 332 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel DonePanel;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 333 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox LaunchBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 338 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageUninstall;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 349 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock DataInfo;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 350 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox RemoveDataBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 352 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock RemoveDataWarning;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 362 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button BackButton;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 363 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button NextButton;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 364 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button CancelButton;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 368 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Grid DialogOverlay;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 369 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border DialogCard;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 372 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Media.ScaleTransform DialogScale;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 376 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock DialogIcon;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 379 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock DialogTitle;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 382 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock DialogText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 385 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button DialogSecondary;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 386 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button DialogPrimary;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
private bool _contentLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// InitializeComponent
|
||||
/// </summary>
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||
public void InitializeComponent() {
|
||||
if (_contentLoaded) {
|
||||
return;
|
||||
}
|
||||
_contentLoaded = true;
|
||||
System.Uri resourceLocater = new System.Uri("/AeroMC-Setup;V0.0.0.0;component/mainwindow.xaml", System.UriKind.Relative);
|
||||
|
||||
#line 1 "..\..\..\MainWindow.xaml"
|
||||
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
}
|
||||
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
|
||||
void System.Windows.Markup.IComponentConnector.Connect(int connectionId, object target) {
|
||||
switch (connectionId)
|
||||
{
|
||||
case 1:
|
||||
this.VersionText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 2:
|
||||
this.Steps = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 3:
|
||||
this.Dot0 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 4:
|
||||
this.Num0 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 5:
|
||||
this.Label0 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 6:
|
||||
this.Line0 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 7:
|
||||
this.Dot1 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 8:
|
||||
this.Num1 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 9:
|
||||
this.Label1 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 10:
|
||||
this.Line1 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 11:
|
||||
this.Dot2 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 12:
|
||||
this.Num2 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 13:
|
||||
this.Label2 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 14:
|
||||
this.Line2 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 15:
|
||||
this.ImportStepRow = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 16:
|
||||
this.Dot3 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 17:
|
||||
this.Num3 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 18:
|
||||
this.Label3 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 19:
|
||||
this.Line3 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 20:
|
||||
this.Dot4 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 21:
|
||||
this.Num4 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 22:
|
||||
this.Label4 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 23:
|
||||
this.WindowTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 24:
|
||||
|
||||
#line 183 "..\..\..\MainWindow.xaml"
|
||||
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.Minimize_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 25:
|
||||
this.CloseButton = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 185 "..\..\..\MainWindow.xaml"
|
||||
this.CloseButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 26:
|
||||
this.PageWelcome = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 27:
|
||||
this.UpdateHint = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 28:
|
||||
this.UpdateHintText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 29:
|
||||
this.RunningHint = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 30:
|
||||
this.PageLicense = ((System.Windows.Controls.DockPanel)(target));
|
||||
return;
|
||||
case 31:
|
||||
this.AcceptLicense = ((System.Windows.Controls.CheckBox)(target));
|
||||
|
||||
#line 265 "..\..\..\MainWindow.xaml"
|
||||
this.AcceptLicense.Checked += new System.Windows.RoutedEventHandler(this.AcceptLicense_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 265 "..\..\..\MainWindow.xaml"
|
||||
this.AcceptLicense.Unchecked += new System.Windows.RoutedEventHandler(this.AcceptLicense_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 32:
|
||||
this.LicensePanel = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 33:
|
||||
this.PageOptions = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 34:
|
||||
|
||||
#line 285 "..\..\..\MainWindow.xaml"
|
||||
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.ChangeDir_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 35:
|
||||
this.DirBox = ((System.Windows.Controls.TextBox)(target));
|
||||
return;
|
||||
case 36:
|
||||
this.DesktopLinkBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
return;
|
||||
case 37:
|
||||
this.PageImport = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 38:
|
||||
this.ImportList = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 39:
|
||||
this.PageProgress = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 40:
|
||||
this.SuccessIcon = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 41:
|
||||
this.SuccessScale = ((System.Windows.Media.ScaleTransform)(target));
|
||||
return;
|
||||
case 42:
|
||||
this.ProgressTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 43:
|
||||
this.ProgressBarPanel = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 44:
|
||||
this.PercentText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 45:
|
||||
this.ProgressTrack = ((System.Windows.Controls.Border)(target));
|
||||
|
||||
#line 326 "..\..\..\MainWindow.xaml"
|
||||
this.ProgressTrack.SizeChanged += new System.Windows.SizeChangedEventHandler(this.ProgressTrack_SizeChanged);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 46:
|
||||
this.ProgressFill = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 47:
|
||||
this.ProgressText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 48:
|
||||
this.DonePanel = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 49:
|
||||
this.LaunchBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
return;
|
||||
case 50:
|
||||
this.PageUninstall = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 51:
|
||||
this.DataInfo = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 52:
|
||||
this.RemoveDataBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
|
||||
#line 351 "..\..\..\MainWindow.xaml"
|
||||
this.RemoveDataBox.Checked += new System.Windows.RoutedEventHandler(this.RemoveDataBox_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 351 "..\..\..\MainWindow.xaml"
|
||||
this.RemoveDataBox.Unchecked += new System.Windows.RoutedEventHandler(this.RemoveDataBox_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 53:
|
||||
this.RemoveDataWarning = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 54:
|
||||
this.BackButton = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 362 "..\..\..\MainWindow.xaml"
|
||||
this.BackButton.Click += new System.Windows.RoutedEventHandler(this.Back_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 55:
|
||||
this.NextButton = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 363 "..\..\..\MainWindow.xaml"
|
||||
this.NextButton.Click += new System.Windows.RoutedEventHandler(this.Next_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 56:
|
||||
this.CancelButton = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 364 "..\..\..\MainWindow.xaml"
|
||||
this.CancelButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 57:
|
||||
this.DialogOverlay = ((System.Windows.Controls.Grid)(target));
|
||||
return;
|
||||
case 58:
|
||||
this.DialogCard = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 59:
|
||||
this.DialogScale = ((System.Windows.Media.ScaleTransform)(target));
|
||||
return;
|
||||
case 60:
|
||||
this.DialogIcon = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 61:
|
||||
this.DialogTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 62:
|
||||
this.DialogText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 63:
|
||||
this.DialogSecondary = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 385 "..\..\..\MainWindow.xaml"
|
||||
this.DialogSecondary.Click += new System.Windows.RoutedEventHandler(this.DialogSecondary_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 64:
|
||||
this.DialogPrimary = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 386 "..\..\..\MainWindow.xaml"
|
||||
this.DialogPrimary.Click += new System.Windows.RoutedEventHandler(this.DialogPrimary_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
}
|
||||
this._contentLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||
</startup>
|
||||
</configuration>
|
||||
@@ -0,0 +1,20 @@
|
||||
AeroMC-Setup
|
||||
|
||||
|
||||
winexe
|
||||
C#
|
||||
.cs
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\
|
||||
AeroMCSetup
|
||||
none
|
||||
false
|
||||
TRACE;RELEASE;NETFRAMEWORK;NET48;NET30_OR_GREATER;NET35_OR_GREATER;NET40_OR_GREATER;NET45_OR_GREATER;NET451_OR_GREATER;NET452_OR_GREATER;NET46_OR_GREATER;NET461_OR_GREATER;NET462_OR_GREATER;NET47_OR_GREATER;NET471_OR_GREATER;NET472_OR_GREATER;NET48_OR_GREATER
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\App.xaml
|
||||
41549299691
|
||||
|
||||
7-621693760
|
||||
251587687494
|
||||
MainWindow.xaml;Themes\Colors.Dark.xaml;Themes\Colors.Light.xaml;Themes\Controls.xaml;
|
||||
|
||||
False
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("M_Viper")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
|
||||
[assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper. Alle Rechte vorbehalten.")]
|
||||
[assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert AeroMC")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.2.3.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.2.3")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("AeroMC Setup")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("AeroMC Setup")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.2.3.0")]
|
||||
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
|
||||
|
||||
// Von der MSBuild WriteCodeFragment-Klasse generiert.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
111db8ee3dc7193961107c73afd8913fff02b92fd4e49b8b7e4bf051181600bd
|
||||
@@ -0,0 +1,14 @@
|
||||
is_global = true
|
||||
build_property.ApplicationManifest = app.manifest
|
||||
build_property.StartupObject =
|
||||
build_property.ApplicationDefaultFont =
|
||||
build_property.ApplicationHighDpiMode =
|
||||
build_property.ApplicationUseCompatibleTextRendering =
|
||||
build_property.ApplicationVisualStyles =
|
||||
build_property.RootNamespace = AeroMCSetup
|
||||
build_property.ProjectDir = C:\Users\Viper\Desktop\AeroMC-Launcher\installer\
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.CsWinRTUseWindowsUIXamlProjections = false
|
||||
build_property.EffectiveAnalysisLevelStyle =
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
@@ -0,0 +1,6 @@
|
||||
// <auto-generated/>
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Linq;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
@@ -0,0 +1 @@
|
||||
75ed8898b0d415db8840bc8e86cbe862583b62e51d945f302629778ca05c0e99
|
||||
@@ -0,0 +1,26 @@
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\dist-setup-test\AeroMC-Setup.exe.config
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\dist-setup-test\AeroMC-Setup.exe
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\dist-setup-test\AeroMC-Setup.pdb
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\AeroMCSetup.csproj.AssemblyReference.cache
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\MainWindow.baml
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\Themes\Colors.Dark.baml
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\Themes\Colors.Light.baml
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\Themes\Controls.baml
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\App.baml
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\MainWindow.g.cs
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\App.g.cs
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\AeroMC-Setup_MarkupCompile.cache
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\AeroMC-Setup.g.resources
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\AeroMCSetup.GeneratedMSBuildEditorConfig.editorconfig
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\AeroMCSetup.AssemblyInfoInputs.cache
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\AeroMCSetup.AssemblyInfo.cs
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\AeroMCSetup.csproj.CoreCompileInputs.cache
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\installer\obj\Release\net48\AeroMC-Setup.exe
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\dist\.setup\stub\AeroMC-Setup.exe.config
|
||||
C:\Users\Viper\Desktop\AeroMC-Launcher\dist\.setup\stub\AeroMC-Setup.exe
|
||||
C:\Users\Viper\AppData\Local\Temp\claude\C--Users-Viper-Desktop-AeroMC-Launcher\1859f458-aac9-4e4b-be87-3e40160ebab1\scratchpad\stubtest\AeroMC-Setup.exe.config
|
||||
C:\Users\Viper\AppData\Local\Temp\claude\C--Users-Viper-Desktop-AeroMC-Launcher\1859f458-aac9-4e4b-be87-3e40160ebab1\scratchpad\stubtest\AeroMC-Setup.exe
|
||||
C:\Users\Viper\AppData\Local\Temp\claude\C--Users-Viper-Desktop-AeroMC-Launcher\1859f458-aac9-4e4b-be87-3e40160ebab1\scratchpad\stubtest\AeroMC-Setup.pdb
|
||||
C:\Users\Viper\AppData\Local\Temp\claude\C--Users-Viper-Desktop-AeroMC-Launcher\1859f458-aac9-4e4b-be87-3e40160ebab1\scratchpad\stubtest2\AeroMC-Setup.exe.config
|
||||
C:\Users\Viper\AppData\Local\Temp\claude\C--Users-Viper-Desktop-AeroMC-Launcher\1859f458-aac9-4e4b-be87-3e40160ebab1\scratchpad\stubtest2\AeroMC-Setup.exe
|
||||
C:\Users\Viper\AppData\Local\Temp\claude\C--Users-Viper-Desktop-AeroMC-Launcher\1859f458-aac9-4e4b-be87-3e40160ebab1\scratchpad\stubtest2\AeroMC-Setup.pdb
|
||||
@@ -0,0 +1,77 @@
|
||||
#pragma checksum "..\..\..\App.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "85CCB5F529C7F3695DF076BCDBCCA84D8F4ED0DE"
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Automation;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Controls.Ribbon;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Forms.Integration;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Effects;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Media.Media3D;
|
||||
using System.Windows.Media.TextFormatting;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Shell;
|
||||
|
||||
|
||||
namespace AeroMCSetup {
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// App
|
||||
/// </summary>
|
||||
public partial class App : System.Windows.Application {
|
||||
|
||||
private bool _contentLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// InitializeComponent
|
||||
/// </summary>
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||
public void InitializeComponent() {
|
||||
if (_contentLoaded) {
|
||||
return;
|
||||
}
|
||||
_contentLoaded = true;
|
||||
System.Uri resourceLocater = new System.Uri("/AeroMC-Setup;component/app.xaml", System.UriKind.Relative);
|
||||
|
||||
#line 1 "..\..\..\App.xaml"
|
||||
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Application Entry Point.
|
||||
/// </summary>
|
||||
[System.STAThreadAttribute()]
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||
public static void Main() {
|
||||
AeroMCSetup.App app = new AeroMCSetup.App();
|
||||
app.InitializeComponent();
|
||||
app.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,842 @@
|
||||
#pragma checksum "..\..\..\MainWindow.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "8CB5D6EC1BE0A9A19A04D9A92B2B0961370843E0"
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Automation;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Controls.Ribbon;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Forms.Integration;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Effects;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Media.Media3D;
|
||||
using System.Windows.Media.TextFormatting;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Shell;
|
||||
|
||||
|
||||
namespace AeroMCSetup {
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// MainWindow
|
||||
/// </summary>
|
||||
public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector {
|
||||
|
||||
|
||||
#line 140 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock VersionText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 142 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel Steps;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 144 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Dot0;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 144 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Num0;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 145 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Label0;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 147 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Line0;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 149 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Dot1;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 149 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Num1;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 150 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Label1;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 152 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Line1;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 154 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Dot2;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 154 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Num2;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 155 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Label2;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 157 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Line2;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 159 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel ImportStepRow;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 160 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Dot3;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 160 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Num3;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 161 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Label3;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 163 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Line3;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 165 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border Dot4;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 165 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Num4;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 166 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Label4;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 179 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock WindowTitle;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 185 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button CloseButton;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 193 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageWelcome;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 240 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border UpdateHint;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 245 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock UpdateHintText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 248 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border RunningHint;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 260 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.DockPanel PageLicense;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 264 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox AcceptLicense;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 271 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel LicensePanel;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 277 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageOptions;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 286 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBox DirBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 295 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox DesktopLinkBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 301 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageImport;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 306 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel ImportList;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 313 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageProgress;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 314 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border SuccessIcon;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 316 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Media.ScaleTransform SuccessScale;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 321 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock ProgressTitle;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 323 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel ProgressBarPanel;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 324 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock PercentText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 326 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border ProgressTrack;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 327 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border ProgressFill;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 330 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock ProgressText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 332 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel DonePanel;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 333 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox LaunchBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 338 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageUninstall;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 349 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock DataInfo;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 350 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox RemoveDataBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 352 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock RemoveDataWarning;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 362 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button BackButton;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 363 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button NextButton;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 364 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button CancelButton;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 368 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Grid DialogOverlay;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 369 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border DialogCard;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 372 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Media.ScaleTransform DialogScale;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 376 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock DialogIcon;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 379 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock DialogTitle;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 382 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock DialogText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 385 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button DialogSecondary;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 386 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button DialogPrimary;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
private bool _contentLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// InitializeComponent
|
||||
/// </summary>
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||
public void InitializeComponent() {
|
||||
if (_contentLoaded) {
|
||||
return;
|
||||
}
|
||||
_contentLoaded = true;
|
||||
System.Uri resourceLocater = new System.Uri("/AeroMC-Setup;component/mainwindow.xaml", System.UriKind.Relative);
|
||||
|
||||
#line 1 "..\..\..\MainWindow.xaml"
|
||||
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
}
|
||||
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
|
||||
void System.Windows.Markup.IComponentConnector.Connect(int connectionId, object target) {
|
||||
switch (connectionId)
|
||||
{
|
||||
case 1:
|
||||
this.VersionText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 2:
|
||||
this.Steps = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 3:
|
||||
this.Dot0 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 4:
|
||||
this.Num0 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 5:
|
||||
this.Label0 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 6:
|
||||
this.Line0 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 7:
|
||||
this.Dot1 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 8:
|
||||
this.Num1 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 9:
|
||||
this.Label1 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 10:
|
||||
this.Line1 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 11:
|
||||
this.Dot2 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 12:
|
||||
this.Num2 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 13:
|
||||
this.Label2 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 14:
|
||||
this.Line2 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 15:
|
||||
this.ImportStepRow = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 16:
|
||||
this.Dot3 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 17:
|
||||
this.Num3 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 18:
|
||||
this.Label3 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 19:
|
||||
this.Line3 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 20:
|
||||
this.Dot4 = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 21:
|
||||
this.Num4 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 22:
|
||||
this.Label4 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 23:
|
||||
this.WindowTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 24:
|
||||
|
||||
#line 183 "..\..\..\MainWindow.xaml"
|
||||
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.Minimize_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 25:
|
||||
this.CloseButton = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 185 "..\..\..\MainWindow.xaml"
|
||||
this.CloseButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 26:
|
||||
this.PageWelcome = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 27:
|
||||
this.UpdateHint = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 28:
|
||||
this.UpdateHintText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 29:
|
||||
this.RunningHint = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 30:
|
||||
this.PageLicense = ((System.Windows.Controls.DockPanel)(target));
|
||||
return;
|
||||
case 31:
|
||||
this.AcceptLicense = ((System.Windows.Controls.CheckBox)(target));
|
||||
|
||||
#line 265 "..\..\..\MainWindow.xaml"
|
||||
this.AcceptLicense.Checked += new System.Windows.RoutedEventHandler(this.AcceptLicense_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 265 "..\..\..\MainWindow.xaml"
|
||||
this.AcceptLicense.Unchecked += new System.Windows.RoutedEventHandler(this.AcceptLicense_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 32:
|
||||
this.LicensePanel = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 33:
|
||||
this.PageOptions = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 34:
|
||||
|
||||
#line 285 "..\..\..\MainWindow.xaml"
|
||||
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.ChangeDir_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 35:
|
||||
this.DirBox = ((System.Windows.Controls.TextBox)(target));
|
||||
return;
|
||||
case 36:
|
||||
this.DesktopLinkBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
return;
|
||||
case 37:
|
||||
this.PageImport = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 38:
|
||||
this.ImportList = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 39:
|
||||
this.PageProgress = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 40:
|
||||
this.SuccessIcon = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 41:
|
||||
this.SuccessScale = ((System.Windows.Media.ScaleTransform)(target));
|
||||
return;
|
||||
case 42:
|
||||
this.ProgressTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 43:
|
||||
this.ProgressBarPanel = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 44:
|
||||
this.PercentText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 45:
|
||||
this.ProgressTrack = ((System.Windows.Controls.Border)(target));
|
||||
|
||||
#line 326 "..\..\..\MainWindow.xaml"
|
||||
this.ProgressTrack.SizeChanged += new System.Windows.SizeChangedEventHandler(this.ProgressTrack_SizeChanged);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 46:
|
||||
this.ProgressFill = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 47:
|
||||
this.ProgressText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 48:
|
||||
this.DonePanel = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 49:
|
||||
this.LaunchBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
return;
|
||||
case 50:
|
||||
this.PageUninstall = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 51:
|
||||
this.DataInfo = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 52:
|
||||
this.RemoveDataBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
|
||||
#line 351 "..\..\..\MainWindow.xaml"
|
||||
this.RemoveDataBox.Checked += new System.Windows.RoutedEventHandler(this.RemoveDataBox_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 351 "..\..\..\MainWindow.xaml"
|
||||
this.RemoveDataBox.Unchecked += new System.Windows.RoutedEventHandler(this.RemoveDataBox_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 53:
|
||||
this.RemoveDataWarning = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 54:
|
||||
this.BackButton = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 362 "..\..\..\MainWindow.xaml"
|
||||
this.BackButton.Click += new System.Windows.RoutedEventHandler(this.Back_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 55:
|
||||
this.NextButton = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 363 "..\..\..\MainWindow.xaml"
|
||||
this.NextButton.Click += new System.Windows.RoutedEventHandler(this.Next_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 56:
|
||||
this.CancelButton = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 364 "..\..\..\MainWindow.xaml"
|
||||
this.CancelButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 57:
|
||||
this.DialogOverlay = ((System.Windows.Controls.Grid)(target));
|
||||
return;
|
||||
case 58:
|
||||
this.DialogCard = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 59:
|
||||
this.DialogScale = ((System.Windows.Media.ScaleTransform)(target));
|
||||
return;
|
||||
case 60:
|
||||
this.DialogIcon = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 61:
|
||||
this.DialogTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 62:
|
||||
this.DialogText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 63:
|
||||
this.DialogSecondary = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 385 "..\..\..\MainWindow.xaml"
|
||||
this.DialogSecondary.Click += new System.Windows.RoutedEventHandler(this.DialogSecondary_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 64:
|
||||
this.DialogPrimary = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 386 "..\..\..\MainWindow.xaml"
|
||||
this.DialogPrimary.Click += new System.Windows.RoutedEventHandler(this.DialogPrimary_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
}
|
||||
this._contentLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,471 @@
|
||||
{
|
||||
"version": 3,
|
||||
"targets": {
|
||||
".NETFramework,Version=v4.8": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies/1.0.3": {
|
||||
"type": "package",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net48/1.0.3": {
|
||||
"type": "package",
|
||||
"build": {
|
||||
"build/Microsoft.NETFramework.ReferenceAssemblies.net48.targets": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies/1.0.3": {
|
||||
"sha512": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
|
||||
"type": "package",
|
||||
"path": "microsoft.netframework.referenceassemblies/1.0.3",
|
||||
"files": [
|
||||
".nupkg.metadata",
|
||||
".signature.p7s",
|
||||
"microsoft.netframework.referenceassemblies.1.0.3.nupkg.sha512",
|
||||
"microsoft.netframework.referenceassemblies.nuspec"
|
||||
]
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net48/1.0.3": {
|
||||
"sha512": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ==",
|
||||
"type": "package",
|
||||
"path": "microsoft.netframework.referenceassemblies.net48/1.0.3",
|
||||
"files": [
|
||||
".nupkg.metadata",
|
||||
".signature.p7s",
|
||||
"build/.NETFramework/v4.8/Accessibility.dll",
|
||||
"build/.NETFramework/v4.8/Accessibility.xml",
|
||||
"build/.NETFramework/v4.8/CustomMarshalers.dll",
|
||||
"build/.NETFramework/v4.8/CustomMarshalers.xml",
|
||||
"build/.NETFramework/v4.8/Facades/Microsoft.Win32.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.AppContext.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Collections.Concurrent.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Collections.NonGeneric.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Collections.Specialized.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Collections.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ComponentModel.Annotations.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ComponentModel.EventBasedAsync.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ComponentModel.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ComponentModel.TypeConverter.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ComponentModel.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Console.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Data.Common.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Contracts.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Debug.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Diagnostics.FileVersionInfo.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Process.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Diagnostics.StackTrace.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Diagnostics.TextWriterTraceListener.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Tools.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Diagnostics.TraceSource.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Drawing.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Dynamic.Runtime.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Globalization.Calendars.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Globalization.Extensions.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Globalization.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.Compression.ZipFile.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.DriveInfo.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.Watcher.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.IsolatedStorage.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.MemoryMappedFiles.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.Pipes.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.UnmanagedMemoryStream.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Linq.Expressions.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Linq.Parallel.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Linq.Queryable.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Linq.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.Http.Rtc.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.NameResolution.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.NetworkInformation.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.Ping.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.Requests.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.Security.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.Sockets.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.WebHeaderCollection.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.WebSockets.Client.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.WebSockets.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ObjectModel.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Reflection.Emit.ILGeneration.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Reflection.Emit.Lightweight.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Reflection.Emit.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Reflection.Extensions.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Reflection.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Reflection.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Resources.Reader.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Resources.ResourceManager.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Resources.Writer.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.CompilerServices.VisualC.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.Extensions.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.Handles.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.InteropServices.RuntimeInformation.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.InteropServices.WindowsRuntime.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.InteropServices.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.Numerics.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Formatters.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Json.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Xml.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Security.Claims.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Algorithms.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Csp.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Encoding.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.X509Certificates.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Security.Principal.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Security.SecureString.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Duplex.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Http.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ServiceModel.NetTcp.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Security.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Text.Encoding.Extensions.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Text.Encoding.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Text.RegularExpressions.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Threading.Overlapped.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Threading.Tasks.Parallel.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Threading.Tasks.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Threading.Thread.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Threading.ThreadPool.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Threading.Timer.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Threading.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ValueTuple.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Xml.ReaderWriter.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Xml.XDocument.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Xml.XPath.XDocument.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Xml.XPath.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Xml.XmlDocument.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Xml.XmlSerializer.dll",
|
||||
"build/.NETFramework/v4.8/Facades/netstandard.dll",
|
||||
"build/.NETFramework/v4.8/ISymWrapper.dll",
|
||||
"build/.NETFramework/v4.8/ISymWrapper.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.Activities.Build.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.Activities.Build.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Conversion.v4.0.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Conversion.v4.0.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Engine.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Engine.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Framework.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Framework.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Tasks.v4.0.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Tasks.v4.0.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Utilities.v4.0.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Utilities.v4.0.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.CSharp.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.CSharp.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.JScript.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.JScript.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.Data.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.Data.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualBasic.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualBasic.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualC.STLCLR.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualC.STLCLR.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualC.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualC.xml",
|
||||
"build/.NETFramework/v4.8/PermissionSets/FullTrust.xml",
|
||||
"build/.NETFramework/v4.8/PermissionSets/Internet.xml",
|
||||
"build/.NETFramework/v4.8/PermissionSets/LocalIntranet.xml",
|
||||
"build/.NETFramework/v4.8/PresentationBuildTasks.dll",
|
||||
"build/.NETFramework/v4.8/PresentationBuildTasks.xml",
|
||||
"build/.NETFramework/v4.8/PresentationCore.dll",
|
||||
"build/.NETFramework/v4.8/PresentationCore.xml",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Aero.dll",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Aero.xml",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Aero2.dll",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Aero2.xml",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.AeroLite.dll",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.AeroLite.xml",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Classic.dll",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Classic.xml",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Luna.dll",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Luna.xml",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Royale.dll",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Royale.xml",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.dll",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.xml",
|
||||
"build/.NETFramework/v4.8/ReachFramework.dll",
|
||||
"build/.NETFramework/v4.8/ReachFramework.xml",
|
||||
"build/.NETFramework/v4.8/RedistList/FrameworkList.xml",
|
||||
"build/.NETFramework/v4.8/System.Activities.Core.Presentation.dll",
|
||||
"build/.NETFramework/v4.8/System.Activities.Core.Presentation.xml",
|
||||
"build/.NETFramework/v4.8/System.Activities.DurableInstancing.dll",
|
||||
"build/.NETFramework/v4.8/System.Activities.DurableInstancing.xml",
|
||||
"build/.NETFramework/v4.8/System.Activities.Presentation.dll",
|
||||
"build/.NETFramework/v4.8/System.Activities.Presentation.xml",
|
||||
"build/.NETFramework/v4.8/System.Activities.dll",
|
||||
"build/.NETFramework/v4.8/System.Activities.xml",
|
||||
"build/.NETFramework/v4.8/System.AddIn.Contract.dll",
|
||||
"build/.NETFramework/v4.8/System.AddIn.Contract.xml",
|
||||
"build/.NETFramework/v4.8/System.AddIn.dll",
|
||||
"build/.NETFramework/v4.8/System.AddIn.xml",
|
||||
"build/.NETFramework/v4.8/System.ComponentModel.Composition.Registration.dll",
|
||||
"build/.NETFramework/v4.8/System.ComponentModel.Composition.Registration.xml",
|
||||
"build/.NETFramework/v4.8/System.ComponentModel.Composition.dll",
|
||||
"build/.NETFramework/v4.8/System.ComponentModel.Composition.xml",
|
||||
"build/.NETFramework/v4.8/System.ComponentModel.DataAnnotations.dll",
|
||||
"build/.NETFramework/v4.8/System.ComponentModel.DataAnnotations.xml",
|
||||
"build/.NETFramework/v4.8/System.Configuration.Install.dll",
|
||||
"build/.NETFramework/v4.8/System.Configuration.Install.xml",
|
||||
"build/.NETFramework/v4.8/System.Configuration.dll",
|
||||
"build/.NETFramework/v4.8/System.Configuration.xml",
|
||||
"build/.NETFramework/v4.8/System.Core.dll",
|
||||
"build/.NETFramework/v4.8/System.Core.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.DataSetExtensions.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.DataSetExtensions.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.Entity.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.Entity.Design.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.Entity.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.Entity.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.Linq.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.Linq.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.OracleClient.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.OracleClient.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.Services.Client.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.Services.Client.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.Services.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.Services.Design.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.Services.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.Services.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.SqlXml.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.SqlXml.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.xml",
|
||||
"build/.NETFramework/v4.8/System.Deployment.dll",
|
||||
"build/.NETFramework/v4.8/System.Deployment.xml",
|
||||
"build/.NETFramework/v4.8/System.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Design.xml",
|
||||
"build/.NETFramework/v4.8/System.Device.dll",
|
||||
"build/.NETFramework/v4.8/System.Device.xml",
|
||||
"build/.NETFramework/v4.8/System.Diagnostics.Tracing.dll",
|
||||
"build/.NETFramework/v4.8/System.Diagnostics.Tracing.xml",
|
||||
"build/.NETFramework/v4.8/System.DirectoryServices.AccountManagement.dll",
|
||||
"build/.NETFramework/v4.8/System.DirectoryServices.AccountManagement.xml",
|
||||
"build/.NETFramework/v4.8/System.DirectoryServices.Protocols.dll",
|
||||
"build/.NETFramework/v4.8/System.DirectoryServices.Protocols.xml",
|
||||
"build/.NETFramework/v4.8/System.DirectoryServices.dll",
|
||||
"build/.NETFramework/v4.8/System.DirectoryServices.xml",
|
||||
"build/.NETFramework/v4.8/System.Drawing.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Drawing.Design.xml",
|
||||
"build/.NETFramework/v4.8/System.Drawing.dll",
|
||||
"build/.NETFramework/v4.8/System.Drawing.xml",
|
||||
"build/.NETFramework/v4.8/System.Dynamic.dll",
|
||||
"build/.NETFramework/v4.8/System.EnterpriseServices.Thunk.dll",
|
||||
"build/.NETFramework/v4.8/System.EnterpriseServices.Wrapper.dll",
|
||||
"build/.NETFramework/v4.8/System.EnterpriseServices.dll",
|
||||
"build/.NETFramework/v4.8/System.EnterpriseServices.xml",
|
||||
"build/.NETFramework/v4.8/System.IO.Compression.FileSystem.dll",
|
||||
"build/.NETFramework/v4.8/System.IO.Compression.FileSystem.xml",
|
||||
"build/.NETFramework/v4.8/System.IO.Compression.dll",
|
||||
"build/.NETFramework/v4.8/System.IO.Compression.xml",
|
||||
"build/.NETFramework/v4.8/System.IO.Log.dll",
|
||||
"build/.NETFramework/v4.8/System.IO.Log.xml",
|
||||
"build/.NETFramework/v4.8/System.IdentityModel.Selectors.dll",
|
||||
"build/.NETFramework/v4.8/System.IdentityModel.Selectors.xml",
|
||||
"build/.NETFramework/v4.8/System.IdentityModel.Services.dll",
|
||||
"build/.NETFramework/v4.8/System.IdentityModel.Services.xml",
|
||||
"build/.NETFramework/v4.8/System.IdentityModel.dll",
|
||||
"build/.NETFramework/v4.8/System.IdentityModel.xml",
|
||||
"build/.NETFramework/v4.8/System.Linq.xml",
|
||||
"build/.NETFramework/v4.8/System.Management.Instrumentation.dll",
|
||||
"build/.NETFramework/v4.8/System.Management.Instrumentation.xml",
|
||||
"build/.NETFramework/v4.8/System.Management.dll",
|
||||
"build/.NETFramework/v4.8/System.Management.xml",
|
||||
"build/.NETFramework/v4.8/System.Messaging.dll",
|
||||
"build/.NETFramework/v4.8/System.Messaging.xml",
|
||||
"build/.NETFramework/v4.8/System.Net.Http.WebRequest.dll",
|
||||
"build/.NETFramework/v4.8/System.Net.Http.WebRequest.xml",
|
||||
"build/.NETFramework/v4.8/System.Net.Http.dll",
|
||||
"build/.NETFramework/v4.8/System.Net.Http.xml",
|
||||
"build/.NETFramework/v4.8/System.Net.dll",
|
||||
"build/.NETFramework/v4.8/System.Net.xml",
|
||||
"build/.NETFramework/v4.8/System.Numerics.dll",
|
||||
"build/.NETFramework/v4.8/System.Numerics.xml",
|
||||
"build/.NETFramework/v4.8/System.Printing.dll",
|
||||
"build/.NETFramework/v4.8/System.Printing.xml",
|
||||
"build/.NETFramework/v4.8/System.Reflection.Context.dll",
|
||||
"build/.NETFramework/v4.8/System.Reflection.Context.xml",
|
||||
"build/.NETFramework/v4.8/System.Runtime.Caching.dll",
|
||||
"build/.NETFramework/v4.8/System.Runtime.Caching.xml",
|
||||
"build/.NETFramework/v4.8/System.Runtime.DurableInstancing.dll",
|
||||
"build/.NETFramework/v4.8/System.Runtime.DurableInstancing.xml",
|
||||
"build/.NETFramework/v4.8/System.Runtime.Remoting.dll",
|
||||
"build/.NETFramework/v4.8/System.Runtime.Remoting.xml",
|
||||
"build/.NETFramework/v4.8/System.Runtime.Serialization.Formatters.Soap.dll",
|
||||
"build/.NETFramework/v4.8/System.Runtime.Serialization.Formatters.Soap.xml",
|
||||
"build/.NETFramework/v4.8/System.Runtime.Serialization.dll",
|
||||
"build/.NETFramework/v4.8/System.Runtime.Serialization.xml",
|
||||
"build/.NETFramework/v4.8/System.Security.dll",
|
||||
"build/.NETFramework/v4.8/System.Security.xml",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Activation.dll",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Activation.xml",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Activities.dll",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Activities.xml",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Channels.dll",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Channels.xml",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Discovery.dll",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Discovery.xml",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Routing.dll",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Routing.xml",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Web.dll",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Web.xml",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.dll",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.xml",
|
||||
"build/.NETFramework/v4.8/System.ServiceProcess.dll",
|
||||
"build/.NETFramework/v4.8/System.ServiceProcess.xml",
|
||||
"build/.NETFramework/v4.8/System.Speech.dll",
|
||||
"build/.NETFramework/v4.8/System.Speech.xml",
|
||||
"build/.NETFramework/v4.8/System.Threading.Tasks.Dataflow.xml",
|
||||
"build/.NETFramework/v4.8/System.Transactions.dll",
|
||||
"build/.NETFramework/v4.8/System.Transactions.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.Abstractions.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.ApplicationServices.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.ApplicationServices.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.DataVisualization.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.DataVisualization.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.DataVisualization.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.DynamicData.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.DynamicData.Design.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.DynamicData.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.DynamicData.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.Entity.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.Entity.Design.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.Entity.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.Entity.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.Extensions.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.Extensions.Design.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.Extensions.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.Extensions.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.Mobile.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.Mobile.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.RegularExpressions.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.RegularExpressions.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.Routing.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.Services.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.Services.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.xml",
|
||||
"build/.NETFramework/v4.8/System.Windows.Controls.Ribbon.dll",
|
||||
"build/.NETFramework/v4.8/System.Windows.Controls.Ribbon.xml",
|
||||
"build/.NETFramework/v4.8/System.Windows.Forms.DataVisualization.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Windows.Forms.DataVisualization.dll",
|
||||
"build/.NETFramework/v4.8/System.Windows.Forms.DataVisualization.xml",
|
||||
"build/.NETFramework/v4.8/System.Windows.Forms.dll",
|
||||
"build/.NETFramework/v4.8/System.Windows.Forms.xml",
|
||||
"build/.NETFramework/v4.8/System.Windows.Input.Manipulations.dll",
|
||||
"build/.NETFramework/v4.8/System.Windows.Input.Manipulations.xml",
|
||||
"build/.NETFramework/v4.8/System.Windows.Presentation.dll",
|
||||
"build/.NETFramework/v4.8/System.Windows.Presentation.xml",
|
||||
"build/.NETFramework/v4.8/System.Windows.dll",
|
||||
"build/.NETFramework/v4.8/System.Workflow.Activities.dll",
|
||||
"build/.NETFramework/v4.8/System.Workflow.Activities.xml",
|
||||
"build/.NETFramework/v4.8/System.Workflow.ComponentModel.dll",
|
||||
"build/.NETFramework/v4.8/System.Workflow.ComponentModel.xml",
|
||||
"build/.NETFramework/v4.8/System.Workflow.Runtime.dll",
|
||||
"build/.NETFramework/v4.8/System.Workflow.Runtime.xml",
|
||||
"build/.NETFramework/v4.8/System.WorkflowServices.dll",
|
||||
"build/.NETFramework/v4.8/System.WorkflowServices.xml",
|
||||
"build/.NETFramework/v4.8/System.Xaml.dll",
|
||||
"build/.NETFramework/v4.8/System.Xaml.xml",
|
||||
"build/.NETFramework/v4.8/System.Xml.Linq.dll",
|
||||
"build/.NETFramework/v4.8/System.Xml.Linq.xml",
|
||||
"build/.NETFramework/v4.8/System.Xml.Serialization.dll",
|
||||
"build/.NETFramework/v4.8/System.Xml.dll",
|
||||
"build/.NETFramework/v4.8/System.Xml.xml",
|
||||
"build/.NETFramework/v4.8/System.dll",
|
||||
"build/.NETFramework/v4.8/System.xml",
|
||||
"build/.NETFramework/v4.8/UIAutomationClient.dll",
|
||||
"build/.NETFramework/v4.8/UIAutomationClient.xml",
|
||||
"build/.NETFramework/v4.8/UIAutomationClientsideProviders.dll",
|
||||
"build/.NETFramework/v4.8/UIAutomationClientsideProviders.xml",
|
||||
"build/.NETFramework/v4.8/UIAutomationProvider.dll",
|
||||
"build/.NETFramework/v4.8/UIAutomationProvider.xml",
|
||||
"build/.NETFramework/v4.8/UIAutomationTypes.dll",
|
||||
"build/.NETFramework/v4.8/UIAutomationTypes.xml",
|
||||
"build/.NETFramework/v4.8/WindowsBase.dll",
|
||||
"build/.NETFramework/v4.8/WindowsBase.xml",
|
||||
"build/.NETFramework/v4.8/WindowsFormsIntegration.dll",
|
||||
"build/.NETFramework/v4.8/WindowsFormsIntegration.xml",
|
||||
"build/.NETFramework/v4.8/XamlBuildTask.dll",
|
||||
"build/.NETFramework/v4.8/XamlBuildTask.xml",
|
||||
"build/.NETFramework/v4.8/mscorlib.dll",
|
||||
"build/.NETFramework/v4.8/mscorlib.xml",
|
||||
"build/.NETFramework/v4.8/namespaces.xml",
|
||||
"build/.NETFramework/v4.8/sysglobl.dll",
|
||||
"build/.NETFramework/v4.8/sysglobl.xml",
|
||||
"build/Microsoft.NETFramework.ReferenceAssemblies.net48.targets",
|
||||
"microsoft.netframework.referenceassemblies.net48.1.0.3.nupkg.sha512",
|
||||
"microsoft.netframework.referenceassemblies.net48.nuspec"
|
||||
]
|
||||
}
|
||||
},
|
||||
"projectFileDependencyGroups": {
|
||||
".NETFramework,Version=v4.8": [
|
||||
"Microsoft.NETFramework.ReferenceAssemblies >= 1.0.3"
|
||||
]
|
||||
},
|
||||
"packageFolders": {
|
||||
"C:\\Users\\Viper\\.nuget\\packages\\": {}
|
||||
},
|
||||
"project": {
|
||||
"version": "1.2.3",
|
||||
"restore": {
|
||||
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
||||
"projectName": "AeroMC-Setup",
|
||||
"projectPath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
||||
"packagesPath": "C:\\Users\\Viper\\.nuget\\packages\\",
|
||||
"outputPath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\Viper\\AppData\\Roaming\\NuGet\\NuGet.Config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net48"
|
||||
],
|
||||
"sources": {
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net48": {
|
||||
"targetAlias": "net48",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.100"
|
||||
},
|
||||
"frameworks": {
|
||||
"net48": {
|
||||
"targetAlias": "net48",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"suppressParent": "All",
|
||||
"target": "Package",
|
||||
"version": "[1.0.3, )",
|
||||
"autoReferenced": true
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.112\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "5nf4o8NXPS0=",
|
||||
"success": true,
|
||||
"projectFilePath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
||||
"expectedPackageFiles": [
|
||||
"C:\\Users\\Viper\\.nuget\\packages\\microsoft.netframework.referenceassemblies\\1.0.3\\microsoft.netframework.referenceassemblies.1.0.3.nupkg.sha512",
|
||||
"C:\\Users\\Viper\\.nuget\\packages\\microsoft.netframework.referenceassemblies.net48\\1.0.3\\microsoft.netframework.referenceassemblies.net48.1.0.3.nupkg.sha512"
|
||||
],
|
||||
"logs": []
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# AeroMC Capes
|
||||
|
||||
Shows the **custom capes (including HD up to 1024×512)** of players who designed their cape in the
|
||||
[AeroMC Launcher](https://aeromc.viper.ipv64.net) – right in the game, even if you use a different
|
||||
launcher yourself.
|
||||
|
||||
## How it works
|
||||
- Drop the mod into your `mods` folder – done. No configuration, no Fabric API required.
|
||||
- Whenever Minecraft loads a player's skin, the mod asks the AeroMC server whether that player has
|
||||
an AeroMC cape and shows it instead of the regular cape.
|
||||
- If a player has no AeroMC cape, nothing changes (official Mojang cape or none).
|
||||
- **Client-side only** – servers don't need anything.
|
||||
|
||||
## Compatibility
|
||||
- **Fabric** and **Quilt**
|
||||
- **One jar for all versions**: Minecraft 1.14 to 26.x
|
||||
- Works with old Fabric Loader versions too (0.10 and newer)
|
||||
- Players using the AeroMC Launcher don't need this mod (the launcher already does this on its
|
||||
own) – but having it installed doesn't hurt either.
|
||||
|
||||
## Privacy
|
||||
The mod only sends the (public) UUIDs of players whose skins are currently being loaded to
|
||||
`viper-network.de` to look up their capes. Results are cached for a few minutes.
|
||||
No account or login data is ever transmitted.
|
||||
|
||||
## Create your own cape
|
||||
In the AeroMC Launcher: click your account name (top right) → **Skin ändern…** (Change skin) →
|
||||
in the **Umhang** (Cape) section click **Gestalten…** (Design). You can paint your cape pixel by
|
||||
pixel or insert any image, in sizes from 64×32 up to HD 1024×512.
|
||||
|
||||
---
|
||||
|
||||
### Hinweise zum Hochladen (nicht Teil der Beschreibung)
|
||||
- Name: AeroMC Capes (URL bleibt `aeromc-umhaenge`)
|
||||
- Kurzbeschreibung: Shows custom capes (HD up to 1024×512) made in the AeroMC Launcher – even if you use another launcher. One jar for 1.14–26.x, client-side.
|
||||
- Datei: `dist/aeromc-umhaenge-fabric-1.0.0.jar`
|
||||
- Loader: Fabric, Quilt · Umgebung: Client · Server: nicht nötig
|
||||
- Spielversionen: alle Releases von 1.14 bis zur aktuellen
|
||||
- Kategorien: Sozial (hervorgehoben), Werkzeug („Cosmetic“ gibt es für Mods nicht)
|
||||
- Abhängigkeiten: keine
|
||||
- Lizenz: aktuell „All Rights Reserved“ (in `fabric.mod.json`) – Modrinth akzeptiert das, eine
|
||||
offene Lizenz (z. B. MIT) wirkt aber vertrauenswürdiger.
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"required": false,
|
||||
"minVersion": "0.8",
|
||||
"package": "de.aeromc.capes.mixin",
|
||||
"compatibilityLevel": "JAVA_8",
|
||||
"client": ["SessionServiceMixin", "SkinLaderMixin"],
|
||||
"injectors": {
|
||||
"defaultRequire": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "aeromc-capes",
|
||||
"version": "1.0.0",
|
||||
"name": "AeroMC Umhänge",
|
||||
"description": "Zeigt die eigenen Umhänge (auch HD) von AeroMC-Spielern im Spiel – gestaltet im AeroMC Launcher. Nur auf dem Client nötig. Funktioniert mit jeder Minecraft-Version, die Fabric unterstützt.",
|
||||
"authors": ["M_Viper"],
|
||||
"contact": {
|
||||
"homepage": "https://aeromc.viper.ipv64.net"
|
||||
},
|
||||
"license": "LicenseRef-All-Rights-Reserved",
|
||||
"icon": "assets/aeromc-capes/icon.png",
|
||||
"environment": "client",
|
||||
"mixins": ["aeromc-capes.mixins.json"],
|
||||
"depends": {
|
||||
"fabricloader": ">=0.10.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package de.aeromc.capes.mixin;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.properties.Property;
|
||||
import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService;
|
||||
import de.aeromc.capes.mod.Umhaenge;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Inject;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
/**
|
||||
* Hängt sich an die Textur-Methoden von authlib (beide Fassungen, siehe
|
||||
* agent/…/Umbau.java). require = 0: Es greift immer nur die Methode, die es in
|
||||
* der jeweiligen authlib-Fassung gibt, die andere wird still übersprungen.
|
||||
* remap = false: authlib ist nicht verschleiert, Namen bleiben wie sie sind.
|
||||
*/
|
||||
@Mixin(value = YggdrasilMinecraftSessionService.class, remap = false)
|
||||
public abstract class SessionServiceMixin {
|
||||
|
||||
// wird in den statischen Initialisierer von authlib übernommen: zeigt im Spielprotokoll, dass der Mod greift
|
||||
static {
|
||||
Umhaenge.authlibGreift = true;
|
||||
Umhaenge.meldeAktiv("authlib");
|
||||
}
|
||||
|
||||
// authlib 5+ (Minecraft 1.20.2 … 26.x)
|
||||
@Inject(method = "unpackTextures", at = @At("RETURN"), cancellable = true, remap = false, require = 0)
|
||||
private void aeromc$neu(Property eigenschaft, CallbackInfoReturnable<Object> cir) {
|
||||
aeromc$ersetze(cir, eigenschaft);
|
||||
}
|
||||
|
||||
// authlib 1–4 (Minecraft 1.14 … 1.20.1)
|
||||
@Inject(method = "getTextures(Lcom/mojang/authlib/GameProfile;Z)Ljava/util/Map;", at = @At("RETURN"), cancellable = true, remap = false, require = 0)
|
||||
private void aeromc$alt(GameProfile profil, boolean sicher, CallbackInfoReturnable<Object> cir) {
|
||||
aeromc$ersetze(cir, profil);
|
||||
}
|
||||
|
||||
private static void aeromc$ersetze(CallbackInfoReturnable<Object> cir, Object argument) {
|
||||
try {
|
||||
Object alt = cir.getReturnValue();
|
||||
Object neu = Umhaenge.mitUmhang(alt, argument);
|
||||
if (neu != alt) cir.setReturnValue(neu);
|
||||
} catch (Throwable ignoriert) {
|
||||
// im Zweifel bleibt alles wie ohne Mod
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.aeromc.capes.mixin;
|
||||
|
||||
import com.mojang.authlib.GameProfile;
|
||||
import com.mojang.authlib.minecraft.MinecraftSessionService;
|
||||
import de.aeromc.capes.mod.Umhaenge;
|
||||
import net.minecraft.class_1071;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Zweiter Weg für alte Fabric Loader (vor 0.12): Die lassen Mixins nur in
|
||||
* Minecraft selbst zu, nicht in authlib – SessionServiceMixin greift dort also
|
||||
* nie. Deshalb wird hier der Aufruf von getTextures im Skin-Lader von Minecraft
|
||||
* (PlayerSkinProvider = class_1071, Intermediary-Name, gleich in 1.14 … 1.20.1)
|
||||
* umgeleitet. Bei neuen Loadern erledigt das bereits SessionServiceMixin, dann
|
||||
* reicht diese Umleitung das Ergebnis nur unverändert durch.
|
||||
*/
|
||||
@Mixin(value = class_1071.class, targets = "net.minecraft.class_1071$1", remap = false)
|
||||
public abstract class SkinLaderMixin {
|
||||
|
||||
@Redirect(method = "*", at = @At(value = "INVOKE", remap = false,
|
||||
target = "Lcom/mojang/authlib/minecraft/MinecraftSessionService;getTextures(Lcom/mojang/authlib/GameProfile;Z)Ljava/util/Map;"),
|
||||
remap = false, require = 0)
|
||||
private Map aeromc$texturen(MinecraftSessionService dienst, GameProfile profil, boolean sicher) {
|
||||
Map texturen = dienst.getTextures(profil, sicher);
|
||||
if (Umhaenge.authlibGreift) return texturen;
|
||||
Umhaenge.meldeAktiv("Skin-Lader");
|
||||
try {
|
||||
Object neu = Umhaenge.mitUmhang(texturen, profil);
|
||||
return neu instanceof Map ? (Map) neu : texturen;
|
||||
} catch (Throwable ignoriert) {
|
||||
return texturen;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package de.aeromc.capes.mod;
|
||||
|
||||
import de.aeromc.capes.Nachschlagen;
|
||||
|
||||
/**
|
||||
* AeroMC Umhänge – Fabric/Quilt-Mod für Spieler, die NICHT den AeroMC Launcher
|
||||
* nutzen. Er macht dasselbe wie der Umhang-Agent des Launchers: In Mojangs
|
||||
* authlib wird der AeroMC-Umhang eines Spielers eingesetzt, sobald Minecraft
|
||||
* dessen Skin lädt. Da nur authlib angepasst wird (nicht Minecraft selbst),
|
||||
* passt ein und derselbe Mod zu jeder Minecraft-Version, die Fabric kennt.
|
||||
* Alte Fabric Loader (vor 0.12) erlauben keine Mixins in authlib, dort greift
|
||||
* stattdessen SkinLaderMixin im Skin-Lader von Minecraft.
|
||||
*/
|
||||
public final class Umhaenge {
|
||||
private static final Nachschlagen DIENST = new Nachschlagen(Nachschlagen.STANDARD_BASIS);
|
||||
|
||||
/** true, sobald SessionServiceMixin in authlib eingebaut wurde (dann ist SkinLaderMixin überflüssig). */
|
||||
public static volatile boolean authlibGreift;
|
||||
private static volatile boolean gemeldet;
|
||||
|
||||
private Umhaenge() {}
|
||||
|
||||
/** Schreibt einmalig ins Spielprotokoll, dass (und über welchen Weg) der Mod greift. */
|
||||
public static void meldeAktiv(String weg) {
|
||||
if (gemeldet || agentAktiv()) return;
|
||||
gemeldet = true;
|
||||
System.out.println("[AeroMC] Umhang-Mod aktiv (" + weg + ")");
|
||||
}
|
||||
|
||||
/** Läuft das Spiel über den AeroMC Launcher, setzt schon dessen Agent die Umhänge ein. */
|
||||
public static boolean agentAktiv() {
|
||||
return System.getProperties().containsKey("aeromc.capes.hook");
|
||||
}
|
||||
|
||||
public static Object mitUmhang(Object ergebnis, Object argument) {
|
||||
if (agentAktiv()) return ergebnis;
|
||||
return DIENST.apply(ergebnis, argument);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package net.minecraft;
|
||||
|
||||
/** Nur zum Kompilieren: Platzhalter für Minecrafts PlayerSkinProvider (Intermediary-Name). Kommt nicht ins Mod-Jar. */
|
||||
public class class_1071 {}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "aeromc",
|
||||
"productName": "AeroMC",
|
||||
"version": "1.0.9",
|
||||
"version": "1.2.3",
|
||||
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
|
||||
"homepage": "https://m-viper.de",
|
||||
"launcherWebsite": "https://aeromc.viper.ipv64.net",
|
||||
@@ -11,10 +11,10 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"prebuild": "node scripts/write-build-config.js",
|
||||
"prebuild:portable": "node scripts/write-build-config.js",
|
||||
"build": "electron-builder --win nsis --x64 && node -e \"const fs=require('fs');const path=require('path');['dist/builder-debug.yml','dist/builder-effective-config.yaml','dist/win-unpacked'].forEach((target)=>fs.rmSync(target,{recursive:true,force:true}));if(fs.existsSync('dist')){const source=path.join('dist','AeroMC.exe');const target=path.join('dist','AeroMC-Setup.exe');if(fs.existsSync(target))fs.rmSync(target,{force:true});if(fs.existsSync(source))fs.renameSync(source,target);for(const name of fs.readdirSync('dist')){if((name.endsWith('.exe')&&name!=='AeroMC-Setup.exe'&&name!=='AeroMC-Portable.exe')||name.endsWith('.blockmap')){fs.rmSync(path.join('dist',name),{force:true})}}}\"",
|
||||
"build:portable": "electron-builder --win portable --x64 && node -e \"const fs=require('fs');const path=require('path');['dist/builder-debug.yml','dist/builder-effective-config.yaml','dist/win-unpacked'].forEach((target)=>fs.rmSync(target,{recursive:true,force:true}));if(fs.existsSync('dist')){const source=path.join('dist','AeroMC.exe');const target=path.join('dist','AeroMC-Portable.exe');if(fs.existsSync(target))fs.rmSync(target,{force:true});if(fs.existsSync(source))fs.renameSync(source,target);for(const name of fs.readdirSync('dist')){if((name.endsWith('.exe')&&name!=='AeroMC-Setup.exe'&&name!=='AeroMC-Portable.exe')||name.endsWith('.blockmap')){fs.rmSync(path.join('dist',name),{force:true})}}}\""
|
||||
"prebuild": "node scripts/pruefe-version.js && node scripts/write-build-config.js",
|
||||
"prebuild:portable": "node scripts/pruefe-version.js && node scripts/write-build-config.js",
|
||||
"build": "electron-builder --win dir && node scripts/build-setup.js && node scripts/sign-release.js setup",
|
||||
"build:portable": "electron-builder --win portable --x64 && node -e \"const fs=require('fs');const path=require('path');['dist/builder-debug.yml','dist/builder-effective-config.yaml','dist/win-unpacked'].forEach((target)=>fs.rmSync(target,{recursive:true,force:true}));if(fs.existsSync('dist')){const source=path.join('dist','AeroMC.exe');const target=path.join('dist','AeroMC-Portable.exe');if(fs.existsSync(target))fs.rmSync(target,{force:true});if(fs.existsSync(source))fs.renameSync(source,target);for(const name of fs.readdirSync('dist')){if((name.endsWith('.exe')&&name!=='AeroMC-Setup.exe'&&name!=='AeroMC-Portable.exe')||name.endsWith('.blockmap')){fs.rmSync(path.join('dist',name),{force:true})}}}\" && node scripts/sign-release.js portable"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.aeromc.launcher",
|
||||
@@ -26,23 +26,39 @@
|
||||
"package.json",
|
||||
"LICENSE"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"node_modules/loopback-capture/**/*",
|
||||
"node_modules/node-window-manager/**/*",
|
||||
"node_modules/ffmpeg-static/**/*"
|
||||
],
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"target": "dir",
|
||||
"icon": "src/assets/icon.ico"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
"electronFuses": {
|
||||
"runAsNode": false,
|
||||
"enableNodeOptionsEnvironmentVariable": false,
|
||||
"enableNodeCliInspectArguments": false,
|
||||
"enableEmbeddedAsarIntegrityValidation": true,
|
||||
"onlyLoadAppFromAsar": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^28.0.0",
|
||||
"electron-builder": "^24.0.0"
|
||||
"electron": "^44.6.0",
|
||||
"electron-builder": "^26.15.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xhayper/discord-rpc": "^1.3.4",
|
||||
"archiver": "^7.0.1",
|
||||
"ffmpeg-static": "^5.3.0",
|
||||
"loopback-capture": "^3.0.2",
|
||||
"node-window-manager": "^2.2.4",
|
||||
"skinview3d": "^3.4.2",
|
||||
"sql.js": "^1.14.2",
|
||||
"undici": "^6.28.0",
|
||||
"unzipper": "^0.12.5"
|
||||
},
|
||||
"overrides": {
|
||||
"brace-expansion@^2.0.0": "^2.1.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* build-agent.js – baut src/assets/agent/aeromc-capes.jar (AeroMC-Umhang-Agent)
|
||||
* ------------------------------------------------------------------------------
|
||||
* Braucht ein JDK (javac/java, ab 11) und ASM 9 – beides wird nur beim Bauen
|
||||
* gebraucht, die fertige Datei liegt im Projekt. Gesucht wird das JDK über
|
||||
* JAVA_HOME bzw. unter "C:\Program Files\Eclipse Adoptium", ASM im
|
||||
* Bibliotheksordner von AeroMC (dort legen Fabric/Forge es ohnehin ab).
|
||||
* Der Agent wird für Java 8 übersetzt, läuft also mit jeder Minecraft-Version.
|
||||
*
|
||||
* Aufruf: node scripts/build-agent.js
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const WURZEL = path.join(__dirname, '..');
|
||||
const ZIEL = path.join(WURZEL, 'src', 'assets', 'agent', 'aeromc-capes.jar');
|
||||
|
||||
function findeJdk() {
|
||||
const kandidaten = [];
|
||||
if (process.env.JAVA_HOME) kandidaten.push(process.env.JAVA_HOME);
|
||||
const adoptium = 'C:\\Program Files\\Eclipse Adoptium';
|
||||
if (fs.existsSync(adoptium)) {
|
||||
for (const d of fs.readdirSync(adoptium).filter((n) => /^jdk-/.test(n)).sort().reverse()) kandidaten.push(path.join(adoptium, d));
|
||||
}
|
||||
const jdk = kandidaten.find((d) => fs.existsSync(path.join(d, 'bin', process.platform === 'win32' ? 'javac.exe' : 'javac')));
|
||||
if (!jdk) throw new Error('Kein JDK gefunden (JAVA_HOME setzen).');
|
||||
return jdk;
|
||||
}
|
||||
|
||||
function findeAsm(name) {
|
||||
const basis = path.join(process.env.APPDATA || os.homedir(), 'aeromc', 'shared', 'libraries', 'org', 'ow2', 'asm', name);
|
||||
if (!fs.existsSync(basis)) throw new Error(`ASM (${name}) nicht gefunden unter ${basis}`);
|
||||
const versionen = fs.readdirSync(basis).filter((v) => /^9\./.test(v))
|
||||
.sort((a, b) => b.split('.').map(Number).reduce((s, x, i) => s || x - (a.split('.').map(Number)[i] || 0), 0));
|
||||
const datei = path.join(basis, versionen[0], `${name}-${versionen[0]}.jar`);
|
||||
if (!fs.existsSync(datei)) throw new Error('ASM-Datei fehlt: ' + datei);
|
||||
return datei;
|
||||
}
|
||||
|
||||
function quellen(ordner) {
|
||||
return fs.readdirSync(ordner, { withFileTypes: true }).flatMap((e) => {
|
||||
const p = path.join(ordner, e.name);
|
||||
return e.isDirectory() ? quellen(p) : (e.name.endsWith('.java') ? [p] : []);
|
||||
});
|
||||
}
|
||||
|
||||
const jdk = findeJdk();
|
||||
const javac = path.join(jdk, 'bin', 'javac');
|
||||
const java = path.join(jdk, 'bin', 'java');
|
||||
const asm = findeAsm('asm');
|
||||
const asmCommons = findeAsm('asm-commons');
|
||||
const asmTree = findeAsm('asm-tree');
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'aeromc-agent-'));
|
||||
const klassen = path.join(tmp, 'klassen');
|
||||
const werkzeug = path.join(tmp, 'werkzeug');
|
||||
console.log('JDK:', jdk);
|
||||
console.log('ASM:', asm);
|
||||
try {
|
||||
execFileSync(javac, ['--release', '8', '-nowarn', '-encoding', 'UTF-8', '-cp', [asm, asmTree].join(path.delimiter), '-d', klassen,
|
||||
...quellen(path.join(WURZEL, 'agent', 'src'))], { stdio: 'inherit' });
|
||||
execFileSync(javac, ['-encoding', 'UTF-8', '-cp', [asm, asmCommons].join(path.delimiter), '-d', werkzeug,
|
||||
path.join(WURZEL, 'agent', 'tools', 'Shade.java')], { stdio: 'inherit' });
|
||||
fs.mkdirSync(path.dirname(ZIEL), { recursive: true });
|
||||
execFileSync(java, ['-cp', [werkzeug, asm, asmCommons].join(path.delimiter), 'Shade', ZIEL, klassen, asm, asmTree], { stdio: 'inherit' });
|
||||
console.log(`Fertig: ${ZIEL} (${fs.statSync(ZIEL).size} Bytes)`);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* build-mod.js – baut den Fabric/Quilt-Mod "AeroMC Umhänge" (dist/aeromc-umhaenge-fabric-<version>.jar)
|
||||
* -------------------------------------------------------------------------------------------------
|
||||
* Für Spieler, die NICHT den AeroMC Launcher nutzen. Der Mod greift wie der
|
||||
* Umhang-Agent nur in Mojangs authlib ein (Mixin, ohne Namensumsetzung) – darum
|
||||
* braucht es weder Gradle noch Fabric Loom, und eine Datei passt zu allen
|
||||
* Minecraft-Versionen, die Fabric unterstützt. Übersetzt wird für Java 8.
|
||||
*
|
||||
* Gebraucht werden (nur beim Bauen) ein JDK sowie sponge-mixin und authlib aus
|
||||
* dem Bibliotheksordner von AeroMC. Der Nachschlage-Code ist derselbe wie im
|
||||
* Agenten (agent/src/de/aeromc/capes/Nachschlagen.java).
|
||||
*
|
||||
* Aufruf: node scripts/build-mod.js
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const WURZEL = path.join(__dirname, '..');
|
||||
const LIB = path.join(process.env.APPDATA || os.homedir(), 'aeromc', 'shared', 'libraries');
|
||||
const modInfo = JSON.parse(fs.readFileSync(path.join(WURZEL, 'mod', 'resources', 'fabric.mod.json'), 'utf8'));
|
||||
const ZIEL = path.join(WURZEL, 'dist', `aeromc-umhaenge-fabric-${modInfo.version}.jar`);
|
||||
|
||||
function findeJdk() {
|
||||
const kandidaten = [];
|
||||
if (process.env.JAVA_HOME) kandidaten.push(process.env.JAVA_HOME);
|
||||
const adoptium = 'C:\\Program Files\\Eclipse Adoptium';
|
||||
if (fs.existsSync(adoptium)) {
|
||||
for (const d of fs.readdirSync(adoptium).filter((n) => /^jdk-/.test(n)).sort().reverse()) kandidaten.push(path.join(adoptium, d));
|
||||
}
|
||||
const jdk = kandidaten.find((d) => fs.existsSync(path.join(d, 'bin', process.platform === 'win32' ? 'javac.exe' : 'javac')));
|
||||
if (!jdk) throw new Error('Kein JDK gefunden (JAVA_HOME setzen).');
|
||||
return jdk;
|
||||
}
|
||||
|
||||
// neueste (oder mit aelteste=true älteste) Fassung einer Bibliothek im AeroMC-Bibliotheksordner
|
||||
function bibliothek(gruppe, name, aelteste = false) {
|
||||
const basis = path.join(LIB, ...gruppe.split('.'), name);
|
||||
if (!fs.existsSync(basis)) throw new Error(`${name} nicht gefunden unter ${basis}`);
|
||||
const fassungen = fs.readdirSync(basis).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
||||
const v = aelteste ? fassungen[0] : fassungen.pop();
|
||||
const datei = path.join(basis, v, `${name}-${v}.jar`);
|
||||
if (!fs.existsSync(datei)) throw new Error('Datei fehlt: ' + datei);
|
||||
return datei;
|
||||
}
|
||||
|
||||
function quellen(ordner) {
|
||||
return fs.readdirSync(ordner, { withFileTypes: true }).flatMap((e) => {
|
||||
const p = path.join(ordner, e.name);
|
||||
return e.isDirectory() ? quellen(p) : (e.name.endsWith('.java') ? [p] : []);
|
||||
});
|
||||
}
|
||||
|
||||
// authlib in der ältesten Fassung: SkinLaderMixin ruft getTextures(GameProfile, boolean) auf, das es nur bis authlib 4 gibt
|
||||
const jdk = findeJdk();
|
||||
const klassenpfad = [bibliothek('net.fabricmc', 'sponge-mixin'), bibliothek('com.mojang', 'authlib', true)].join(path.delimiter);
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'aeromc-mod-'));
|
||||
const inhalt = path.join(tmp, 'inhalt');
|
||||
const stubs = path.join(tmp, 'stubs');
|
||||
console.log('JDK:', jdk);
|
||||
try {
|
||||
// Platzhalter für Minecraft-Klassen (nur zum Kompilieren, landen nicht im Jar)
|
||||
execFileSync(path.join(jdk, 'bin', 'javac'), ['--release', '8', '-nowarn', '-encoding', 'UTF-8', '-d', stubs,
|
||||
...quellen(path.join(WURZEL, 'mod', 'stubs'))], { stdio: 'inherit' });
|
||||
execFileSync(path.join(jdk, 'bin', 'javac'), ['--release', '8', '-nowarn', '-encoding', 'UTF-8', '-proc:none', '-cp', klassenpfad + path.delimiter + stubs, '-d', inhalt,
|
||||
...quellen(path.join(WURZEL, 'mod', 'src')),
|
||||
path.join(WURZEL, 'agent', 'src', 'de', 'aeromc', 'capes', 'Nachschlagen.java')], { stdio: 'inherit' });
|
||||
for (const datei of fs.readdirSync(path.join(WURZEL, 'mod', 'resources'))) {
|
||||
fs.copyFileSync(path.join(WURZEL, 'mod', 'resources', datei), path.join(inhalt, datei));
|
||||
}
|
||||
fs.mkdirSync(path.join(inhalt, 'assets', 'aeromc-capes'), { recursive: true });
|
||||
fs.copyFileSync(path.join(WURZEL, 'src', 'assets', 'logo.png'), path.join(inhalt, 'assets', 'aeromc-capes', 'icon.png'));
|
||||
fs.mkdirSync(path.dirname(ZIEL), { recursive: true });
|
||||
if (fs.existsSync(ZIEL)) fs.rmSync(ZIEL);
|
||||
execFileSync(path.join(jdk, 'bin', 'jar'), ['--create', '--file', ZIEL, '-C', inhalt, '.'], { stdio: 'inherit' });
|
||||
console.log(`Fertig: ${ZIEL} (${fs.statSync(ZIEL).size} Bytes)`);
|
||||
} finally {
|
||||
fs.rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* build-setup.js – eigenes Setup (installer/) mit der App zusammenbauen
|
||||
* ---------------------------------------------------------------------
|
||||
* Läuft nach "electron-builder --win dir" (siehe "build" in package.json):
|
||||
*
|
||||
* 1. dist/win-unpacked (die fertige App) als ZIP packen
|
||||
* 2. installer/ (WPF, .NET Framework 4.8) mit der Version aus package.json bauen
|
||||
* 3. ZIP hinten an die Setup-.exe hängen:
|
||||
* [Setup.exe][ZIP][8 Byte ZIP-Länge][8 Byte "AEROMCPK"]
|
||||
* (das Setup liest den Anhang selbst aus, siehe installer/Payload.cs)
|
||||
* 4. Zwischenstände wegräumen → übrig bleibt dist/AeroMC-Setup.exe
|
||||
*
|
||||
* Benötigt das .NET SDK (dotnet) – das Setup selbst läuft danach auf jedem
|
||||
* Windows 10/11 ohne Zusatzinstallation.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const DIST = path.join(ROOT, 'dist');
|
||||
const APP_DIR = path.join(DIST, 'win-unpacked');
|
||||
const WORK = path.join(DIST, '.setup');
|
||||
const ZIP = path.join(WORK, 'app.zip');
|
||||
const OUT = path.join(DIST, 'AeroMC-Setup.exe');
|
||||
const MAGIC = Buffer.from('AEROMCPK', 'ascii');
|
||||
|
||||
function schritt(text) {
|
||||
console.log(`\n» ${text}`);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const version = require(path.join(ROOT, 'package.json')).version;
|
||||
if (!fs.existsSync(path.join(APP_DIR, 'AeroMC.exe'))) {
|
||||
throw new Error(`${APP_DIR}\\AeroMC.exe fehlt – vorher "electron-builder --win dir" ausführen.`);
|
||||
}
|
||||
fs.rmSync(WORK, { recursive: true, force: true });
|
||||
fs.mkdirSync(WORK, { recursive: true });
|
||||
|
||||
schritt('App wird gepackt …');
|
||||
// .NET-ZIP über PowerShell: in jedem Windows enthalten, keine zusätzliche npm-Abhängigkeit
|
||||
const ps = [
|
||||
'Add-Type -AssemblyName System.IO.Compression.FileSystem',
|
||||
`[System.IO.Compression.ZipFile]::CreateFromDirectory('${APP_DIR.replace(/'/g, "''")}', '${ZIP.replace(/'/g, "''")}', [System.IO.Compression.CompressionLevel]::Optimal, $false)`,
|
||||
].join('; ');
|
||||
execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ps], { stdio: 'inherit' });
|
||||
|
||||
schritt(`Setup ${version} wird gebaut …`);
|
||||
const stubDir = path.join(WORK, 'stub');
|
||||
execFileSync('dotnet', [
|
||||
'build', path.join(ROOT, 'installer', 'AeroMCSetup.csproj'),
|
||||
'-c', 'Release', `-p:Version=${version}`, '-p:DebugType=none',
|
||||
'--nologo', '-v', 'q', '-o', stubDir,
|
||||
], { stdio: 'inherit' });
|
||||
const stub = path.join(stubDir, 'AeroMC-Setup.exe');
|
||||
if (!fs.existsSync(stub)) throw new Error('Build des Setups fehlgeschlagen.');
|
||||
|
||||
schritt('App wird ans Setup angehängt …');
|
||||
const zipSize = fs.statSync(ZIP).size;
|
||||
const footer = Buffer.alloc(16);
|
||||
footer.writeBigInt64LE(BigInt(zipSize), 0);
|
||||
MAGIC.copy(footer, 8);
|
||||
fs.rmSync(OUT, { force: true });
|
||||
fs.copyFileSync(stub, OUT);
|
||||
// ZIP in Blöcken anhängen statt komplett in den Speicher zu laden
|
||||
const fd = fs.openSync(OUT, 'a');
|
||||
try {
|
||||
const src = fs.openSync(ZIP, 'r');
|
||||
try {
|
||||
const buf = Buffer.alloc(8 * 1024 * 1024);
|
||||
let n;
|
||||
while ((n = fs.readSync(src, buf, 0, buf.length, null)) > 0) fs.writeSync(fd, buf, 0, n);
|
||||
} finally { fs.closeSync(src); }
|
||||
fs.writeSync(fd, footer);
|
||||
} finally { fs.closeSync(fd); }
|
||||
|
||||
schritt('Aufräumen …');
|
||||
for (const t of [WORK, APP_DIR, path.join(DIST, 'builder-debug.yml'), path.join(DIST, 'builder-effective-config.yaml')]) {
|
||||
fs.rmSync(t, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const mb = (fs.statSync(OUT).size / 1024 / 1024).toFixed(1);
|
||||
console.log(`\nFertig: ${OUT} (${mb} MB)`);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (err) {
|
||||
console.error(`\nFehler: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
make-mc-textures.py – erzeugt die 16×16-Kacheln für das Minecraft-Design
|
||||
(src/assets/design/*.png). Eigene, zufällig erzeugte Muster im Block-Stil –
|
||||
keine Original-Texturen aus dem Spiel.
|
||||
|
||||
Aufruf: python scripts/make-mc-textures.py
|
||||
"""
|
||||
import os
|
||||
import random
|
||||
from PIL import Image
|
||||
|
||||
ZIEL = os.path.join(os.path.dirname(__file__), '..', 'src', 'assets', 'design')
|
||||
os.makedirs(ZIEL, exist_ok=True)
|
||||
|
||||
|
||||
def klemme(v):
|
||||
return max(0, min(255, int(v)))
|
||||
|
||||
|
||||
def rauschen(basis, streuung, seed, flecken=None):
|
||||
"""Grundfarbe mit Pixelrauschen; flecken = (anteil, farbe, streuung)."""
|
||||
rnd = random.Random(seed)
|
||||
img = Image.new('RGB', (16, 16))
|
||||
px = img.load()
|
||||
for y in range(16):
|
||||
for x in range(16):
|
||||
d = rnd.randint(-streuung, streuung)
|
||||
farbe = basis
|
||||
if flecken and rnd.random() < flecken[0]:
|
||||
farbe = flecken[1]
|
||||
d = rnd.randint(-flecken[2], flecken[2])
|
||||
px[x, y] = tuple(klemme(c + d) for c in farbe)
|
||||
return img
|
||||
|
||||
|
||||
def speichere(img, name):
|
||||
img.save(os.path.join(ZIEL, name))
|
||||
print(name)
|
||||
|
||||
|
||||
# Hintergrund der Instanzliste: dunkle Erde (wie hinter Menüs im Spiel)
|
||||
speichere(rauschen((44, 32, 23), 6, 11, (0.16, (31, 22, 15), 4)), 'erde-dunkel.png')
|
||||
|
||||
# Leisten (Werkzeugleiste, Seitenleiste, Statusleiste): dunkler Schiefer
|
||||
schiefer = rauschen((46, 46, 50), 6, 23, (0.12, (34, 34, 38), 3))
|
||||
px = schiefer.load()
|
||||
rnd = random.Random(5)
|
||||
for _ in range(5): # ein paar waagerechte Bruchkanten wie bei Tiefenschiefer
|
||||
y = rnd.randrange(16)
|
||||
x0 = rnd.randrange(16)
|
||||
for x in range(x0, min(16, x0 + rnd.randint(3, 7))):
|
||||
px[x, y] = (30, 30, 33)
|
||||
speichere(schiefer, 'schiefer.png')
|
||||
|
||||
# Dialoge: Stein
|
||||
stein = rauschen((78, 78, 80), 8, 37, (0.10, (60, 60, 62), 4))
|
||||
speichere(stein, 'stein.png')
|
||||
|
||||
# Holzbretter (z. B. Kopf der Dialoge): vier Bretter zu je 4 Pixeln
|
||||
bretter = Image.new('RGB', (16, 16))
|
||||
px = bretter.load()
|
||||
rnd = random.Random(41)
|
||||
farben = [(122, 92, 56), (114, 86, 52), (128, 97, 60), (118, 89, 54)]
|
||||
fugen = [3, 11, 7, 14]
|
||||
for brett in range(4):
|
||||
for y in range(brett * 4, brett * 4 + 4):
|
||||
for x in range(16):
|
||||
c = farben[brett]
|
||||
d = rnd.randint(-5, 5)
|
||||
if y == brett * 4 + 3: # dunkle Fuge unter jedem Brett
|
||||
c, d = (74, 55, 33), rnd.randint(-2, 2)
|
||||
elif x == fugen[brett]: # Stoßfuge
|
||||
c, d = (88, 66, 40), 0
|
||||
px[x, y] = tuple(klemme(v + d) for v in c)
|
||||
speichere(bretter, 'bretter.png')
|
||||
|
||||
# Grasnarbe: oberer Rand der Werkzeugleiste (16×4, grün mit "Tropfen")
|
||||
gras = Image.new('RGBA', (16, 4), (0, 0, 0, 0))
|
||||
px = gras.load()
|
||||
rnd = random.Random(53)
|
||||
tiefe = [2, 3, 2, 2, 4, 3, 2, 3, 2, 2, 3, 4, 2, 3, 2, 3]
|
||||
for x in range(16):
|
||||
for y in range(tiefe[x]):
|
||||
g = rnd.randint(-10, 10)
|
||||
px[x, y] = (klemme(84 + g), klemme(140 + g), klemme(52 + g // 2), 255)
|
||||
speichere(gras, 'gras.png')
|
||||
@@ -0,0 +1,270 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* make-pixelfont.js – erzeugt src/assets/aeropixel.ttf
|
||||
* -----------------------------------------------------
|
||||
* Eigene Pixel-Schrift für das Minecraft-Design (keine fremde Schrift, also
|
||||
* keine Lizenzfragen). Jede Glyphe ist ein Raster aus '#' (Pixel) und ' ':
|
||||
* Zeile 0 ist die Oberkante der Großbuchstaben, Zeile 6 sitzt auf der
|
||||
* Grundlinie, Zeilen 7–8 sind Unterlängen (g, j, p, q, y, Komma).
|
||||
* Fehlende Zeichen fallen im Launcher automatisch auf Segoe UI zurück.
|
||||
*
|
||||
* Aufruf: node scripts/make-pixelfont.js
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const P = 125; // ein Pixel in Font-Einheiten (8 Pixel = 1 em)
|
||||
const EM = 1000;
|
||||
const ASC = 1000; // 7 Zeilen Großbuchstaben + 1 Zeile Luft
|
||||
const DESC = 250; // 2 Zeilen Unterlänge
|
||||
|
||||
const G = {
|
||||
A: [' ### ', '# #', '# #', '#####', '# #', '# #', '# #'],
|
||||
B: ['#### ', '# #', '# #', '#### ', '# #', '# #', '#### '],
|
||||
C: [' ### ', '# #', '# ', '# ', '# ', '# #', ' ### '],
|
||||
D: ['#### ', '# #', '# #', '# #', '# #', '# #', '#### '],
|
||||
E: ['#####', '# ', '# ', '#### ', '# ', '# ', '#####'],
|
||||
F: ['#####', '# ', '# ', '#### ', '# ', '# ', '# '],
|
||||
G: [' ####', '# ', '# ', '# ##', '# #', '# #', ' ####'],
|
||||
H: ['# #', '# #', '# #', '#####', '# #', '# #', '# #'],
|
||||
I: ['###', ' # ', ' # ', ' # ', ' # ', ' # ', '###'],
|
||||
J: [' #', ' #', ' #', ' #', ' #', '# #', ' ### '],
|
||||
K: ['# #', '# # ', '# # ', '## ', '# # ', '# # ', '# #'],
|
||||
L: ['# ', '# ', '# ', '# ', '# ', '# ', '#####'],
|
||||
M: ['# #', '## ##', '# # #', '# #', '# #', '# #', '# #'],
|
||||
N: ['# #', '## #', '# # #', '# ##', '# #', '# #', '# #'],
|
||||
O: [' ### ', '# #', '# #', '# #', '# #', '# #', ' ### '],
|
||||
P: ['#### ', '# #', '# #', '#### ', '# ', '# ', '# '],
|
||||
Q: [' ### ', '# #', '# #', '# #', '# #', '# # ', ' ## #'],
|
||||
R: ['#### ', '# #', '# #', '#### ', '# # ', '# #', '# #'],
|
||||
S: [' ####', '# ', '# ', ' ### ', ' #', ' #', '#### '],
|
||||
T: ['#####', ' # ', ' # ', ' # ', ' # ', ' # ', ' # '],
|
||||
U: ['# #', '# #', '# #', '# #', '# #', '# #', ' ### '],
|
||||
V: ['# #', '# #', '# #', '# #', '# #', ' # # ', ' # '],
|
||||
W: ['# #', '# #', '# #', '# #', '# # #', '## ##', '# #'],
|
||||
X: ['# #', '# #', ' # # ', ' # ', ' # # ', '# #', '# #'],
|
||||
Y: ['# #', '# #', ' # # ', ' # ', ' # ', ' # ', ' # '],
|
||||
Z: ['#####', ' #', ' # ', ' # ', ' # ', '# ', '#####'],
|
||||
|
||||
a: [' ', ' ', ' ### ', ' #', ' ####', '# #', ' ####'],
|
||||
b: ['# ', '# ', '# ## ', '## #', '# #', '# #', '#### '],
|
||||
c: [' ', ' ', ' ### ', '# #', '# ', '# #', ' ### '],
|
||||
d: [' #', ' #', ' ## #', '# ##', '# #', '# #', ' ####'],
|
||||
e: [' ', ' ', ' ### ', '# #', '#####', '# ', ' ####'],
|
||||
f: [' ##', ' # ', '####', ' # ', ' # ', ' # ', ' # '],
|
||||
g: [' ', ' ', ' ####', '# #', '# #', '# #', ' ####', ' #', ' ### '],
|
||||
h: ['# ', '# ', '# ## ', '## #', '# #', '# #', '# #'],
|
||||
i: ['#', ' ', '#', '#', '#', '#', '#'],
|
||||
j: [' #', ' ', ' #', ' #', ' #', ' #', ' #', '# #', ' ## '],
|
||||
k: ['# ', '# ', '# #', '# # ', '## ', '# # ', '# #'],
|
||||
l: ['# ', '# ', '# ', '# ', '# ', '# ', ' #'],
|
||||
m: [' ', ' ', '## # ', '# # #', '# # #', '# #', '# #'],
|
||||
n: [' ', ' ', '#### ', '# #', '# #', '# #', '# #'],
|
||||
o: [' ', ' ', ' ### ', '# #', '# #', '# #', ' ### '],
|
||||
p: [' ', ' ', '# ## ', '## #', '# #', '# #', '#### ', '# ', '# '],
|
||||
q: [' ', ' ', ' ## #', '# ##', '# #', '# #', ' ####', ' #', ' #'],
|
||||
r: [' ', ' ', '# ## ', '## #', '# ', '# ', '# '],
|
||||
s: [' ', ' ', ' ####', '# ', ' ### ', ' #', '#### '],
|
||||
t: [' # ', ' # ', '###', ' # ', ' # ', ' # ', ' #'],
|
||||
u: [' ', ' ', '# #', '# #', '# #', '# #', ' ####'],
|
||||
v: [' ', ' ', '# #', '# #', '# #', ' # # ', ' # '],
|
||||
w: [' ', ' ', '# #', '# #', '# # #', '# # #', ' ####'],
|
||||
x: [' ', ' ', '# #', ' # # ', ' # ', ' # # ', '# #'],
|
||||
y: [' ', ' ', '# #', '# #', '# #', '# #', ' ####', ' #', '#### '],
|
||||
z: [' ', ' ', '#####', ' # ', ' # ', ' # ', '#####'],
|
||||
|
||||
0: [' ### ', '# #', '# ##', '# # #', '## #', '# #', ' ### '],
|
||||
1: [' # ', ' ## ', ' # ', ' # ', ' # ', ' # ', '#####'],
|
||||
2: [' ### ', '# #', ' #', ' ## ', ' # ', '# ', '#####'],
|
||||
3: [' ### ', '# #', ' #', ' ## ', ' #', '# #', ' ### '],
|
||||
4: [' ##', ' # #', ' # #', '# #', '#####', ' #', ' #'],
|
||||
5: ['#####', '# ', '#### ', ' #', ' #', '# #', ' ### '],
|
||||
6: [' ## ', ' # ', '# ', '#### ', '# #', '# #', ' ### '],
|
||||
7: ['#####', '# #', ' #', ' # ', ' # ', ' # ', ' # '],
|
||||
8: [' ### ', '# #', '# #', ' ### ', '# #', '# #', ' ### '],
|
||||
9: [' ### ', '# #', '# #', ' ####', ' #', ' # ', ' ## '],
|
||||
|
||||
'Ä': ['# #', ' ### ', '# #', '# #', '#####', '# #', '# #'],
|
||||
'Ö': ['# #', ' ### ', '# #', '# #', '# #', '# #', ' ### '],
|
||||
'Ü': ['# #', ' ', '# #', '# #', '# #', '# #', ' ### '],
|
||||
'ä': [' # # ', ' ', ' ### ', ' #', ' ####', '# #', ' ####'],
|
||||
'ö': [' # # ', ' ', ' ### ', '# #', '# #', '# #', ' ### '],
|
||||
'ü': [' # # ', ' ', '# #', '# #', '# #', '# #', ' ####'],
|
||||
'ß': [' ### ', '# #', '# # ', '# # ', '# # ', '# #', '# ## '],
|
||||
'é': [' # ', ' # ', ' ### ', '# #', '#####', '# ', ' ####'],
|
||||
|
||||
'.': [' ', ' ', ' ', ' ', ' ', ' ', '#'],
|
||||
',': [' ', ' ', ' ', ' ', ' ', ' ', '#', '#'],
|
||||
':': [' ', ' ', '#', ' ', ' ', ' ', '#'],
|
||||
';': [' ', ' ', '#', ' ', ' ', ' ', '#', '#'],
|
||||
'!': ['#', '#', '#', '#', '#', ' ', '#'],
|
||||
'?': [' ### ', '# #', ' #', ' # ', ' # ', ' ', ' # '],
|
||||
'-': [' ', ' ', ' ', '####', ' ', ' ', ' '],
|
||||
'–': [' ', ' ', ' ', '#####', ' ', ' ', ' '],
|
||||
'_': [' ', ' ', ' ', ' ', ' ', ' ', ' ', '#####'],
|
||||
'(': [' #', ' # ', '# ', '# ', '# ', ' # ', ' #'],
|
||||
')': ['# ', ' # ', ' #', ' #', ' #', ' # ', '# '],
|
||||
'[': ['##', '# ', '# ', '# ', '# ', '# ', '##'],
|
||||
']': ['##', ' #', ' #', ' #', ' #', ' #', '##'],
|
||||
'/': [' #', ' # ', ' # ', ' # ', ' # ', ' # ', '# '],
|
||||
'\\': ['# ', ' # ', ' # ', ' # ', ' # ', ' # ', ' #'],
|
||||
'+': [' ', ' # ', ' # ', '#####', ' # ', ' # ', ' '],
|
||||
'=': [' ', ' ', '#####', ' ', '#####', ' ', ' '],
|
||||
'*': [' ', ' ', '# # #', ' ### ', '# # #', ' ', ' '],
|
||||
'%': ['# #', ' #', ' # ', ' # ', ' # ', '# ', '# #'],
|
||||
'&': [' ## ', '# # ', ' ## ', ' ## #', '# # ', '# # ', ' ## #'],
|
||||
'#': [' # # ', '#####', ' # # ', ' # # ', ' # # ', '#####', ' # # '],
|
||||
'<': [' #', ' # ', ' # ', '# ', ' # ', ' # ', ' #'],
|
||||
'>': ['# ', ' # ', ' # ', ' #', ' # ', ' # ', '# '],
|
||||
"'": ['#', '#'],
|
||||
'"': ['# #', '# #'],
|
||||
'|': ['#', '#', '#', '#', '#', '#', '#', '#', '#'],
|
||||
'·': [' ', ' ', ' ', '#', ' ', ' ', ' '],
|
||||
'…': [' ', ' ', ' ', ' ', ' ', ' ', '# # #'],
|
||||
'@': [' ### ', '# #', '# ###', '# # #', '# ###', '# ', ' ####'],
|
||||
};
|
||||
|
||||
// ---------- Glyphen in Umrisse umwandeln (waagerechte Pixel-Läufe als Rechtecke) ----------
|
||||
function umrisse(zeilen) {
|
||||
const konturen = [];
|
||||
zeilen.forEach((zeile, r) => {
|
||||
const yOben = (7 - r) * P;
|
||||
const yUnten = yOben - P;
|
||||
let x = 0;
|
||||
while (x < zeile.length) {
|
||||
if (zeile[x] !== '#') { x++; continue; }
|
||||
let ende = x;
|
||||
while (ende < zeile.length && zeile[ende] === '#') ende++;
|
||||
// im Uhrzeigersinn (TrueType-Außenkontur)
|
||||
konturen.push([[x * P, yUnten], [x * P, yOben], [ende * P, yOben], [ende * P, yUnten]]);
|
||||
x = ende;
|
||||
}
|
||||
});
|
||||
return konturen;
|
||||
}
|
||||
|
||||
const glyphen = [{ name: '.notdef', code: null, breite: 4, konturen: [[[0, 0], [0, 875], [500, 875], [500, 0]], [[125, 125], [375, 125], [375, 750], [125, 750]]] },
|
||||
{ name: 'space', code: 32, breite: 3, konturen: [] }];
|
||||
for (const [zeichen, zeilen] of Object.entries(G)) {
|
||||
const breite = Math.max(...zeilen.map((z) => z.length));
|
||||
glyphen.push({ code: zeichen.codePointAt(0), breite, konturen: umrisse(zeilen) });
|
||||
}
|
||||
const sortiert = [glyphen[0], glyphen[1], ...glyphen.slice(2).sort((a, b) => a.code - b.code)];
|
||||
|
||||
// ---------- Hilfen zum Schreiben ----------
|
||||
class Puffer {
|
||||
constructor() { this.teile = []; this.laenge = 0; }
|
||||
u8(v) { const b = Buffer.alloc(1); b.writeUInt8(v); return this.roh(b); }
|
||||
u16(v) { const b = Buffer.alloc(2); b.writeUInt16BE(v & 0xffff); return this.roh(b); }
|
||||
i16(v) { const b = Buffer.alloc(2); b.writeInt16BE(v); return this.roh(b); }
|
||||
u32(v) { const b = Buffer.alloc(4); b.writeUInt32BE(v >>> 0); return this.roh(b); }
|
||||
i64(v) { const b = Buffer.alloc(8); b.writeBigInt64BE(BigInt(v)); return this.roh(b); }
|
||||
roh(b) { this.teile.push(b); this.laenge += b.length; return this; }
|
||||
fertig() { return Buffer.concat(this.teile); }
|
||||
}
|
||||
const auf4 = (b) => (b.length % 4 ? Buffer.concat([b, Buffer.alloc(4 - (b.length % 4))]) : b);
|
||||
function pruefsumme(b) {
|
||||
const p = auf4(b);
|
||||
let s = 0;
|
||||
for (let i = 0; i < p.length; i += 4) s = (s + p.readUInt32BE(i)) >>> 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
// ---------- glyf + loca + hmtx ----------
|
||||
const glyf = new Puffer();
|
||||
const loca = [];
|
||||
const hmtx = new Puffer();
|
||||
let maxPunkte = 0, maxKonturen = 0;
|
||||
let gxMin = 0, gyMin = 0, gxMax = 0, gyMax = 0;
|
||||
let maxVorschub = 0, minLsb = 1e9, minRsb = 1e9, maxXAusdehnung = 0;
|
||||
for (const g of sortiert) {
|
||||
loca.push(glyf.laenge);
|
||||
const vorschub = (g.breite + 1) * P;
|
||||
maxVorschub = Math.max(maxVorschub, vorschub);
|
||||
if (!g.konturen.length) { hmtx.u16(vorschub).i16(0); continue; }
|
||||
const punkte = g.konturen.flat();
|
||||
const xs = punkte.map((p) => p[0]), ys = punkte.map((p) => p[1]);
|
||||
const xMin = Math.min(...xs), xMax = Math.max(...xs), yMin = Math.min(...ys), yMax = Math.max(...ys);
|
||||
gxMin = Math.min(gxMin, xMin); gyMin = Math.min(gyMin, yMin); gxMax = Math.max(gxMax, xMax); gyMax = Math.max(gyMax, yMax);
|
||||
minLsb = Math.min(minLsb, xMin); minRsb = Math.min(minRsb, vorschub - xMax); maxXAusdehnung = Math.max(maxXAusdehnung, xMax);
|
||||
maxPunkte = Math.max(maxPunkte, punkte.length); maxKonturen = Math.max(maxKonturen, g.konturen.length);
|
||||
const b = new Puffer();
|
||||
b.i16(g.konturen.length).i16(xMin).i16(yMin).i16(xMax).i16(yMax);
|
||||
let ende = -1;
|
||||
for (const k of g.konturen) { ende += k.length; b.u16(ende); }
|
||||
b.u16(0); // keine Hinting-Anweisungen
|
||||
for (let i = 0; i < punkte.length; i++) b.u8(0x01); // alle Punkte auf der Kurve, Koordinaten als int16
|
||||
let px = 0, py = 0;
|
||||
for (const [x] of punkte) { b.i16(x - px); px = x; }
|
||||
for (const [, y] of punkte) { b.i16(y - py); py = y; }
|
||||
glyf.roh(auf4(b.fertig()));
|
||||
hmtx.u16(vorschub).i16(xMin);
|
||||
}
|
||||
loca.push(glyf.laenge);
|
||||
const anzahl = sortiert.length;
|
||||
|
||||
const locaB = new Puffer();
|
||||
for (const o of loca) locaB.u32(o);
|
||||
|
||||
// ---------- cmap (Format 4, ein Segment je Zeichen) ----------
|
||||
const abbildung = sortiert.map((g, i) => [g.code, i]).filter(([c]) => c !== null).sort((a, b) => a[0] - b[0]);
|
||||
const segmente = abbildung.map(([c, i]) => ({ start: c, ende: c, delta: (i - c) & 0xffff }));
|
||||
segmente.push({ start: 0xffff, ende: 0xffff, delta: 1 });
|
||||
const segX2 = segmente.length * 2;
|
||||
const suchbereich = 2 * 2 ** Math.floor(Math.log2(segmente.length));
|
||||
const sub = new Puffer();
|
||||
sub.u16(4).u16(16 + segmente.length * 8).u16(0).u16(segX2).u16(suchbereich)
|
||||
.u16(Math.log2(suchbereich / 2)).u16(segX2 - suchbereich);
|
||||
for (const s of segmente) sub.u16(s.ende);
|
||||
sub.u16(0);
|
||||
for (const s of segmente) sub.u16(s.start);
|
||||
for (const s of segmente) sub.u16(s.delta);
|
||||
for (const s of segmente) sub.u16(0);
|
||||
const cmap = new Puffer().u16(0).u16(1).u16(3).u16(1).u32(12).roh(sub.fertig());
|
||||
|
||||
// ---------- head / hhea / maxp / OS/2 / name / post ----------
|
||||
const zeit = Math.floor(Date.UTC(2026, 9, 3) / 1000) + 2082844800; // Sekunden seit 1904
|
||||
const head = new Puffer().u32(0x00010000).u32(0x00010000).u32(0).u32(0x5f0f3cf5).u16(0x000b).u16(EM)
|
||||
.i64(zeit).i64(zeit).i16(gxMin).i16(gyMin).i16(gxMax).i16(gyMax).u16(0).u16(8).i16(2).i16(1).i16(0);
|
||||
const hhea = new Puffer().u32(0x00010000).i16(ASC).i16(-DESC).i16(0).u16(maxVorschub).i16(minLsb).i16(minRsb)
|
||||
.i16(maxXAusdehnung).i16(1).i16(0).i16(0).i16(0).i16(0).i16(0).i16(0).i16(0).u16(anzahl);
|
||||
const maxp = new Puffer().u32(0x00010000).u16(anzahl).u16(maxPunkte).u16(maxKonturen).u16(0).u16(0)
|
||||
.u16(2).u16(0).u16(0).u16(0).u16(0).u16(0).u16(0).u16(0).u16(0);
|
||||
const codes = abbildung.map(([c]) => c);
|
||||
const os2 = new Puffer().u16(4).i16(5 * P).u16(400).u16(5).u16(0)
|
||||
.i16(650).i16(600).i16(0).i16(75).i16(650).i16(600).i16(0).i16(350).i16(P).i16(3 * P)
|
||||
.i16(0).roh(Buffer.alloc(10)).u32(3).u32(0).u32(0).u32(0).roh(Buffer.from('AERO'))
|
||||
.u16(0x40).u16(Math.min(...codes)).u16(Math.min(0xffff, Math.max(...codes)))
|
||||
.i16(ASC).i16(-DESC).i16(0).u16(ASC).u16(DESC).u32(1).u32(0).i16(5 * P).i16(7 * P).u16(0).u16(32).u16(1);
|
||||
const namen = { 1: 'AeroPixel', 2: 'Regular', 3: 'AeroPixel Regular', 4: 'AeroPixel', 5: 'Version 1.0', 6: 'AeroPixel-Regular' };
|
||||
const nameDaten = Object.entries(namen).map(([id, text]) => [Number(id), Buffer.from(text, 'utf16le').swap16()]);
|
||||
const name = new Puffer().u16(0).u16(nameDaten.length).u16(6 + nameDaten.length * 12);
|
||||
let versatz = 0;
|
||||
for (const [id, b] of nameDaten) { name.u16(3).u16(1).u16(0x409).u16(id).u16(b.length).u16(versatz); versatz += b.length; }
|
||||
for (const [, b] of nameDaten) name.roh(b);
|
||||
const post = new Puffer().u32(0x00030000).u32(0).i16(-P).i16(P).u32(0).u32(0).u32(0).u32(0).u32(0);
|
||||
|
||||
// ---------- zusammensetzen ----------
|
||||
const tabellen = [
|
||||
['OS/2', os2], ['cmap', cmap], ['glyf', glyf], ['head', head], ['hhea', hhea],
|
||||
['hmtx', hmtx], ['loca', locaB], ['maxp', maxp], ['name', name], ['post', post],
|
||||
].map(([tag, p]) => [tag, p.fertig()]);
|
||||
const n = tabellen.length;
|
||||
const sb = 16 * 2 ** Math.floor(Math.log2(n));
|
||||
const kopf = new Puffer().u32(0x00010000).u16(n).u16(sb).u16(Math.log2(sb / 16)).u16(n * 16 - sb);
|
||||
let offset = 12 + n * 16;
|
||||
const verzeichnis = new Puffer();
|
||||
for (const [tag, daten] of tabellen) {
|
||||
verzeichnis.roh(Buffer.from(tag, 'latin1')).u32(pruefsumme(daten)).u32(offset).u32(daten.length);
|
||||
offset += auf4(daten).length;
|
||||
}
|
||||
let schrift = Buffer.concat([kopf.fertig(), verzeichnis.fertig(), ...tabellen.map(([, d]) => auf4(d))]);
|
||||
// checkSumAdjustment im head-Eintrag setzen
|
||||
const headOffset = 12 + n * 16 + tabellen.slice(0, 3).reduce((s, [, d]) => s + auf4(d).length, 0);
|
||||
schrift.writeUInt32BE((0xb1b0afba - pruefsumme(schrift)) >>> 0, headOffset + 8);
|
||||
|
||||
const ziel = path.join(__dirname, '..', 'src', 'assets', 'aeropixel.ttf');
|
||||
fs.writeFileSync(ziel, schrift);
|
||||
console.log(`${ziel}: ${anzahl} Glyphen, ${schrift.length} Bytes`);
|
||||
@@ -0,0 +1,712 @@
|
||||
"""
|
||||
make-spieldesign.py – erzeugt die Grafiken für den AeroMC-Look im Spiel
|
||||
=======================================================================
|
||||
Ausgabe nach src/assets/spieldesign/:
|
||||
|
||||
titel_alt.png / titel_neu.png Titel-Logo (textures/gui/title/minecraft.png)
|
||||
alt = bis 1.19.4 (zweigeteilt im 256×256-Atlas),
|
||||
neu = ab 1.20 (ein Stück, 256×64)
|
||||
edition_alt.png / edition_neu.png Untertitel "DEIN MINECRAFT ERLEBNIS" (edition.png)
|
||||
ladelogo.png Ladebildschirm ab 1.16 als Vollbild (der Agent zieht
|
||||
die Logo-Fläche auf Bildschirmgröße); im Format von
|
||||
mojangstudios.png: linke Bildhälfte oben, rechte unten
|
||||
ladelogo_alt.png Logo im Ladebildschirm 1.14/1.15 (ersetzt mojang.png)
|
||||
panorama_0..5.png Hintergrund des Hauptmenüs (Würfelseiten)
|
||||
pack.png, icon_<n>.rgba Paket- und Fenstersymbol
|
||||
|
||||
Eigene Bilder (optional) in scripts/spieldesign-quelle/:
|
||||
hintergrund.png|jpg Ladebildschirm-Hintergrund (am besten 16:9, z. B. 1920×1080)
|
||||
panorama.png|jpg Rundum-Panorama (360° × 180°, equirektangular, Seitenverhältnis 2:1)
|
||||
Fehlen sie, wird eine stilisierte Block-Landschaft in AeroMC-Blau gezeichnet.
|
||||
|
||||
Alle Grafiken sind eigene Entwürfe – kein Mojang-Material.
|
||||
Aufruf: python scripts/make-spieldesign.py
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import random
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageChops, ImageDraw, ImageFilter, ImageFont
|
||||
|
||||
WURZEL = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')
|
||||
ZIEL = os.path.join(WURZEL, 'src', 'assets', 'spieldesign')
|
||||
QUELLE = os.path.join(WURZEL, 'scripts', 'spieldesign-quelle')
|
||||
LOGO = os.path.join(WURZEL, 'src', 'assets', 'logo.png')
|
||||
SCHRIFT = 'C:/Windows/Fonts/seguisb.ttf'
|
||||
NACHTBLAU = (10, 14, 26) # Hintergrund des Ladebildschirms (muss zu Design.HINTERGRUND passen)
|
||||
KONTUR = (7, 14, 36)
|
||||
|
||||
# ---------- Block-Buchstaben (7 Zeilen hoch) ----------
|
||||
GLYPHEN = {
|
||||
'A': ['.####.', '##..##', '##..##', '######', '##..##', '##..##', '##..##'],
|
||||
'E': ['######', '##....', '##....', '#####.', '##....', '##....', '######'],
|
||||
'R': ['#####.', '##..##', '##..##', '#####.', '##.##.', '##..##', '##..##'],
|
||||
'O': ['.####.', '##..##', '##..##', '##..##', '##..##', '##..##', '.####.'],
|
||||
'M': ['##...##', '###.###', '#######', '##.#.##', '##...##', '##...##', '##...##'],
|
||||
'C': ['.#####', '##....', '##....', '##....', '##....', '##....', '.#####'],
|
||||
}
|
||||
# kleine Pixelschrift für den Untertitel (5 Zeilen hoch)
|
||||
KLEIN = {
|
||||
'D': ['###.', '#..#', '#..#', '#..#', '###.'], 'E': ['###', '#..', '##.', '#..', '###'],
|
||||
'I': ['#', '#', '#', '#', '#'], 'N': ['#..#', '##.#', '#.##', '#..#', '#..#'],
|
||||
'M': ['#...#', '##.##', '#.#.#', '#...#', '#...#'], 'C': ['.##', '#..', '#..', '#..', '.##'],
|
||||
'R': ['##.', '#.#', '##.', '#.#', '#.#'], 'A': ['.#.', '#.#', '###', '#.#', '#.#'],
|
||||
'F': ['###', '#..', '##.', '#..', '#..'], 'T': ['###', '.#.', '.#.', '.#.', '.#.'],
|
||||
'L': ['#..', '#..', '#..', '#..', '###'], 'B': ['##.', '#.#', '##.', '#.#', '##.'],
|
||||
'S': ['.##', '#..', '.#.', '..#', '##.'],
|
||||
}
|
||||
|
||||
|
||||
def raster(text, glyphen, abstand=1, wortabstand=3):
|
||||
punkte, x = [], 0
|
||||
for i, z in enumerate(text):
|
||||
if z == ' ':
|
||||
x += wortabstand
|
||||
continue
|
||||
g = glyphen[z]
|
||||
for y, zeile in enumerate(g):
|
||||
for dx, c in enumerate(zeile):
|
||||
if c == '#':
|
||||
punkte.append((x + dx, y, i))
|
||||
x += len(g[0]) + abstand
|
||||
return punkte, x - abstand, len(next(iter(glyphen.values())))
|
||||
|
||||
|
||||
def verlauf(h, w, oben, unten):
|
||||
t = np.linspace(0, 1, h)[:, None, None]
|
||||
return np.broadcast_to(np.array(oben, float) * (1 - t) + np.array(unten, float) * t, (h, w, 3)).copy()
|
||||
|
||||
|
||||
def rgba(arr, maske):
|
||||
a = np.dstack([np.clip(arr, 0, 255), maske]).astype(np.uint8)
|
||||
return Image.fromarray(a, 'RGBA')
|
||||
|
||||
|
||||
# ---------- Schriftzug ----------
|
||||
|
||||
def schriftzug(b, flach=False):
|
||||
"""'AERO MC' aus Blöcken der Größe b (Pixel): Vorderseite, Tiefe nach unten, Kontur."""
|
||||
punkte, breite, hoehe = raster('AERO MC', GLYPHEN, 1, 2)
|
||||
tiefe = 0 if flach else max(2, int(b * 0.55))
|
||||
k = 0 if flach else max(2, b // 5)
|
||||
W, H = breite * b + 2 * k, hoehe * b + tiefe + 2 * k
|
||||
front = np.zeros((H, W), bool)
|
||||
blau = np.zeros((H, W), bool)
|
||||
kante = np.zeros((H, W), float)
|
||||
belegt = {(x, y) for x, y, _ in punkte}
|
||||
rnd = np.random.RandomState(7)
|
||||
rauschen = np.zeros((H, W))
|
||||
zell = max(1, b // 4)
|
||||
for bx, by, i in punkte:
|
||||
x0, y0 = k + bx * b, k + by * b
|
||||
front[y0:y0 + b, x0:x0 + b] = True
|
||||
if i >= 5:
|
||||
blau[y0:y0 + b, x0:x0 + b] = True
|
||||
# Stein-Struktur in kleinen Zellen
|
||||
for yy in range(0, b, zell):
|
||||
for xx in range(0, b, zell):
|
||||
rauschen[y0 + yy:y0 + yy + zell, x0 + xx:x0 + xx + zell] = rnd.randint(-9, 6)
|
||||
if (bx, by - 1) not in belegt:
|
||||
kante[y0:y0 + max(1, b // 6), x0:x0 + b] = 34 # helle Oberkante
|
||||
if (bx + 1, by) not in belegt:
|
||||
kante[y0:y0 + b, x0 + b - max(1, b // 7):x0 + b] -= 26 # dunklere rechte Kante
|
||||
hell = verlauf(H, W, (255, 255, 255), (176, 190, 214))
|
||||
mc = verlauf(H, W, (170, 236, 255), (34, 108, 232))
|
||||
farbe = np.where(blau[..., None], mc, hell) + (rauschen + kante)[..., None]
|
||||
if flach:
|
||||
return rgba(farbe, front * 255)
|
||||
seite = np.zeros((H, W), bool)
|
||||
for t in range(1, tiefe + 1):
|
||||
seite[t:, :] |= front[:-t, :]
|
||||
seite &= ~front
|
||||
blau_seite = np.zeros((H, W), bool)
|
||||
for t in range(1, tiefe + 1):
|
||||
blau_seite[t:, :] |= blau[:-t, :]
|
||||
seite_farbe = np.where(blau_seite[..., None], verlauf(H, W, (30, 84, 190), (14, 44, 120)),
|
||||
verlauf(H, W, (112, 126, 156), (66, 78, 104)))
|
||||
alles = Image.fromarray(((front | seite) * 255).astype(np.uint8))
|
||||
kontur = np.array(alles.filter(ImageFilter.MaxFilter(2 * k + 1))) > 0
|
||||
bild = np.zeros((H, W, 3))
|
||||
bild[kontur] = KONTUR
|
||||
bild[seite] = seite_farbe[seite]
|
||||
bild[front] = farbe[front]
|
||||
return rgba(bild, kontur * 255)
|
||||
|
||||
|
||||
def pyramide(w, h):
|
||||
"""Blaue Pyramide (linke Seite hell, rechte dunkel, leuchtender Grat)."""
|
||||
w, h = int(w), int(h)
|
||||
links = Image.new('L', (w, h), 0)
|
||||
rechts = Image.new('L', (w, h), 0)
|
||||
spitze, mitte = (w * 0.5, 0), (w * 0.56, h)
|
||||
ImageDraw.Draw(links).polygon([spitze, (0, h), mitte], fill=255)
|
||||
ImageDraw.Draw(rechts).polygon([spitze, mitte, (w, h)], fill=255)
|
||||
hell = verlauf(h, w, (150, 232, 255), (40, 126, 236))
|
||||
dunkel = verlauf(h, w, (52, 130, 236), (14, 48, 140))
|
||||
l, r = np.array(links) > 0, np.array(rechts) > 0
|
||||
bild = np.zeros((h, w, 3))
|
||||
bild[l] = hell[l]
|
||||
bild[r] = dunkel[r]
|
||||
img = rgba(bild, np.maximum(np.array(links), np.array(rechts)))
|
||||
d = ImageDraw.Draw(img)
|
||||
d.line([spitze, mitte], fill=(215, 248, 255, 255), width=max(2, w // 70))
|
||||
d.line([spitze, (0, h)], fill=(120, 215, 255, 255), width=max(1, w // 140))
|
||||
return img
|
||||
|
||||
|
||||
def leuchten(bild, radius, farbe=(70, 190, 255), staerke=0.75):
|
||||
a = bild.split()[3].filter(ImageFilter.MaxFilter(3)).filter(ImageFilter.GaussianBlur(radius))
|
||||
a = a.point(lambda v: int(v * staerke))
|
||||
g = Image.new('RGBA', bild.size, farbe + (0,))
|
||||
g.putalpha(a)
|
||||
return g
|
||||
|
||||
|
||||
def logo(b, mit_leuchten=True):
|
||||
"""Pyramide hinter dem Schriftzug, wie im Entwurf. Rückgabe: Bild, Unterkante des Schriftzugs."""
|
||||
text = schriftzug(b)
|
||||
pw, ph = text.width * 0.46, text.height * 1.55
|
||||
pyr = pyramide(pw, ph)
|
||||
oben = int(ph - text.height * 0.62) # so weit ragt die Pyramide über den Text
|
||||
rand = int(b * 1.2) if mit_leuchten else 0
|
||||
W, H = text.width + 2 * rand, oben + text.height + 2 * rand
|
||||
bild = Image.new('RGBA', (W, H), (0, 0, 0, 0))
|
||||
px = rand + (text.width - pyr.width) // 2 + int(text.width * 0.03)
|
||||
schicht = Image.new('RGBA', (W, H), (0, 0, 0, 0))
|
||||
schicht.alpha_composite(pyr, (px, rand))
|
||||
schicht.alpha_composite(text, (rand, rand + oben))
|
||||
if mit_leuchten:
|
||||
bild.alpha_composite(leuchten(schicht, b * 0.9))
|
||||
bild.alpha_composite(schicht)
|
||||
return bild, rand + oben + text.height
|
||||
|
||||
|
||||
def einpassen(bild, w, h):
|
||||
f = min(w / bild.width, h / bild.height, 1.0)
|
||||
return bild.resize((max(1, int(bild.width * f)), max(1, int(bild.height * f))), Image.LANCZOS) if f < 1 else bild
|
||||
|
||||
|
||||
# ---------- Titel-Texturen ----------
|
||||
|
||||
def titel_art():
|
||||
"""Pyramide, Schriftzug und Untertitel zusammen (Titel im Hauptmenü)."""
|
||||
art, unterkante = logo(15)
|
||||
unter, _ = untertitel_bild(5)
|
||||
bild = Image.new('RGBA', (max(art.width, unter.width), unterkante + 8 + unter.height), (0, 0, 0, 0))
|
||||
bild.alpha_composite(art, ((bild.width - art.width) // 2, 0))
|
||||
bild.alpha_composite(unter, ((bild.width - unter.width) // 2, unterkante + 8))
|
||||
return bild
|
||||
|
||||
|
||||
def titel(alt):
|
||||
"""
|
||||
Der Agent erweitert den gezeichneten Streifen auf 64 Punkte Höhe (sonst 44).
|
||||
neu (ab 1.20): 256×64-Textur, ganz genutzt.
|
||||
alt (bis 1.19.4): Stücke (0,0,155,64) und (0,65,155,64) im 256×256-Atlas,
|
||||
sichtbare Mitte bei x=137 des 310 breiten Streifens.
|
||||
"""
|
||||
art = einpassen(titel_art(), (274 if alt else 256) * 4 - 8, 64 * 4)
|
||||
if not alt:
|
||||
t = Image.new('RGBA', (1024, 256), (0, 0, 0, 0))
|
||||
t.alpha_composite(art, ((1024 - art.width) // 2, 256 - art.height))
|
||||
return t
|
||||
streifen = Image.new('RGBA', (310 * 4, 64 * 4), (0, 0, 0, 0))
|
||||
streifen.alpha_composite(art, (137 * 4 - art.width // 2, 256 - art.height))
|
||||
t = Image.new('RGBA', (1024, 1024), (0, 0, 0, 0))
|
||||
t.alpha_composite(streifen.crop((0, 0, 620, 256)), (0, 0))
|
||||
t.alpha_composite(streifen.crop((620, 0, 1240, 256)), (0, 65 * 4))
|
||||
return t
|
||||
|
||||
|
||||
def untertitel_bild(skala, farbe=(236, 244, 255), schatten=(8, 16, 40)):
|
||||
punkte, breite, hoehe = raster('DEIN MINECRAFT ERLEBNIS', KLEIN, 1, 3)
|
||||
img = Image.new('RGBA', ((breite + 1) * skala, (hoehe + 1) * skala), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(img)
|
||||
for dx, dy, f in ((1, 1, schatten), (0, 0, farbe)):
|
||||
for x, y, _ in punkte:
|
||||
d.rectangle(((x + dx) * skala, (y + dy) * skala, (x + dx + 1) * skala - 1, (y + dy + 1) * skala - 1), fill=f + (255,))
|
||||
return img, breite
|
||||
|
||||
|
||||
def edition(breite_max):
|
||||
img, breite = untertitel_bild(4)
|
||||
if breite + 1 > breite_max:
|
||||
raise SystemExit(f'Untertitel zu breit: {breite} > {breite_max}')
|
||||
t = Image.new('RGBA', (512, 64), (0, 0, 0, 0))
|
||||
t.alpha_composite(img, ((breite_max * 4 - img.width) // 2, (56 - img.height) // 2))
|
||||
return t
|
||||
|
||||
|
||||
# ---------- stilisierte Block-Landschaft (Abendstimmung am Bergsee) ----------
|
||||
# Eine Szene, zwei Ansichten: als Rundum-Panorama (Menü) und als flacher
|
||||
# Ausschnitt nach vorne (Ladebildschirm). Gleicher Zufallswert -> gleiche Berge.
|
||||
|
||||
def _mix(a, b, t):
|
||||
return np.array(a, np.float32) * (1 - t) + np.array(b, np.float32) * t
|
||||
|
||||
|
||||
def _profil(phi, seed, schritt_rad):
|
||||
"""periodisches Höhenprofil 0..1, blockig gestuft"""
|
||||
r = random.Random(seed)
|
||||
stufe = np.floor(phi / schritt_rad) * schritt_rad + schritt_rad / 2
|
||||
werte = np.zeros_like(phi)
|
||||
for k in range(1, 16):
|
||||
werte += r.uniform(0.25, 1.0) / k ** 1.05 * np.sin(k * stufe + r.uniform(0, 6.3))
|
||||
return (werte + 2.2) / 4.4
|
||||
|
||||
|
||||
def szene(W, H, horizont, start, spanne, seed=2026):
|
||||
rnd = random.Random(seed)
|
||||
ppd = W / math.degrees(spanne) # Pixel je Grad
|
||||
blk = max(4, int(round(ppd * 0.9))) # Blockgröße der Silhouetten
|
||||
schritt = math.radians(blk / ppd) # ein Block als Winkel
|
||||
phi = (start + np.arange(W, dtype=np.float32) / W * spanne).astype(np.float32)
|
||||
y = np.arange(H, dtype=np.float32)[:, None]
|
||||
grad = (horizont - y) / ppd # Höhe über dem Horizont in Grad
|
||||
|
||||
sonne = math.pi + 0.66
|
||||
dphi = np.angle(np.exp(1j * (phi - sonne))).astype(np.float32)[None, :]
|
||||
|
||||
# Himmel: Blau oben, hell am Horizont, warmer Abendschein um die Sonne
|
||||
t = np.clip(grad / 75, 0, 1)[..., None]
|
||||
himmel = np.where(t < 0.3, _mix((168, 202, 238), (66, 124, 210), t / 0.3), _mix((66, 124, 210), (16, 36, 96), (t - 0.3) / 0.7))
|
||||
warm = (np.exp(-dphi ** 2 / 0.55) * np.exp(-(np.clip(grad, 0, None) / 14) ** 2))[..., None]
|
||||
himmel = himmel * (1 - 0.8 * warm) + np.array([255, 178, 120], np.float32) * 0.8 * warm
|
||||
bild = np.broadcast_to(himmel, (H, W, 3)).astype(np.float32).copy()
|
||||
|
||||
def maske_hoehe(h_grad):
|
||||
return (grad >= 0) & (grad <= h_grad[None, :])
|
||||
|
||||
def stufen(h_grad):
|
||||
return np.floor(h_grad * ppd / blk) * blk / ppd
|
||||
|
||||
# Sonne (eckig, wie im Spiel) mit Schein
|
||||
s_x = (sonne - start) / spanne * W
|
||||
s_y = horizont - 12 * ppd
|
||||
s_r = 2.6 * ppd
|
||||
rr = np.sqrt(((np.arange(W) - s_x) / ppd)[None, :] ** 2 + ((y - s_y) / ppd) ** 2)
|
||||
bild += (np.exp(-(rr / 6) ** 2) * 0.55)[..., None] * np.array([255, 210, 150], np.float32)
|
||||
quadrat = (np.abs(np.arange(W) - s_x)[None, :] < s_r) & (np.abs(y - s_y) < s_r)
|
||||
bild[quadrat] = (255, 244, 205)
|
||||
|
||||
# Wolken (flach und blockig)
|
||||
wolken = Image.new('RGBA', (W, H), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(wolken)
|
||||
for _ in range(18):
|
||||
p = rnd.uniform(0, 2 * math.pi)
|
||||
for versatz in (-2 * math.pi, 0, 2 * math.pi):
|
||||
cx = (p + versatz - start) / spanne * W
|
||||
if -W * 0.2 < cx < W * 1.2:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
cy = horizont - rnd.uniform(20, 42) * ppd
|
||||
s = blk
|
||||
nah_sonne = abs(math.atan2(math.sin(p - sonne), math.cos(p - sonne))) < 0.7
|
||||
farbe = (255, 222, 196) if nah_sonne else (232, 240, 252)
|
||||
for _ in range(rnd.randint(2, 4)):
|
||||
w, h = rnd.randint(6, 16) * s, rnd.randint(1, 2) * s
|
||||
ox, oy = rnd.randint(-4, 4) * s, rnd.randint(-1, 0) * s
|
||||
d.rectangle((cx + ox, cy + oy, cx + ox + w, cy + oy + h), fill=farbe + (170,))
|
||||
d.rectangle((cx + ox, cy + oy + h - max(2, s // 3), cx + ox + w, cy + oy + h), fill=tuple(int(c * 0.82) for c in farbe) + (170,))
|
||||
|
||||
# ferne Berge mit Schnee
|
||||
h_fern = stufen(6 + 22 * _profil(phi, seed + 1, schritt))
|
||||
m = maske_hoehe(h_fern)
|
||||
tb = np.clip(grad / 28, 0, 1)[..., None]
|
||||
fels = _mix((46, 66, 112), (96, 118, 168), tb)
|
||||
schnee = (grad > h_fern[None, :] - 3 * blk / ppd) & (h_fern[None, :] > 15)
|
||||
fels = np.where(schnee[..., None], np.array([236, 242, 255], np.float32), fels)
|
||||
sonnenseite = (np.exp(-dphi ** 2 / 0.6))[..., None] * 0.35
|
||||
fels = fels * (1 - sonnenseite) + np.array([255, 190, 150], np.float32) * sonnenseite
|
||||
bild[m] = np.broadcast_to(fels, bild.shape)[m]
|
||||
# bewaldete Hügel
|
||||
h_huegel = stufen(2 + 7 * _profil(phi, seed + 2, schritt))
|
||||
m = maske_hoehe(h_huegel)
|
||||
huegel = _mix((26, 70, 58), (44, 98, 74), np.clip(grad / 7, 0, 1)[..., None])
|
||||
bild[m] = np.broadcast_to(huegel, bild.shape)[m]
|
||||
|
||||
img = Image.fromarray(np.clip(bild, 0, 255).astype(np.uint8), 'RGB').convert('RGBA')
|
||||
img.alpha_composite(wolken)
|
||||
# Fichten am Ufer
|
||||
wald = Image.new('RGBA', (W, H), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(wald)
|
||||
s = max(3, blk // 2)
|
||||
for _ in range(int(W / s * 0.55)):
|
||||
x = rnd.randrange(W)
|
||||
hoehe = rnd.randint(6, 13)
|
||||
dunkel = rnd.choice(((14, 44, 34), (18, 52, 40), (12, 38, 32)))
|
||||
d.rectangle((x - s // 2, horizont - 2 * s, x + s // 2, horizont), fill=(40, 28, 20, 255))
|
||||
for stufe in range(hoehe):
|
||||
breite = max(0, (hoehe - stufe) // 2 - (stufe % 2))
|
||||
y0 = horizont - (stufe + 2) * s
|
||||
d.rectangle((x - breite * s - s // 2, y0, x + breite * s + s // 2, y0 + s), fill=dunkel + (255,))
|
||||
d.rectangle((x + breite * s - s // 2, y0, x + breite * s + s // 2, y0 + s), fill=tuple(c + 14 for c in dunkel) + (255,))
|
||||
img.alpha_composite(wald)
|
||||
bild = np.array(img.convert('RGB'), np.float32)
|
||||
|
||||
# See: Spiegelung, nach unten dunkler, Wellen und Sonnenglitzern
|
||||
unten = H - horizont
|
||||
zeilen = np.clip(horizont - 1 - np.arange(unten), 0, H - 1)
|
||||
spiegel = bild[zeilen]
|
||||
tiefe = np.linspace(0, 1, unten, dtype=np.float32)[:, None, None]
|
||||
wellen = ((np.sin(np.arange(unten, dtype=np.float32)[:, None] * (0.55 / max(1, ppd / 10)) + np.sin(phi[None, :] * 37) * 1.6) > 0.86) * 14)[..., None]
|
||||
see = spiegel * (0.62 - 0.32 * tiefe) + np.array([10, 30, 62], np.float32) * (0.45 + 0.55 * tiefe) + wellen * np.array([0.5, 0.8, 1.0], np.float32)
|
||||
glitzer = (np.exp(-(dphi[0] / 0.07) ** 2)[None, :] * (np.sin(np.arange(unten)[:, None] * 1.7 + phi[None, :] * 900) > 0.6) * np.exp(-tiefe[..., 0] * 3))[..., None]
|
||||
see += glitzer * np.array([255, 200, 140], np.float32) * 0.8
|
||||
bild[horizont:] = see
|
||||
|
||||
# leuchtende Pixel (AeroMC)
|
||||
img = Image.fromarray(np.clip(bild, 0, 255).astype(np.uint8), 'RGB').convert('RGBA')
|
||||
pixel = Image.new('RGBA', (W, H), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(pixel)
|
||||
for _ in range(int(W / 60)):
|
||||
x = rnd.randrange(W)
|
||||
yy = horizont - rnd.uniform(-8, 30) * ppd
|
||||
g = rnd.choice((0.6, 0.8, 1.0, 1.3)) * ppd
|
||||
d.rectangle((x, yy, x + g, yy + g), fill=(90, 210, 255, rnd.randint(150, 235)))
|
||||
img.alpha_composite(leuchten(pixel, max(4, ppd * 0.8), (60, 170, 255), 1.0))
|
||||
img.alpha_composite(pixel)
|
||||
return img.convert('RGB')
|
||||
|
||||
|
||||
def landschaft(W=4096, H=2048):
|
||||
"""Rundum-Panorama (equirektangular): Spalte W/2 = Blick nach vorne."""
|
||||
return szene(W, H, H // 2, 0.0, 2 * math.pi)
|
||||
|
||||
|
||||
def landschaft_flach(W=1920, H=1080):
|
||||
"""Ausschnitt nach vorne (ca. 110°) für den Ladebildschirm."""
|
||||
spanne = math.radians(110)
|
||||
return szene(W, H, int(H * 0.6), math.pi - spanne / 2, spanne)
|
||||
|
||||
|
||||
def wuerfelseiten(equi, groesse=1024):
|
||||
"""Equirektangulares Panorama -> 6 Würfelseiten (0 vorne, 1 rechts, 2 hinten, 3 links, 4 oben, 5 unten)."""
|
||||
q = np.array(equi.convert('RGB'), float)
|
||||
H, W = q.shape[:2]
|
||||
a = (np.arange(groesse) + 0.5) / groesse * 2 - 1
|
||||
u, v = np.meshgrid(a, a)
|
||||
eins = np.ones_like(u)
|
||||
richtungen = [(u, -v, eins), (eins, -v, -u), (-u, -v, -eins), (-eins, -v, u), (u, eins, v), (u, -eins, -v)]
|
||||
seiten = []
|
||||
for dx, dy, dz in richtungen:
|
||||
lon = np.arctan2(dx, dz)
|
||||
lat = np.arctan2(dy, np.sqrt(dx * dx + dz * dz))
|
||||
x = (lon / (2 * math.pi) + 0.5) * W - 0.5
|
||||
y = (0.5 - lat / math.pi) * H - 0.5
|
||||
x0 = np.floor(x).astype(int); y0 = np.clip(np.floor(y).astype(int), 0, H - 2)
|
||||
fx, fy = (x - x0)[..., None], np.clip(y - y0, 0, 1)[..., None]
|
||||
x1 = (x0 + 1) % W; x0 %= W
|
||||
p = (q[y0, x0] * (1 - fx) * (1 - fy) + q[y0, x1] * fx * (1 - fy) + q[y0 + 1, x0] * (1 - fx) * fy + q[y0 + 1, x1] * fx * fy)
|
||||
seiten.append(Image.fromarray(np.clip(p, 0, 255).astype(np.uint8), 'RGB'))
|
||||
return seiten
|
||||
|
||||
|
||||
def seiten_aus_bild(bild, g=1024):
|
||||
"""
|
||||
Aus einem flachen Bild (16:9) einen nahtlosen Würfel:
|
||||
– vorne = linke Bildhälfte, rechts = rechte Hälfte, hinten/links = dieselben
|
||||
gespiegelt, so stoßen alle senkrechten Kanten passend aneinander;
|
||||
– oben/unten: die vier Seiten werden in ein Rundum-Panorama umgerechnet, dort
|
||||
nach unten (Wasser) bzw. oben (Himmel) gespiegelt fortgesetzt und weich
|
||||
auslaufen gelassen, daraus dann die obere und untere Würfelseite gerechnet.
|
||||
So gehen die Kanten nahtlos über – keine einfarbigen Flächen.
|
||||
"""
|
||||
b = bild.convert('RGB').resize((2 * g, g), Image.LANCZOS)
|
||||
vorne, rechts = b.crop((0, 0, g, g)), b.crop((g, 0, 2 * g, g))
|
||||
hinten, links = rechts.transpose(Image.FLIP_LEFT_RIGHT), vorne.transpose(Image.FLIP_LEFT_RIGHT)
|
||||
seiten = [np.array(s, np.float32) for s in (vorne, rechts, hinten, links)]
|
||||
|
||||
# 1) Seiten -> equirektangulares Panorama (oben/unten zunächst leer)
|
||||
W, H = 4 * g, 2 * g
|
||||
lon = ((np.arange(W) + 0.5) / W - 0.5) * 2 * math.pi
|
||||
lat = (0.5 - (np.arange(H) + 0.5) / H) * math.pi
|
||||
LON, LAT = np.meshgrid(lon, lat)
|
||||
dx, dy, dz = np.cos(LAT) * np.sin(LON), np.sin(LAT), np.cos(LAT) * np.cos(LON)
|
||||
equi = np.zeros((H, W, 3), np.float32)
|
||||
gueltig = np.zeros((H, W), bool)
|
||||
seitlich = np.abs(dy) < np.maximum(np.abs(dx), np.abs(dz))
|
||||
# (Maske, s, u, v) passend zu wuerfelseiten(): vorne (u,-v,1), rechts (1,-v,-u), hinten (-u,-v,-1), links (-1,-v,u)
|
||||
faelle = [
|
||||
(seitlich & (dz >= np.abs(dx)), dz, dx, -dy),
|
||||
(seitlich & (dx > np.abs(dz)), dx, -dz, -dy),
|
||||
(seitlich & (-dz >= np.abs(dx)), -dz, -dx, -dy),
|
||||
(seitlich & (-dx > np.abs(dz)), -dx, dz, -dy),
|
||||
]
|
||||
for (maske, s, u, v), seite in zip(faelle, seiten):
|
||||
su, sv = u[maske] / s[maske], v[maske] / s[maske]
|
||||
px = np.clip(((su + 1) / 2 * g).astype(int), 0, g - 1)
|
||||
py = np.clip(((sv + 1) / 2 * g).astype(int), 0, g - 1)
|
||||
equi[maske] = seite[py, px]
|
||||
gueltig |= maske
|
||||
|
||||
# 2) Lücken spiegeln: unten das Wasser (dunkler werdend), oben der Himmel
|
||||
zeilen = np.arange(H)[:, None]
|
||||
unterste = np.where(gueltig.any(0), H - 1 - np.argmax(gueltig[::-1], axis=0), H // 2)
|
||||
oberste = np.where(gueltig.any(0), np.argmax(gueltig, axis=0), H // 2)
|
||||
spalten = np.arange(W)[None, :]
|
||||
unten = zeilen > unterste[None, :]
|
||||
oben = zeilen < oberste[None, :]
|
||||
quelle_unten = np.clip(2 * unterste[None, :] - zeilen, oberste[None, :], H - 1)
|
||||
quelle_oben = np.broadcast_to(np.clip(oberste[None, :] + 2, 0, H - 1), (H, W)) # Himmel: oberste Zeile nach oben verlängern
|
||||
gespiegelt = equi.copy()
|
||||
gespiegelt[unten] = equi[quelle_unten, np.broadcast_to(spalten, (H, W))][unten]
|
||||
gespiegelt[oben] = equi[quelle_oben, np.broadcast_to(spalten, (H, W))][oben]
|
||||
# zum Pol hin weich in eine ruhige Farbe auslaufen lassen (keine Kreuznaht im Würfel)
|
||||
abstand_unten = np.clip((zeilen - unterste[None, :]) / (H * 0.34), 0, 1)[..., None] ** 1.3
|
||||
abstand_oben = np.clip((oberste[None, :] - zeilen) / (H * 0.34), 0, 1)[..., None] ** 1.3
|
||||
# Wasserfarbe aus der Bildmitte (am unteren Rand stehen Pflanzen), Himmel vom oberen Rand
|
||||
roh = np.array(b, np.float32)
|
||||
wasser = np.median(roh[int(g * 0.62):int(g * 0.82), int(g * 0.5):int(g * 1.5)].reshape(-1, 3), axis=0)
|
||||
himmel = equi[np.clip(oberste + 4, 0, H - 1), np.arange(W)].mean(0)
|
||||
gespiegelt = np.where(unten[..., None], gespiegelt * (1 - abstand_unten) * 0.9 + wasser * 0.8 * abstand_unten, gespiegelt)
|
||||
gespiegelt = np.where(oben[..., None], gespiegelt * (1 - abstand_oben) + himmel * abstand_oben, gespiegelt)
|
||||
bild_equi = Image.fromarray(np.clip(gespiegelt, 0, 255).astype(np.uint8), 'RGB')
|
||||
weich = bild_equi.filter(ImageFilter.GaussianBlur(16))
|
||||
maske_luecke = Image.fromarray(((unten | oben) * 255).astype(np.uint8)).filter(ImageFilter.GaussianBlur(6))
|
||||
bild_equi = Image.composite(weich, bild_equi, maske_luecke)
|
||||
|
||||
# 3) daraus oben/unten; die vier Seiten bleiben die scharfen Originale
|
||||
neu = wuerfelseiten(bild_equi, g)
|
||||
return [vorne, rechts, hinten, links, neu[4], neu[5]]
|
||||
|
||||
|
||||
def quelle(name):
|
||||
for endung in ('png', 'jpg', 'jpeg', 'webp'):
|
||||
p = os.path.join(QUELLE, f'{name}.{endung}')
|
||||
if os.path.exists(p):
|
||||
return Image.open(p).convert('RGB')
|
||||
return None
|
||||
|
||||
|
||||
def fuellen(bild, w, h):
|
||||
f = max(w / bild.width, h / bild.height)
|
||||
b = bild.resize((int(bild.width * f + 0.5), int(bild.height * f + 0.5)), Image.LANCZOS)
|
||||
x, y = (b.width - w) // 2, (b.height - h) // 2
|
||||
return b.crop((x, y, x + w, y + h))
|
||||
|
||||
|
||||
# ---------- Ladebildschirm (Vollbild) ----------
|
||||
|
||||
def ladebild(mit_text=True):
|
||||
W, H = 1920, 1080
|
||||
eigenes = quelle('hintergrund')
|
||||
if eigenes is not None:
|
||||
grund = fuellen(eigenes, W, H)
|
||||
else: # gleiche Landschaft wie im Menü, als Blick nach vorne
|
||||
grund = landschaft_flach(W, H)
|
||||
bild = grund.convert('RGBA')
|
||||
# leichte Abdunklung zur Mitte und unten, damit Logo und Balken lesbar sind
|
||||
schatten = Image.new('L', (W, H), 0)
|
||||
ImageDraw.Draw(schatten).ellipse((W * 0.18, H * 0.12, W * 0.82, H * 0.72), fill=95)
|
||||
schatten = schatten.filter(ImageFilter.GaussianBlur(120))
|
||||
unten = Image.fromarray((np.clip((np.arange(H) - H * 0.62) / (H * 0.38), 0, 1) * 120).astype(np.uint8)[:, None].repeat(W, 1))
|
||||
dunkel = Image.new('RGBA', (W, H), (4, 8, 20, 0))
|
||||
dunkel.putalpha(ImageChops.lighter(schatten, unten))
|
||||
bild.alpha_composite(dunkel)
|
||||
art, _ = logo(30)
|
||||
art = einpassen(art, int(W * 0.62), int(H * 0.56))
|
||||
lx, ly = (W - art.width) // 2, int(H * 0.40 - art.height / 2)
|
||||
bild.alpha_composite(art, (lx, ly))
|
||||
unter, _ = untertitel_bild(6)
|
||||
bild.alpha_composite(unter, ((W - unter.width) // 2, ly + art.height + 6))
|
||||
if not mit_text: # Neustart-Bildschirm des Launchers setzt Text und Balken selbst darüber
|
||||
return bild.convert('RGB')
|
||||
# "Wird geladen …" unter dem Balken (Minecraft zeichnet den Balken bei 83,25 % der Höhe)
|
||||
schrift = ImageFont.truetype(SCHRIFT, 34)
|
||||
text = 'Wird geladen …'
|
||||
tb = ImageDraw.Draw(bild).textbbox((0, 0), text, font=schrift)
|
||||
tw, th = tb[2] - tb[0], tb[3] - tb[1]
|
||||
kasten = Image.new('RGBA', (W, H), (0, 0, 0, 0))
|
||||
kx, ky = (W - tw) // 2, int(H * 0.885)
|
||||
ImageDraw.Draw(kasten).rounded_rectangle((kx - 26, ky - 14, kx + tw + 26, ky + th + 22), radius=10, fill=(6, 12, 28, 175))
|
||||
bild.alpha_composite(kasten)
|
||||
ImageDraw.Draw(bild).text((kx - tb[0], ky - tb[1] + 2), text, font=schrift, fill=(236, 244, 255, 255))
|
||||
return bild.convert('RGB')
|
||||
|
||||
|
||||
def als_logotextur(bild):
|
||||
"""
|
||||
Minecraft zeichnet das Logo additiv auf den Hintergrund und teilt es: linke
|
||||
Hälfte aus der oberen Texturhälfte, rechte aus der unteren. Darum: Nachtblau
|
||||
abziehen (wird beim Zeichnen wieder addiert) und die Hälften gestreckt stapeln.
|
||||
"""
|
||||
a = np.clip(np.array(bild, float) - np.array(NACHTBLAU, float), 0, 255).astype(np.uint8)
|
||||
b = Image.fromarray(a, 'RGB')
|
||||
w, h = b.size
|
||||
t = Image.new('RGB', (2048, 2048))
|
||||
t.paste(b.crop((0, 0, w // 2, h)).resize((2048, 1024), Image.LANCZOS), (0, 0))
|
||||
t.paste(b.crop((w // 2, 0, w, h)).resize((2048, 1024), Image.LANCZOS), (0, 1024))
|
||||
return t.convert('RGBA')
|
||||
|
||||
|
||||
# ---------- Knöpfe und Symbole (wie im Entwurf) ----------
|
||||
|
||||
SYMBOL_ZEICHEN = ['', '', '', '', '', ''] # Spitzhacke, Personen, Globus, Zahnrad, Ein/Aus, Puzzle (Mods)
|
||||
|
||||
|
||||
def knopf(hell, f):
|
||||
"""Knopf 200×20 Punkte in f-facher Auflösung: dunkles Schiefer bzw. leuchtendes Blau."""
|
||||
w, h = 200 * f, 20 * f
|
||||
if hell:
|
||||
oben, unten, rand, licht = (64, 166, 248), (20, 92, 200), (130, 232, 255), (170, 225, 255)
|
||||
else:
|
||||
oben, unten, rand, licht = (46, 56, 74), (24, 30, 42), (124, 142, 172), (78, 92, 118)
|
||||
a = verlauf(h, w, oben, unten)
|
||||
bild = rgba(a, np.full((h, w), 255, np.uint8))
|
||||
d = ImageDraw.Draw(bild)
|
||||
s = max(1, f // 2)
|
||||
d.rectangle((0, 0, w - 1, h - 1), outline=(6, 10, 20, 255), width=s) # dunkle Außenkante
|
||||
d.rectangle((s, s, w - 1 - s, h - 1 - s), outline=rand + (255,), width=s) # heller Rand
|
||||
d.line((2 * s, 2 * s, w - 1 - 2 * s, 2 * s), fill=licht + (255,), width=s) # Lichtkante oben
|
||||
for x0, y0 in ((0, 0), (w - s, 0), (0, h - s), (w - s, h - s)): # Ecken leicht abgerundet
|
||||
d.rectangle((x0, y0, x0 + s - 1, y0 + s - 1), fill=(0, 0, 0, 0))
|
||||
return bild
|
||||
|
||||
|
||||
def puzzle(farbe=(238, 246, 255, 255), groesse=64):
|
||||
"""Puzzleteil (Symbol für Mods)."""
|
||||
b = Image.new('RGBA', (64, 64), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(b)
|
||||
d.rounded_rectangle((8, 18, 46, 56), radius=5, fill=farbe)
|
||||
d.ellipse((19, 4, 35, 22), fill=farbe) # Nase oben
|
||||
d.ellipse((42, 29, 60, 45), fill=farbe) # Nase rechts
|
||||
d.ellipse((4, 29, 18, 43), fill=(0, 0, 0, 0)) # Kerbe links
|
||||
return b if groesse == 64 else b.resize((groesse, groesse), Image.LANCZOS)
|
||||
|
||||
|
||||
def symbole():
|
||||
"""6 Symbole, je 16×16 (aus 64×64 verkleinert), weiß – als Schriftzeichen."""
|
||||
groesse, f = 64, 4
|
||||
leinwand = Image.new('RGBA', (16 * 6, 16), (0, 0, 0, 0))
|
||||
farbe = (238, 246, 255, 255)
|
||||
|
||||
def neu():
|
||||
return Image.new('RGBA', (groesse, groesse), (0, 0, 0, 0))
|
||||
|
||||
bilder = []
|
||||
b = neu(); d = ImageDraw.Draw(b) # Spitzhacke: aufrecht zeichnen, dann kippen
|
||||
d.line((32, 16, 32, 62), fill=farbe, width=10)
|
||||
d.arc((2, 12, 62, 56), 198, 342, fill=farbe, width=12)
|
||||
d.polygon([(4, 30), (0, 40), (10, 34)], fill=farbe); d.polygon([(60, 30), (64, 40), (54, 34)], fill=farbe)
|
||||
bilder.append(b.rotate(-40, resample=Image.BICUBIC))
|
||||
b = neu(); d = ImageDraw.Draw(b) # zwei Personen
|
||||
d.ellipse((30, 8, 50, 28), fill=farbe); d.pieslice((22, 30, 58, 70), 180, 360, fill=farbe)
|
||||
d.ellipse((10, 14, 28, 32), fill=farbe); d.pieslice((2, 34, 36, 68), 180, 360, fill=farbe)
|
||||
bilder.append(b)
|
||||
b = neu(); d = ImageDraw.Draw(b) # Globus
|
||||
d.ellipse((4, 4, 60, 60), outline=farbe, width=8)
|
||||
d.ellipse((20, 4, 44, 60), outline=farbe, width=7)
|
||||
d.line((4, 32, 60, 32), fill=farbe, width=7)
|
||||
d.arc((10, -10, 54, 26), 30, 150, fill=farbe, width=3); d.arc((10, 38, 54, 74), 210, 330, fill=farbe, width=3)
|
||||
bilder.append(b)
|
||||
b = neu(); d = ImageDraw.Draw(b) # Zahnrad
|
||||
for k in range(8):
|
||||
w = k * math.pi / 4
|
||||
cx, cy = 32 + math.cos(w) * 23, 32 + math.sin(w) * 23
|
||||
pts = [(cx + math.cos(w + p) * 7, cy + math.sin(w + p) * 7) for p in (0.8, 2.35, 3.9, 5.5)]
|
||||
d.polygon(pts, fill=farbe)
|
||||
d.ellipse((12, 12, 52, 52), fill=farbe)
|
||||
d.ellipse((26, 26, 38, 38), fill=(0, 0, 0, 0))
|
||||
bilder.append(b)
|
||||
b = neu(); d = ImageDraw.Draw(b) # Ein/Aus
|
||||
d.arc((6, 8, 58, 60), -60, 240, fill=farbe, width=10)
|
||||
d.line((32, 2, 32, 30), fill=farbe, width=10)
|
||||
bilder.append(b)
|
||||
bilder.append(puzzle(farbe))
|
||||
for i, b in enumerate(bilder):
|
||||
leinwand.alpha_composite(b.resize((16, 16), Image.LANCZOS), (16 * i, 0))
|
||||
return leinwand
|
||||
|
||||
|
||||
def sprachdateien():
|
||||
"""Symbole vor die Beschriftungen der Menüknöpfe setzen (de_de und en_us)."""
|
||||
import json
|
||||
sp, mp, rl, op, qu, md = SYMBOL_ZEICHEN
|
||||
texte = {
|
||||
'de_de': {'menu.singleplayer': 'Einzelspieler', 'menu.multiplayer': 'Mehrspieler', 'menu.online': 'Minecraft Realms',
|
||||
'menu.options': 'Optionen …', 'menu.quit': 'Spiel beenden'},
|
||||
'en_us': {'menu.singleplayer': 'Singleplayer', 'menu.multiplayer': 'Multiplayer', 'menu.online': 'Minecraft Realms',
|
||||
'menu.options': 'Options...', 'menu.quit': 'Quit Game'},
|
||||
}
|
||||
zeichen = {'menu.singleplayer': sp, 'menu.multiplayer': mp, 'menu.online': rl, 'menu.options': op, 'menu.quit': qu}
|
||||
aus = {}
|
||||
for sprache, t in texte.items():
|
||||
aus[sprache] = json.dumps({k: zeichen[k] + ' ' + v for k, v in t.items()}, ensure_ascii=True, indent=2)
|
||||
mods = {
|
||||
'de_de': {'menu.sendFeedback': md + ' Mods verwalten …', 'menu.feedback': md + ' Mods & Feedback …'},
|
||||
'en_us': {'menu.sendFeedback': md + ' Manage Mods...', 'menu.feedback': md + ' Mods & Feedback...'},
|
||||
}
|
||||
for sprache, t in mods.items():
|
||||
aus['mods_' + sprache] = json.dumps(t, ensure_ascii=True, indent=2)
|
||||
schrift = json.dumps({'providers': [{'type': 'bitmap', 'file': 'aeromc:font/symbole.png', 'ascent': 7, 'height': 8,
|
||||
'chars': [''.join(SYMBOL_ZEICHEN)]}]}, ensure_ascii=True, indent=2)
|
||||
return aus, schrift
|
||||
|
||||
|
||||
def knopf_dateien():
|
||||
import json
|
||||
knopf(False, 2).save(os.path.join(ZIEL, 'knopf.png'))
|
||||
knopf(True, 2).save(os.path.join(ZIEL, 'knopf_hell.png'))
|
||||
knopf(False, 4).save(os.path.join(ZIEL, 'knopf_4x.png'))
|
||||
knopf(True, 4).save(os.path.join(ZIEL, 'knopf_hell_4x.png'))
|
||||
meta = json.dumps({'gui': {'scaling': {'type': 'nine_slice', 'width': 200, 'height': 20, 'border': 3}}}, indent=2)
|
||||
with open(os.path.join(ZIEL, 'knopf.png.mcmeta'), 'w', encoding='utf-8') as f:
|
||||
f.write(meta)
|
||||
symbole().save(os.path.join(ZIEL, 'symbole.png'))
|
||||
puzzle(groesse=40).save(os.path.join(ZIEL, 'mods_knopf.png')) # Symbol-Knopf im Pausemenü (26.x)
|
||||
sprachen, schrift = sprachdateien()
|
||||
for sprache, text in sprachen.items():
|
||||
with open(os.path.join(ZIEL, f'lang_{sprache}.json'), 'w', encoding='utf-8') as f:
|
||||
f.write(text)
|
||||
with open(os.path.join(ZIEL, 'font_default.json'), 'w', encoding='utf-8') as f:
|
||||
f.write(schrift)
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(ZIEL, exist_ok=True)
|
||||
titel(False).save(os.path.join(ZIEL, 'titel_neu.png'))
|
||||
titel(True).save(os.path.join(ZIEL, 'titel_alt.png'))
|
||||
# Der Untertitel steckt jetzt im Titel-Logo – edition.png bleibt leer
|
||||
leer = Image.new('RGBA', (512, 64), (0, 0, 0, 0))
|
||||
leer.save(os.path.join(ZIEL, 'edition_neu.png'))
|
||||
leer.save(os.path.join(ZIEL, 'edition_alt.png'))
|
||||
|
||||
equi, flach = quelle('panorama'), quelle('hintergrund')
|
||||
if equi is not None:
|
||||
seiten = wuerfelseiten(equi) # echtes Rundum-Panorama
|
||||
elif flach is not None:
|
||||
seiten = seiten_aus_bild(flach) # flaches Bild, nahtlos gespiegelt
|
||||
else:
|
||||
seiten = wuerfelseiten(landschaft()) # gezeichnete Block-Landschaft
|
||||
for i, s in enumerate(seiten):
|
||||
s.save(os.path.join(ZIEL, f'panorama_{i}.png'), optimize=True)
|
||||
|
||||
lade = ladebild()
|
||||
lade.save(os.path.join(QUELLE if os.path.isdir(QUELLE) else ZIEL, '_vorschau_ladebildschirm.png'))
|
||||
als_logotextur(lade).save(os.path.join(ZIEL, 'ladelogo.png'), optimize=True)
|
||||
# derselbe Ladebildschirm ohne Text – für den Neustart über den Mod-Schalter
|
||||
ladebild(mit_text=False).save(os.path.join(ZIEL, 'neustart.jpg'), quality=88)
|
||||
|
||||
# 1.14/1.15: festes 256×256-Feld in der Mitte, Logo auf durchsichtigem Grund
|
||||
alt = Image.new('RGBA', (1024, 1024), (0, 0, 0, 0))
|
||||
art, _ = logo(26)
|
||||
art = einpassen(art, 980, 600)
|
||||
alt.alpha_composite(art, ((1024 - art.width) // 2, (1024 - art.height) // 2))
|
||||
alt.save(os.path.join(ZIEL, 'ladelogo_alt.png'))
|
||||
|
||||
knopf_dateien()
|
||||
|
||||
symbol = Image.open(LOGO).convert('RGBA')
|
||||
symbol.resize((128, 128), Image.LANCZOS).save(os.path.join(ZIEL, 'pack.png'))
|
||||
for n in (16, 32, 48, 64, 128, 256):
|
||||
with open(os.path.join(ZIEL, f'icon_{n}.rgba'), 'wb') as f:
|
||||
f.write(symbol.resize((n, n), Image.LANCZOS).tobytes())
|
||||
print('Fertig:', ZIEL, '| Hintergrund:', 'eigenes Bild' if quelle('hintergrund') else 'Panorama-Ausschnitt',
|
||||
'| Panorama:', 'eigenes Panorama' if quelle('panorama') else ('aus dem Hintergrundbild' if quelle('hintergrund') else 'Block-Landschaft'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,39 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* pruefe-version.js – läuft vor jedem Build (prebuild)
|
||||
* ----------------------------------------------------
|
||||
* Bricht ab, wenn die Version aus package.json schon als Release in Gitea
|
||||
* veröffentlicht ist. Ein neuer Build derselben Version ergäbe andere Dateien
|
||||
* mit neuer Signatur – die bereits veröffentlichten EXEs würden dann nicht mehr
|
||||
* dazu passen und kein Launcher nähme das Update an.
|
||||
*
|
||||
* Ausnahme (z. B. Testbuild, der nie hochgeladen wird): AEROMC_GLEICHE_VERSION=1
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const API = 'https://git.viper.ipv64.net/api/v1/repos/M_Viper/AeroMc-Launcher/releases?limit=50';
|
||||
const version = require(path.join(__dirname, '..', 'package.json')).version;
|
||||
|
||||
(async () => {
|
||||
if (process.env.AEROMC_GLEICHE_VERSION === '1') return;
|
||||
let releases;
|
||||
try {
|
||||
const res = await fetch(API, { headers: { 'User-Agent': 'AeroMC-Release', Accept: 'application/json' } });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
releases = await res.json();
|
||||
} catch (err) {
|
||||
console.warn(`\n! Gitea nicht erreichbar (${err.message}) – Versionsprüfung übersprungen.`);
|
||||
return;
|
||||
}
|
||||
const norm = (v) => String(v || '').trim().replace(/^v/i, '');
|
||||
const treffer = (releases || []).find((r) => !r.draft && norm(r.tag_name) === version);
|
||||
if (treffer) {
|
||||
console.error(`\n✖ Version ${version} ist bereits in Gitea veröffentlicht (${treffer.published_at}).`);
|
||||
console.error(' Bitte zuerst die Versionsnummer in package.json erhöhen – sonst passen die');
|
||||
console.error(' veröffentlichten Dateien nicht mehr zur Signatur.');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`» Version ${version} ist noch nicht veröffentlicht – Build läuft.`);
|
||||
})();
|
||||
@@ -0,0 +1,227 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const readline = require('readline');
|
||||
const { execFileSync } = require('child_process');
|
||||
const { signaturText, pruefeSignatur, holeSignatur, SIGNATUR_API } = require('../src/updater');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const DIST = path.join(ROOT, 'dist');
|
||||
const ORDNER = path.join(os.homedir(), '.aeromc-release');
|
||||
const OEFFENTLICH = path.join(ROOT, 'src', 'update-schluessel.pem');
|
||||
const PRIVAT = process.env.AEROMC_SIGN_KEY || path.join(ORDNER, 'release-key.pem');
|
||||
const ZUGANG = path.join(ORDNER, 'wordpress.json');
|
||||
const DATEIEN = { setup: 'AeroMC-Setup.exe', portable: 'AeroMC-Portable.exe' };
|
||||
const WP_STANDARD = new URL(SIGNATUR_API).origin;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Schlüssel
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function schluesselAnlegen() {
|
||||
if (fs.existsSync(PRIVAT)) {
|
||||
console.log(`Es gibt bereits einen Release-Schlüssel: ${PRIVAT}`);
|
||||
console.log('Er wird nicht überschrieben – sonst nähmen bestehende Launcher keine Updates mehr an.');
|
||||
return;
|
||||
}
|
||||
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
||||
fs.mkdirSync(path.dirname(PRIVAT), { recursive: true });
|
||||
fs.writeFileSync(PRIVAT, privateKey.export({ type: 'pkcs8', format: 'pem' }), { mode: 0o600 });
|
||||
fs.writeFileSync(OEFFENTLICH, publicKey.export({ type: 'spki', format: 'pem' }));
|
||||
console.log(`Privater Schlüssel: ${PRIVAT} (SICHERN, niemals weitergeben!)`);
|
||||
console.log(`Öffentlicher Schlüssel: ${OEFFENTLICH} (wird in den Launcher eingebaut)`);
|
||||
}
|
||||
|
||||
|
||||
function dpapi(skript, eingabe) {
|
||||
return execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', skript],
|
||||
{ input: eingabe, encoding: 'utf8', windowsHide: true }).trim();
|
||||
}
|
||||
function verschluesseln(text) {
|
||||
return dpapi('$t = [Console]::In.ReadToEnd(); ConvertTo-SecureString $t -AsPlainText -Force | ConvertFrom-SecureString', text);
|
||||
}
|
||||
function entschluesseln(hex) {
|
||||
return dpapi('$h = [Console]::In.ReadToEnd().Trim(); $s = ConvertTo-SecureString $h; '
|
||||
+ '[Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR($s))', hex);
|
||||
}
|
||||
|
||||
function frage(text, verdeckt) {
|
||||
return new Promise((resolve) => {
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
|
||||
if (verdeckt) {
|
||||
rl._writeToOutput = (s) => { if (s.includes(text)) process.stdout.write(text); else if (s === '\r\n' || s === '\n') process.stdout.write(s); };
|
||||
}
|
||||
rl.question(text, (antwort) => { rl.close(); if (verdeckt) process.stdout.write('\n'); resolve(antwort.trim()); });
|
||||
});
|
||||
}
|
||||
|
||||
function ladeZugang() {
|
||||
if (!fs.existsSync(ZUGANG)) return null;
|
||||
const z = JSON.parse(fs.readFileSync(ZUGANG, 'utf8').replace(/^/, ''));
|
||||
return { seite: z.seite, benutzer: z.benutzer, passwort: entschluesseln(z.passwort) };
|
||||
}
|
||||
|
||||
function kopfzeilen(zugang) {
|
||||
const daten = Buffer.from(`${zugang.benutzer}:${zugang.passwort}`).toString('base64');
|
||||
return {
|
||||
Authorization: 'Basic ' + daten,
|
||||
// Ersatz für Server, die "Authorization" nicht an PHP weitergeben (siehe signaturen.php)
|
||||
'X-AeroMC-Auth': daten,
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'AeroMC-Release',
|
||||
};
|
||||
}
|
||||
|
||||
// Übersetzt die Diagnose des Plugins (ab 1.5.4) in eine klare Meldung
|
||||
function zugangsFehler(d, benutzer) {
|
||||
if (!d || !d.version) {
|
||||
return 'Das Plugin auf der Seite ist zu alt – bitte "AeroMC Freunde" 1.5.4 oder neuer installieren.';
|
||||
}
|
||||
if (!d.anwendungspasswoerter) return 'Anwendungspasswörter sind auf der Seite abgeschaltet (z. B. durch ein Sicherheits-Plugin).';
|
||||
if (!d.authorization_kommt_an && !d.eigener_kopf_kommt_an) {
|
||||
return 'Der Server leitet keine Anmeldedaten an WordPress weiter (beide Header fehlen) – das muss der Hoster freigeben.';
|
||||
}
|
||||
switch (d.grund) {
|
||||
case 'invalid_username': return `Den Benutzer "${benutzer}" gibt es nicht (Benutzername, nicht E-Mail-Adresse).`;
|
||||
case 'incorrect_password':
|
||||
return 'Falsches Anwendungspasswort. Achtung: Gemeint ist NICHT das Login-Passwort, sondern das unter '
|
||||
+ 'Benutzer → Profil → "Anwendungspasswörter" erzeugte (Form "abcd efgh ijkl …").';
|
||||
case 'application_passwords_disabled':
|
||||
case 'application_passwords_disabled_for_user':
|
||||
return `Für "${benutzer}" sind Anwendungspasswörter abgeschaltet.`;
|
||||
default: break;
|
||||
}
|
||||
if (d.benutzer && !d.darf_hochladen) {
|
||||
return `Anmeldung als "${d.benutzer}" klappt, aber der Benutzer hat nicht die Rolle "AeroMC Release" (Benutzer bearbeiten → Rolle).`;
|
||||
}
|
||||
return 'Anmeldung abgelehnt' + (d.grund ? ` (${d.grund})` : '') + '.';
|
||||
}
|
||||
|
||||
async function pruefeZugang(zugang) {
|
||||
const res = await fetch(`${zugang.seite}/wp-json/aeromc/v1/update-signatur/zugang`, { headers: kopfzeilen(zugang) });
|
||||
if (res.status === 404) throw new Error('Die Seite kennt die Signatur-Schnittstelle nicht – ist das Plugin "AeroMC Freunde" (ab 1.5.4) installiert und aktiviert?');
|
||||
let d = null;
|
||||
try { d = await res.json(); } catch { /* keine JSON-Antwort */ }
|
||||
if (res.ok && d && d.ok) return d;
|
||||
if (res.status === 401 || res.status === 403) throw new Error(zugangsFehler(d, zugang.benutzer));
|
||||
throw new Error('WordPress antwortet mit HTTP ' + res.status);
|
||||
}
|
||||
|
||||
async function wordpressEinrichten() {
|
||||
console.log('WordPress-Zugang für das automatische Hochladen der Signaturen einrichten.');
|
||||
console.log('Vorher in WordPress: Benutzer mit der Rolle "AeroMC Release" anlegen, dann in dessen Profil');
|
||||
console.log('unter "Anwendungspasswörter" ein Passwort erzeugen (Name z. B. "AeroMC Build").\n');
|
||||
const seite = (await frage(`Adresse der WordPress-Seite [${WP_STANDARD}]: `)) || WP_STANDARD;
|
||||
if (!/^https:\/\//i.test(seite)) throw new Error('Bitte eine https://-Adresse angeben.');
|
||||
const benutzer = await frage('WordPress-Benutzername: ');
|
||||
const passwort = await frage('Anwendungspasswort (Eingabe bleibt unsichtbar): ', true);
|
||||
if (!benutzer || !passwort) throw new Error('Benutzername und Passwort werden gebraucht.');
|
||||
const zugang = { seite: seite.replace(/\/+$/, ''), benutzer, passwort };
|
||||
const antwort = await pruefeZugang(zugang);
|
||||
fs.mkdirSync(ORDNER, { recursive: true });
|
||||
fs.writeFileSync(ZUGANG, JSON.stringify({ seite: zugang.seite, benutzer, passwort: verschluesseln(passwort) }, null, 2), { mode: 0o600 });
|
||||
console.log(`\n✔ Zugang geprüft (angemeldet als ${antwort.benutzer}) und gespeichert: ${ZUGANG}`);
|
||||
console.log(' Das Passwort ist mit Windows-DPAPI verschlüsselt.');
|
||||
}
|
||||
|
||||
|
||||
|
||||
function sha256(datei) {
|
||||
const h = crypto.createHash('sha256');
|
||||
const fd = fs.openSync(datei, 'r');
|
||||
const buf = Buffer.alloc(4 * 1024 * 1024);
|
||||
let n;
|
||||
while ((n = fs.readSync(fd, buf, 0, buf.length, null)) > 0) h.update(buf.subarray(0, n));
|
||||
fs.closeSync(fd);
|
||||
return h.digest('hex');
|
||||
}
|
||||
|
||||
function signieren(art) {
|
||||
const name = DATEIEN[art];
|
||||
if (!name) throw new Error(`Unbekannte Art "${art}" – erwartet: setup, portable, schluessel, wordpress oder hochladen.`);
|
||||
const datei = path.join(DIST, name);
|
||||
if (!fs.existsSync(datei)) throw new Error(`${datei} fehlt – erst bauen.`);
|
||||
if (!fs.existsSync(PRIVAT)) {
|
||||
throw new Error(`Kein Release-Schlüssel unter ${PRIVAT}. Einmalig "node scripts/sign-release.js schluessel" ausführen.`);
|
||||
}
|
||||
const privat = crypto.createPrivateKey(fs.readFileSync(PRIVAT, 'utf8'));
|
||||
|
||||
const eingebaut = crypto.createPublicKey(fs.readFileSync(OEFFENTLICH, 'utf8')).export({ type: 'spki', format: 'der' });
|
||||
const passend = crypto.createPublicKey(privat).export({ type: 'spki', format: 'der' });
|
||||
if (!eingebaut.equals(passend)) {
|
||||
throw new Error('Der private Schlüssel passt nicht zu src/update-schluessel.pem – Signatur wäre für den Launcher wertlos.');
|
||||
}
|
||||
|
||||
const version = require(path.join(ROOT, 'package.json')).version;
|
||||
const hash = sha256(datei);
|
||||
const sig = {
|
||||
format: 'AeroMC-Update/1',
|
||||
datei: name,
|
||||
version,
|
||||
sha256: hash,
|
||||
signatur: crypto.sign(null, signaturText(name, version, hash), privat).toString('base64'),
|
||||
};
|
||||
pruefeSignatur(sig, name, version, hash);
|
||||
fs.writeFileSync(datei + '.sig', JSON.stringify(sig, null, 2) + '\n');
|
||||
console.log(`\n» ${name} ${version} signiert`);
|
||||
return sig;
|
||||
}
|
||||
|
||||
async function hochladen(sig) {
|
||||
const zugang = ladeZugang();
|
||||
if (!zugang) {
|
||||
throw new Error('Kein WordPress-Zugang eingerichtet – Signatur NICHT hochgeladen.\n'
|
||||
+ ' Einmalig ausführen: node scripts/sign-release.js wordpress\n'
|
||||
+ ` danach: node scripts/sign-release.js hochladen ${sig.datei === DATEIEN.setup ? 'setup' : 'portable'}`);
|
||||
}
|
||||
// Nie die Signatur einer schon veröffentlichten Datei überschreiben: Dann passte
|
||||
// die EXE im Gitea-Release nicht mehr, und kein Launcher nähme das Update an.
|
||||
const vorhanden = await holeSignatur(sig.datei, sig.version);
|
||||
if (vorhanden && vorhanden.sha256 !== sig.sha256 && process.env.AEROMC_SIG_ERSETZEN !== '1') {
|
||||
throw new Error(`Für ${sig.datei} ${sig.version} liegt schon eine andere Signatur auf WordPress – `
|
||||
+ 'diese Version ist vermutlich bereits veröffentlicht. Bitte die Versionsnummer in package.json erhöhen.\n'
|
||||
+ ' (Nur wenn die Version sicher NICHT veröffentlicht ist: AEROMC_SIG_ERSETZEN=1 setzen.)');
|
||||
}
|
||||
if (vorhanden && vorhanden.sha256 === sig.sha256) {
|
||||
console.log('» Signatur liegt bereits unverändert auf WordPress.');
|
||||
return;
|
||||
}
|
||||
const res = await fetch(`${zugang.seite}/wp-json/aeromc/v1/update-signatur`, {
|
||||
method: 'POST', headers: kopfzeilen(zugang), body: JSON.stringify(sig),
|
||||
});
|
||||
let antwort = {};
|
||||
try { antwort = await res.json(); } catch { /* leer */ }
|
||||
if (!res.ok || !antwort.ok) {
|
||||
throw new Error(`Hochladen fehlgeschlagen (HTTP ${res.status}): ${antwort.message || 'unbekannter Fehler'}`);
|
||||
}
|
||||
// Gegenprobe: liefert die öffentliche Schnittstelle genau das, was der Launcher braucht?
|
||||
const zurueck = await holeSignatur(sig.datei, sig.version);
|
||||
if (!zurueck || zurueck.signatur !== sig.signatur || zurueck.sha256 !== sig.sha256) {
|
||||
throw new Error('Signatur wurde hochgeladen, die Seite liefert aber etwas anderes zurück (Cache?).');
|
||||
}
|
||||
console.log(`» Signatur auf ${zugang.seite} hochgeladen und geprüft${antwort.geprueft ? ' (vom Server gegengeprüft)' : ''} – ins Gitea-Release kommt nur die EXE.`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [art, zusatz] = process.argv.slice(2);
|
||||
if (art === 'schluessel') return schluesselAnlegen();
|
||||
if (art === 'wordpress') return wordpressEinrichten();
|
||||
if (art === 'hochladen') {
|
||||
const name = DATEIEN[zusatz];
|
||||
if (!name) throw new Error('Erwartet: hochladen setup|portable');
|
||||
const sigDatei = path.join(DIST, name + '.sig');
|
||||
if (!fs.existsSync(sigDatei)) throw new Error(`${sigDatei} fehlt – erst bauen und signieren.`);
|
||||
const sig = JSON.parse(fs.readFileSync(sigDatei, 'utf8'));
|
||||
pruefeSignatur(sig, name, sig.version, sha256(path.join(DIST, name)));
|
||||
return hochladen(sig);
|
||||
}
|
||||
return hochladen(signieren(art));
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('\n✖ ' + err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
After Width: | Height: | Size: 3.1 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
@@ -1,58 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* AeroMC – CurseForge-Weiterleitung
|
||||
* ---------------------------------
|
||||
* Hält den CurseForge-API-Schlüssel und stellt die Anfragen stellvertretend
|
||||
* für den Launcher. So bleibt der Schlüssel auf dem Server: in der verteilten
|
||||
* .exe wäre er für jeden auslesbar, und CurseForge untersagt die Weitergabe
|
||||
* an Dritte ausdrücklich.
|
||||
*
|
||||
* Wege:
|
||||
* GET /health – Lebenszeichen, ohne Schlüsselangaben
|
||||
* POST /v1/mods/files – Dateiliste auflösen (Rumpf: { fileIds: [...] })
|
||||
* GET /v1/download?url=... – Mod-Datei durchreichen (CDN verlangt seit
|
||||
* Juli 2026 ebenfalls einen Schlüssel)
|
||||
*
|
||||
* Einstellungen über Umgebungsvariablen, siehe .env.example.
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const { Readable } = require('stream');
|
||||
|
||||
const KEY = (process.env.CF_API_KEY || '').trim();
|
||||
const PORT = Number(process.env.PORT || 8420);
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
/*
|
||||
* Ein oder mehrere gültige Token, durch Komma getrennt.
|
||||
*
|
||||
* Mehrere deshalb, weil das Token in ausgelieferten Paketen steckt: Beim
|
||||
* Wechsel trägst du eine Zeit lang altes und neues ein, damit ältere
|
||||
* Installationen weiterlaufen, bis alle das Update haben. Danach das alte
|
||||
* streichen.
|
||||
*/
|
||||
|
||||
const TOKENS = new Set(
|
||||
String(process.env.CF_PROXY_TOKEN || '')
|
||||
.split(',').map((t) => t.trim()).filter(Boolean),
|
||||
);
|
||||
const LIMIT_PRO_MIN = Number(process.env.RATE_LIMIT_PER_MIN || 60);
|
||||
const MAX_MB = Number(process.env.MAX_DOWNLOAD_MB || 512);
|
||||
// Anzahl der Reverse-Proxies davor. 0 = keiner, 1 = z. B. Nginx Proxy Manager,
|
||||
// 2 = z. B. Cloudflare vor dem Proxy Manager.
|
||||
|
||||
const PROXY_STUFEN = Math.max(0, Number(process.env.TRUST_PROXY || 0) || 0);
|
||||
|
||||
const CF_API = 'https://api.curseforge.com/v1';
|
||||
|
||||
// Ohne User-Agent weisen Dienste hinter Cloudflare Anfragen gerne mit 403 ab.
|
||||
const UA = 'AeroMC-Launcher-Proxy/1.0 (+https://git.viper.ipv64.net)';
|
||||
|
||||
function cfKopfzeilen(weitere) {
|
||||
return Object.assign({ 'x-api-key': KEY, Accept: 'application/json', 'User-Agent': UA }, weitere || {});
|
||||
}
|
||||
|
||||
// Nur diese Hosts dürfen durchgereicht werden. Ohne diese Liste wäre der
|
||||
// Server ein offenes Tor in dein Netz – jemand könnte ihn Adressen im
|
||||
// Heimnetz abrufen lassen und die Antwort mitlesen.
|
||||
const ERLAUBTE_HOSTS = new Set([
|
||||
'edge.forgecdn.net',
|
||||
'mediafilez.forgecdn.net',
|
||||
|
||||
|
After Width: | Height: | Size: 484 B |
|
After Width: | Height: | Size: 542 B |
|
After Width: | Height: | Size: 211 B |
|
After Width: | Height: | Size: 536 B |
|
After Width: | Height: | Size: 572 B |
|
After Width: | Height: | Size: 207 B |
|
After Width: | Height: | Size: 207 B |
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"providers": [
|
||||
{
|
||||
"type": "bitmap",
|
||||
"file": "aeromc:font/symbole.png",
|
||||
"ascent": 7,
|
||||
"height": 8,
|
||||
"chars": [
|
||||
"\ue900\ue901\ue902\ue903\ue904\ue905"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||