Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
972a3e73da | ||
|
|
e83c951256 | ||
|
|
67fe4cba48 |
+18
-2
@@ -2,7 +2,20 @@
|
|||||||
|
|
||||||
Alle nennenswerten Änderungen an AeroMC werden hier festgehalten.
|
Alle nennenswerten Änderungen an AeroMC werden hier festgehalten.
|
||||||
|
|
||||||
## [1.1.6] - 2026-10-02
|
## [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
|
### Neu
|
||||||
- Skin-Editor: Skins direkt im Launcher pixeln – erreichbar im Skin-Fenster über „Im Editor bearbeiten…“
|
- Skin-Editor: Skins direkt im Launcher pixeln – erreichbar im Skin-Fenster über „Im Editor bearbeiten…“
|
||||||
@@ -15,11 +28,14 @@ Alle nennenswerten Änderungen an AeroMC werden hier festgehalten.
|
|||||||
- 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
|
- 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“
|
- 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
|
- 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 gewähltes Design gilt sofort in allen offenen Fenstern
|
- 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
|
- 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
|
- 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
|
- 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
|
### Behoben
|
||||||
- Das Symbol für „Optionen“ sah aus wie eine Sonne – jetzt ein Zahnrad
|
- 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
|
- Der Skin-Hintergrund zeigte schmale (Alex-)Skins mit breiten Armen – dadurch erschienen falsche Streifen an den Armen
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
Ein Minecraft-Launcher für Windows mit getrennten Instanzen, Mod-Unterstützung, Spielaufzeichnung, Freundesystem und Microsoft-Anmeldung.
|
Ein Minecraft-Launcher für Windows mit getrennten Instanzen, Mod-Unterstützung, Spielaufzeichnung, Freundesystem und Microsoft-Anmeldung.
|
||||||
|
|
||||||
Aktueller Stand: Version 1.1.6
|
Aktueller Stand: Version 1.1.7
|
||||||
|
|
||||||
<img src="docs/aeromc.png" alt="AeroMC – Instanzliste mit Seitenleiste und Skin-Vorschau" width="820" />
|
<img src="docs/aeromc.png" alt="AeroMC – Instanzliste mit Seitenleiste und Skin-Vorschau" width="820" />
|
||||||
|
|
||||||
@@ -65,6 +65,8 @@ Der Launcher konzentriert sich auf den Alltag: Instanz anlegen, Konto auswählen
|
|||||||
- Microsoft-Login mit mehreren Konten
|
- Microsoft-Login mit mehreren Konten
|
||||||
- Eigenen Skin im Launcher hochladen, mit Live-Vorschau anpassen (klassisch/schlank) und zurücksetzen
|
- 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
|
- **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
|
||||||
- Skin-Hintergrund des aktiven Kontos (mehrere Posen, passende Armform, einstellbare Deckkraft, optional wechselnd)
|
- Skin-Hintergrund des aktiven Kontos (mehrere Posen, passende Armform, einstellbare Deckkraft, optional wechselnd)
|
||||||
- Welten, Ressourcenpakete, Datenpakete, Screenshots, Serverliste und Logdateien verwalten
|
- Welten, Ressourcenpakete, Datenpakete, Screenshots, Serverliste und Logdateien verwalten
|
||||||
- Serverliste mit Live-Status und Desktop-Verknüpfung pro Server (startet die Instanz und verbindet direkt)
|
- Serverliste mit Live-Status und Desktop-Verknüpfung pro Server (startet die Instanz und verbindet direkt)
|
||||||
@@ -162,7 +164,7 @@ Im Launcher:
|
|||||||
|
|
||||||
| Taste | Wirkung |
|
| Taste | Wirkung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Strg+F | Suche fokussieren |
|
| Strg+F | Suche öffnen (Lupe in der Werkzeugleiste), Esc leert und schließt sie |
|
||||||
| Strg+N | Neue Instanz anlegen |
|
| Strg+N | Neue Instanz anlegen |
|
||||||
| Strg+1 … Strg+5 | Schnellstart-Instanz starten |
|
| Strg+1 … Strg+5 | Schnellstart-Instanz starten |
|
||||||
| Entf | Markierte Instanz(en) löschen |
|
| Entf | Markierte Instanz(en) löschen |
|
||||||
@@ -279,6 +281,33 @@ Die Änderung wird direkt bei Mojang für das angemeldete Konto hinterlegt und g
|
|||||||
| M | Spiegeln ein/aus |
|
| M | Spiegeln ein/aus |
|
||||||
| L | Schicht wechseln |
|
| L | Schicht wechseln |
|
||||||
| Strg+Z / Strg+Y | Rückgängig / Wiederholen |
|
| 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
|
## Backup und Wiederherstellung
|
||||||
|
|
||||||
@@ -312,6 +341,7 @@ Speicherort: `Dokumente\AeroMC Launcher\Backups`.
|
|||||||
|
|
||||||
### Spiel
|
### Spiel
|
||||||
- Standard-RAM (min./max.)
|
- Standard-RAM (min./max.)
|
||||||
|
- Umhänge: AeroMC-Umhänge im Spiel anzeigen (gilt ab dem nächsten Start)
|
||||||
- Fenstergröße bzw. maximiert starten
|
- Fenstergröße bzw. maximiert starten
|
||||||
- Mods: vor jedem Start automatisch nach Updates suchen und installieren
|
- Mods: vor jedem Start automatisch nach Updates suchen und installieren
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package de.aeromc.capes;
|
||||||
|
|
||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AeroMC-Umhang-Agent.
|
||||||
|
*
|
||||||
|
* Wird vom Launcher per -javaagent an Minecraft angehängt (jede Version, jeder
|
||||||
|
* Loader). Er 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 wie bei einem offiziellen Umhang.
|
||||||
|
*
|
||||||
|
* Argument: Basisadresse des Umhang-Servers (…/wp-json/aeromc/v1).
|
||||||
|
*/
|
||||||
|
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) {
|
||||||
|
try {
|
||||||
|
String basis = argument != null && argument.startsWith("http") ? argument : Nachschlagen.STANDARD_BASIS;
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
*/
|
||||||
|
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 Umhang-Agent");
|
||||||
|
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
|
||||||
|
try (JarFile asm = new JarFile(a[2])) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
},
|
},
|
||||||
"projects": {
|
"projects": {
|
||||||
"C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj": {
|
"C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj": {
|
||||||
"version": "1.1.6",
|
"version": "0.0.0",
|
||||||
"restore": {
|
"restore": {
|
||||||
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
||||||
"projectName": "AeroMC-Setup",
|
"projectName": "AeroMC-Setup",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -14,11 +14,11 @@ using System.Reflection;
|
|||||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
|
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
|
||||||
[assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper. Alle Rechte vorbehalten.")]
|
[assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper. Alle Rechte vorbehalten.")]
|
||||||
[assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert AeroMC")]
|
[assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert AeroMC")]
|
||||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.1.6.0")]
|
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.1.7.0")]
|
||||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.1.6")]
|
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.1.7")]
|
||||||
[assembly: System.Reflection.AssemblyProductAttribute("AeroMC Setup")]
|
[assembly: System.Reflection.AssemblyProductAttribute("AeroMC Setup")]
|
||||||
[assembly: System.Reflection.AssemblyTitleAttribute("AeroMC Setup")]
|
[assembly: System.Reflection.AssemblyTitleAttribute("AeroMC Setup")]
|
||||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.1.6.0")]
|
[assembly: System.Reflection.AssemblyVersionAttribute("1.1.7.0")]
|
||||||
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
|
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
|
||||||
|
|
||||||
// Von der MSBuild WriteCodeFragment-Klasse generiert.
|
// Von der MSBuild WriteCodeFragment-Klasse generiert.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
5b6f28fb41c3f0e5fe29f9489cfc618a7d6593efb71d420030e4a1d4fdb4066e
|
acf80df582ccd59574a6332950a42ff0628e88f4288eb303a60e8577b37c7bef
|
||||||
|
|||||||
@@ -418,7 +418,7 @@
|
|||||||
"C:\\Users\\Viper\\.nuget\\packages\\": {}
|
"C:\\Users\\Viper\\.nuget\\packages\\": {}
|
||||||
},
|
},
|
||||||
"project": {
|
"project": {
|
||||||
"version": "1.1.6",
|
"version": "0.0.0",
|
||||||
"restore": {
|
"restore": {
|
||||||
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
||||||
"projectName": "AeroMC-Setup",
|
"projectName": "AeroMC-Setup",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dgSpecHash": "lJiGEWCFg6M=",
|
"dgSpecHash": "FnRoda2hVew=",
|
||||||
"success": true,
|
"success": true,
|
||||||
"projectFilePath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
"projectFilePath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj",
|
||||||
"expectedPackageFiles": [
|
"expectedPackageFiles": [
|
||||||
|
|||||||
@@ -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 {}
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"version": "1.1.6",
|
"version": "1.1.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"version": "1.1.6",
|
"version": "1.1.7",
|
||||||
"license": "SEE LICENSE IN LICENSE",
|
"license": "SEE LICENSE IN LICENSE",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xhayper/discord-rpc": "^1.3.4",
|
"@xhayper/discord-rpc": "^1.3.4",
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "aeromc",
|
"name": "aeromc",
|
||||||
"productName": "AeroMC",
|
"productName": "AeroMC",
|
||||||
"version": "1.1.6",
|
"version": "1.1.7",
|
||||||
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
|
"description": "AeroMC – Instanz-basierter Minecraft-Launcher (à la MultiMC/Prism)",
|
||||||
"homepage": "https://m-viper.de",
|
"homepage": "https://m-viper.de",
|
||||||
"launcherWebsite": "https://aeromc.viper.ipv64.net",
|
"launcherWebsite": "https://aeromc.viper.ipv64.net",
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
'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 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, '-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], { 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 });
|
||||||
|
}
|
||||||
Binary file not shown.
+2
-2
@@ -73,7 +73,7 @@
|
|||||||
:root:is([data-design="aero"],[data-design="lila"]) .tbtn:active{background:var(--surface2)}
|
:root:is([data-design="aero"],[data-design="lila"]) .tbtn:active{background:var(--surface2)}
|
||||||
:root:is([data-design="aero"],[data-design="lila"]) .tbtn svg,
|
:root:is([data-design="aero"],[data-design="lila"]) .tbtn svg,
|
||||||
:root:is([data-design="aero"],[data-design="lila"]) #btn-new svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-import svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-open-dir svg,
|
:root:is([data-design="aero"],[data-design="lila"]) #btn-new svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-import svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-open-dir svg,
|
||||||
:root:is([data-design="aero"],[data-design="lila"]) #btn-console svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-account svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-settings svg,
|
:root:is([data-design="aero"],[data-design="lila"]) #btn-search svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-console svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-account svg,:root:is([data-design="aero"],[data-design="lila"]) #btn-settings svg,
|
||||||
:root:is([data-design="aero"],[data-design="lila"]) .acct-av svg{color:var(--cyan-soft)}
|
:root:is([data-design="aero"],[data-design="lila"]) .acct-av svg{color:var(--cyan-soft)}
|
||||||
:root:is([data-design="aero"],[data-design="lila"]) .tbsep{background:var(--border)}
|
:root:is([data-design="aero"],[data-design="lila"]) .tbsep{background:var(--border)}
|
||||||
:root:is([data-design="aero"],[data-design="lila"]) .search-wrap input{border-radius:999px;padding:5px 12px;width:150px}
|
:root:is([data-design="aero"],[data-design="lila"]) .search-wrap input{border-radius:999px;padding:5px 12px;width:150px}
|
||||||
@@ -270,7 +270,7 @@
|
|||||||
:root[data-design="minecraft"] .tbtn{color:#fff;border-radius:0;text-shadow:1px 1px 0 #1a1a1a;image-rendering:auto}
|
:root[data-design="minecraft"] .tbtn{color:#fff;border-radius:0;text-shadow:1px 1px 0 #1a1a1a;image-rendering:auto}
|
||||||
:root[data-design="minecraft"] .tbtn:hover{background:rgba(0,0,0,.35);border-color:#fff}
|
:root[data-design="minecraft"] .tbtn:hover{background:rgba(0,0,0,.35);border-color:#fff}
|
||||||
:root[data-design="minecraft"] .tbtn:active{background:rgba(0,0,0,.5)}
|
:root[data-design="minecraft"] .tbtn:active{background:rgba(0,0,0,.5)}
|
||||||
:root[data-design="minecraft"] .tbtn svg,:root[data-design="minecraft"] #btn-console svg,:root[data-design="minecraft"] .acct-av svg{color:#e6e6e6}
|
:root[data-design="minecraft"] #btn-search svg,:root[data-design="minecraft"] .tbtn svg,:root[data-design="minecraft"] #btn-console svg,:root[data-design="minecraft"] .acct-av svg{color:#e6e6e6}
|
||||||
:root[data-design="minecraft"] #btn-new svg,:root[data-design="minecraft"] #btn-account svg{color:#7fd858}
|
:root[data-design="minecraft"] #btn-new svg,:root[data-design="minecraft"] #btn-account svg{color:#7fd858}
|
||||||
:root[data-design="minecraft"] #btn-import svg{color:#6db8ff}
|
:root[data-design="minecraft"] #btn-import svg{color:#6db8ff}
|
||||||
:root[data-design="minecraft"] #btn-open-dir svg{color:#f2c94c}
|
:root[data-design="minecraft"] #btn-open-dir svg{color:#f2c94c}
|
||||||
|
|||||||
@@ -200,7 +200,32 @@ async function sendReport(daten) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- AeroMC-Umhänge ----------
|
||||||
|
// Hochladen/Entfernen nur angemeldet; Abrufen ist öffentlich (auch für den Mod)
|
||||||
|
function uploadCape(uuid, image) {
|
||||||
|
return anfrage('/cape', { method: 'POST', body: { uuid, image } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeCape(uuid) {
|
||||||
|
return anfrage('/cape/remove', { method: 'POST', body: { uuid } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getCapes(uuids) {
|
||||||
|
try {
|
||||||
|
const liste = [...new Set((uuids || []).map((u) => String(u).replace(/-/g, '').toLowerCase()))].filter(Boolean);
|
||||||
|
if (!liste.length) return { ok: true, capes: {} };
|
||||||
|
const { res, daten } = await roheAnfrage('/capes', { method: 'GET', body: { uuids: liste.join(',') } });
|
||||||
|
// ältere Plugin-Fassung ohne Umhänge
|
||||||
|
if (res.status === 404) return { ok: false, fehlt: true, message: 'Der Server kennt noch keine Umhänge (Plugin „AeroMC Freunde“ 1.5.0 nötig).' };
|
||||||
|
if (!res.ok) return { ok: false, message: (daten && daten.message) || `Fehler ${res.status}` };
|
||||||
|
return { ok: true, capes: (daten && daten.capes) || {} };
|
||||||
|
} catch (err) {
|
||||||
|
return { ok: false, message: 'Umhang-Server nicht erreichbar: ' + err.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
BASIS_URL, uploadCape, removeCape, getCapes,
|
||||||
heartbeat, getFriends, getRequests, getSentRequests, sendRequest, respond, cancelRequest, removeFriend,
|
heartbeat, getFriends, getRequests, getSentRequests, sendRequest, respond, cancelRequest, removeFriend,
|
||||||
sendInvite, getInvites, dismissInvite, createShare, getShare, setzeZugangsHelfer,
|
sendInvite, getInvites, dismissInvite, createShare, getShare, setzeZugangsHelfer,
|
||||||
sendMessage, getMessages, getUnreadMessages, markMessagesRead, sendReport,
|
sendMessage, getMessages, getUnreadMessages, markMessagesRead, sendReport,
|
||||||
|
|||||||
+62
-4
@@ -27,8 +27,9 @@
|
|||||||
<button id="btn-stats" class="tbtn" data-ic="chart" title="Spielzeit-Statistik">Statistik</button>
|
<button id="btn-stats" class="tbtn" data-ic="chart" title="Spielzeit-Statistik">Statistik</button>
|
||||||
<button id="btn-friends" class="tbtn" data-ic="users" title="Freunde (ingame: Strg+Alt+F zeigt, wer online ist)">Freunde<span id="friends-badge" class="tb-badge hidden"></span></button>
|
<button id="btn-friends" class="tbtn" data-ic="users" title="Freunde (ingame: Strg+Alt+F zeigt, wer online ist)">Freunde<span id="friends-badge" class="tb-badge hidden"></span></button>
|
||||||
<span class="tbgrow"></span>
|
<span class="tbgrow"></span>
|
||||||
<div class="search-wrap">
|
<div id="search-wrap" class="search-wrap">
|
||||||
<input id="search" type="text" placeholder="Suchen… (Strg+F)" autocomplete="off" title="Strg+F fokussiert die Suche" />
|
<button id="btn-search" class="tbtn tb-lupe" data-ic="search" title="Instanzen suchen (Strg+F)"></button>
|
||||||
|
<input id="search" type="text" placeholder="Suchen… (Esc schließt)" autocomplete="off" title="Instanzen nach Name, Version oder Gruppe filtern" />
|
||||||
</div>
|
</div>
|
||||||
<button id="btn-account" class="tbtn acct-btn" title="Microsoft-Konto"><span id="account-av" class="acct-av"></span><span id="account-name">Konten</span></button>
|
<button id="btn-account" class="tbtn acct-btn" title="Microsoft-Konto"><span id="account-av" class="acct-av"></span><span id="account-name">Konten</span></button>
|
||||||
<button id="btn-settings" class="tbtn" data-ic="settings" title="Einstellungen">Optionen</button>
|
<button id="btn-settings" class="tbtn" data-ic="settings" title="Einstellungen">Optionen</button>
|
||||||
@@ -445,6 +446,14 @@
|
|||||||
</label>
|
</label>
|
||||||
<small class="hint">Gilt für Mods aus Modrinth. Dauert die Suche länger als 20 Sekunden, startet Minecraft ohne Update. Abgeschaltete Mods bleiben abgeschaltet.</small>
|
<small class="hint">Gilt für Mods aus Modrinth. Dauert die Suche länger als 20 Sekunden, startet Minecraft ohne Update. Abgeschaltete Mods bleiben abgeschaltet.</small>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-card">
|
||||||
|
<h5>Umhänge</h5>
|
||||||
|
<label class="checkbox-field">
|
||||||
|
<input id="s-aeromc-capes" type="checkbox" /> <span>AeroMC-Umhänge im Spiel anzeigen</span>
|
||||||
|
</label>
|
||||||
|
<small class="hint">Zeigt deinen eigenen Umhang und die anderer AeroMC-Spieler – in jeder Minecraft-Version und mit jedem Loader, ohne Mod. Gestalten im Skin-Fenster unter „Umhang“. Gilt ab dem nächsten Start.</small>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1249,6 +1258,26 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<p id="skin-status" class="hint"></p>
|
<p id="skin-status" class="hint"></p>
|
||||||
|
<div class="skin-umhang">
|
||||||
|
<div class="skin-label">Umhang</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<label class="field">
|
||||||
|
<span>Offizieller Umhang <small class="opt">(im Spiel für alle sichtbar)</small></span>
|
||||||
|
<select id="skin-cape-offiziell" title="Nur Umhänge, die dein Konto bei Mojang besitzt">
|
||||||
|
<option value="">Kein Umhang</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div class="field">
|
||||||
|
<span>AeroMC-Umhang <small class="opt">(eigener, auch HD)</small></span>
|
||||||
|
<div class="skin-umhang-aero">
|
||||||
|
<span id="skin-cape-aero" class="hint">–</span>
|
||||||
|
<button id="skin-cape-edit" class="btn btn-mini" title="Im Skin-Editor einen eigenen Umhang gestalten">Gestalten…</button>
|
||||||
|
<button id="skin-cape-remove" class="btn btn-mini btn-danger hidden">Entfernen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small id="skin-cape-status" class="hint">Den AeroMC-Umhang sehen im Spiel alle, die AeroMC nutzen oder den Mod „AeroMC Umhänge“ (Fabric/Quilt) installiert haben – dort hat er Vorrang vor dem offiziellen.</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-foot">
|
<div class="modal-foot">
|
||||||
<button id="skin-reset" class="btn btn-danger">Auf Standard zurücksetzen</button>
|
<button id="skin-reset" class="btn btn-danger">Auf Standard zurücksetzen</button>
|
||||||
@@ -1268,6 +1297,7 @@
|
|||||||
<div class="se-modus">
|
<div class="se-modus">
|
||||||
<button class="se-modus-btn on" data-se-modus="2d" title="Auf der aufgeklappten Vorlage malen">2D-Vorlage</button>
|
<button class="se-modus-btn on" data-se-modus="2d" title="Auf der aufgeklappten Vorlage malen">2D-Vorlage</button>
|
||||||
<button class="se-modus-btn" data-se-modus="3d" title="Direkt auf der drehbaren Figur malen">3D-Figur</button>
|
<button class="se-modus-btn" data-se-modus="3d" title="Direkt auf der drehbaren Figur malen">3D-Figur</button>
|
||||||
|
<button class="se-modus-btn" data-se-modus="umhang" title="Eigenen Umhang (auch HD) gestalten">Umhang</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="modal-close" data-close>✕</button>
|
<button class="modal-close" data-close>✕</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1290,7 +1320,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="se-zuletzt" class="se-zuletzt"></div>
|
<div id="se-zuletzt" class="se-zuletzt"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="se-gruppe">
|
<div id="se-gruppe-ebene" class="se-gruppe">
|
||||||
<div class="se-titel">Ebene</div>
|
<div class="se-titel">Ebene</div>
|
||||||
<div class="se-ebenen">
|
<div class="se-ebenen">
|
||||||
<button class="se-ebene on" data-se-schicht="0" title="Die eigentliche Figur (L wechselt)">Grundschicht</button>
|
<button class="se-ebene on" data-se-schicht="0" title="Die eigentliche Figur (L wechselt)">Grundschicht</button>
|
||||||
@@ -1300,6 +1330,30 @@
|
|||||||
<label class="se-check"><input id="se-durch" type="checkbox" checked /> Grundschicht durchscheinen</label>
|
<label class="se-check"><input id="se-durch" type="checkbox" checked /> Grundschicht durchscheinen</label>
|
||||||
<button id="se-leeren" class="btn btn-mini">2. Schicht leeren</button>
|
<button id="se-leeren" class="btn btn-mini">2. Schicht leeren</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="se-gruppe-umhang" class="se-gruppe hidden">
|
||||||
|
<div class="se-titel">Umhang</div>
|
||||||
|
<label class="se-feld"><span>Größe</span>
|
||||||
|
<select id="se-u-groesse" title="Größere Umhänge (HD) erlauben feinere Motive">
|
||||||
|
<option value="64">Normal (64×32)</option>
|
||||||
|
<option value="128">HD 128×64</option>
|
||||||
|
<option value="256">HD 256×128</option>
|
||||||
|
<option value="512">HD 512×256</option>
|
||||||
|
<option value="1024">HD 1024×512</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="se-feld"><span>Pinselgröße</span>
|
||||||
|
<select id="se-u-pinsel">
|
||||||
|
<option value="1">1 Pixel</option>
|
||||||
|
<option value="2">2 Pixel</option>
|
||||||
|
<option value="4">4 Pixel</option>
|
||||||
|
<option value="8">8 Pixel</option>
|
||||||
|
<option value="16">16 Pixel</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button id="se-u-bild" class="btn btn-mini" title="Ein beliebiges Bild (PNG, JPG …) auf die Außenseite setzen">Bild einsetzen…</button>
|
||||||
|
<label class="se-check" title="Dieselbe Datei trägt auch die Elytra"><input id="se-u-elytra" type="checkbox" /> Als Elytra zeigen</label>
|
||||||
|
<button id="se-u-einpassen" class="btn btn-mini" title="Zoom zurücksetzen (Taste 0)">Ansicht einpassen</button>
|
||||||
|
</div>
|
||||||
<div class="se-gruppe">
|
<div class="se-gruppe">
|
||||||
<div class="se-titel">Optionen</div>
|
<div class="se-titel">Optionen</div>
|
||||||
<label class="se-check" title="Malt gleichzeitig auf der anderen Körperseite (M)"><input id="se-spiegeln" type="checkbox" /> Spiegeln (links ↔ rechts)</label>
|
<label class="se-check" title="Malt gleichzeitig auf der anderen Körperseite (M)"><input id="se-spiegeln" type="checkbox" /> Spiegeln (links ↔ rechts)</label>
|
||||||
@@ -1319,6 +1373,10 @@
|
|||||||
<div id="se-buehne" class="se-buehne">
|
<div id="se-buehne" class="se-buehne">
|
||||||
<div id="se-2d" class="se-ansicht se-2d"><canvas id="se-2d-canvas"></canvas></div>
|
<div id="se-2d" class="se-ansicht se-2d"><canvas id="se-2d-canvas"></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- gerade nicht gezeigte Ansichten (je nach Modus) -->
|
||||||
|
<div id="se-ablage" class="se-ablage">
|
||||||
|
<div id="se-u2d" class="se-ansicht se-u2d"><canvas id="se-u2d-canvas"></canvas></div>
|
||||||
|
</div>
|
||||||
<div id="se-info" class="se-info hint"></div>
|
<div id="se-info" class="se-info hint"></div>
|
||||||
</section>
|
</section>
|
||||||
<aside class="se-seite">
|
<aside class="se-seite">
|
||||||
@@ -1326,7 +1384,7 @@
|
|||||||
<div id="se-mini" class="se-mini">
|
<div id="se-mini" class="se-mini">
|
||||||
<div id="se-3d" class="se-ansicht se-3d"><canvas id="se-3d-canvas"></canvas><span id="se-3d-leer" class="hint se-3d-leer">wird geladen …</span></div>
|
<div id="se-3d" class="se-ansicht se-3d"><canvas id="se-3d-canvas"></canvas><span id="se-3d-leer" class="hint se-3d-leer">wird geladen …</span></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint se-tipp">Beide Ansichten sind bemalbar. Die <b>2. Schicht</b> liegt wie eine Hülle über der Figur – lässt man Pixel darin frei, wirken Haare, Kapuzen oder Ärmel plastisch.</p>
|
<p id="se-tipp" class="hint se-tipp">Beide Ansichten sind bemalbar. Die <b>2. Schicht</b> liegt wie eine Hülle über der Figur – lässt man Pixel darin frei, wirken Haare, Kapuzen oder Ärmel plastisch.</p>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-foot se-fuss">
|
<div class="modal-foot se-fuss">
|
||||||
|
|||||||
@@ -344,6 +344,8 @@ function buildArgs(vj, ctx) {
|
|||||||
// RAM
|
// RAM
|
||||||
jvm = ['-Xms' + ctx.minMemMb + 'M', '-Xmx' + ctx.maxMemMb + 'M', ...jvm];
|
jvm = ['-Xms' + ctx.minMemMb + 'M', '-Xmx' + ctx.maxMemMb + 'M', ...jvm];
|
||||||
if (ctx.extraArgs) jvm = [...ctx.extraArgs.split(' ').filter(Boolean), ...jvm];
|
if (ctx.extraArgs) jvm = [...ctx.extraArgs.split(' ').filter(Boolean), ...jvm];
|
||||||
|
// AeroMC-Umhang-Agent (zeigt eigene Umhänge im Spiel, siehe agent/)
|
||||||
|
if (ctx.javaAgent) jvm = ['-javaagent:' + ctx.javaAgent, ...jvm];
|
||||||
|
|
||||||
return [...jvm, vj.mainClass, ...game];
|
return [...jvm, vj.mainClass, ...game];
|
||||||
}
|
}
|
||||||
@@ -528,6 +530,7 @@ async function prepareAndLaunch(opts, onProgress) {
|
|||||||
minMemMb: opts.minMemMb || 2048,
|
minMemMb: opts.minMemMb || 2048,
|
||||||
maxMemMb: opts.maxMemMb || 4096,
|
maxMemMb: opts.maxMemMb || 4096,
|
||||||
extraArgs: opts.extraArgs || '',
|
extraArgs: opts.extraArgs || '',
|
||||||
|
javaAgent: opts.javaAgent || '',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fenstergröße bzw. maximiert starten (Minecraft-Argumente)
|
// Fenstergröße bzw. maximiert starten (Minecraft-Argumente)
|
||||||
|
|||||||
+171
@@ -1864,6 +1864,176 @@ ipcMain.handle('skin:currentData', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------- Umhänge (offiziell von Mojang + eigene AeroMC-Umhänge) ----------
|
||||||
|
// Bilder immer als data:-Adresse in die Oberfläche (fremde Server schicken
|
||||||
|
// keine CORS-Kopfzeilen – die 3D-Ansicht dürfte sie sonst nicht laden)
|
||||||
|
async function bildAlsDataUri(url) {
|
||||||
|
const res = await fetch(url, { headers: { 'User-Agent': 'AeroMC-Launcher' } });
|
||||||
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||||
|
const daten = Buffer.from(await res.arrayBuffer());
|
||||||
|
if (daten.length > 6 * 1024 * 1024) throw new Error('Bild zu groß');
|
||||||
|
return 'data:image/png;base64,' + daten.toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
// PNG mit Seitenverhältnis 2:1 (64×32 … 1024×512) – wie der Server es verlangt
|
||||||
|
function umhangPngAusBase64(base64) {
|
||||||
|
const daten = Buffer.from(String(base64 || ''), 'base64');
|
||||||
|
if (daten.length < 33 || !daten.subarray(0, 8).equals(PNG_KENNUNG)) return null;
|
||||||
|
const breite = daten.readUInt32BE(16);
|
||||||
|
const hoehe = daten.readUInt32BE(20);
|
||||||
|
if (![64, 128, 256, 512, 1024].includes(breite) || breite !== 2 * hoehe) return null;
|
||||||
|
return daten;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Umhang eines beliebigen Spielers fürs Anzeigen: eigener AeroMC-Umhang hat
|
||||||
|
// Vorrang, sonst der offizielle aus Mojangs Profil. 5 Minuten gemerkt.
|
||||||
|
const umhangCache = new Map(); // uuid -> { dataUri, quelle, zeit }
|
||||||
|
async function umhangFuer(uuid) {
|
||||||
|
const id = String(uuid || '').replace(/-/g, '').toLowerCase();
|
||||||
|
if (!/^[0-9a-f]{32}$/.test(id)) return null;
|
||||||
|
const gemerkt = umhangCache.get(id);
|
||||||
|
if (gemerkt && Date.now() - gemerkt.zeit < 5 * 60 * 1000) return gemerkt;
|
||||||
|
let ergebnis = { dataUri: null, quelle: null, zeit: Date.now() };
|
||||||
|
try {
|
||||||
|
const aero = await friends.getCapes([id]);
|
||||||
|
const eintrag = aero.ok && aero.capes && aero.capes[id];
|
||||||
|
if (eintrag && eintrag.url) ergebnis = { dataUri: await bildAlsDataUri(eintrag.url), quelle: 'aeromc', zeit: Date.now() };
|
||||||
|
} catch { /* weiter mit Mojang */ }
|
||||||
|
if (!ergebnis.dataUri) {
|
||||||
|
try {
|
||||||
|
const res = await fetch('https://sessionserver.mojang.com/session/minecraft/profile/' + id, { headers: { 'User-Agent': 'AeroMC-Launcher' } });
|
||||||
|
if (res.ok) {
|
||||||
|
const profil = await res.json();
|
||||||
|
const eig = (profil.properties || []).find((p) => p.name === 'textures');
|
||||||
|
const tex = eig ? JSON.parse(Buffer.from(eig.value, 'base64').toString('utf8')) : {};
|
||||||
|
const cape = tex.textures && tex.textures.CAPE;
|
||||||
|
if (cape && cape.url) ergebnis = { dataUri: await bildAlsDataUri(skins.aufHttps(cape.url)), quelle: 'mojang', zeit: Date.now() };
|
||||||
|
}
|
||||||
|
} catch { /* kein Umhang */ }
|
||||||
|
}
|
||||||
|
umhangCache.set(id, ergebnis);
|
||||||
|
return ergebnis;
|
||||||
|
}
|
||||||
|
ipcMain.handle('cape:forUuid', async (_e, uuid) => {
|
||||||
|
const r = await umhangFuer(uuid);
|
||||||
|
return r && r.dataUri ? { dataUri: r.dataUri, quelle: r.quelle } : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Übersicht fürs Skin-Fenster: offizielle Umhänge + eigener AeroMC-Umhang
|
||||||
|
ipcMain.handle('cape:info', async () => {
|
||||||
|
const konto = aktivesFreundeKonto();
|
||||||
|
const token = await aktivesToken();
|
||||||
|
if (!konto || !token) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||||
|
const ergebnis = { ok: true, name: konto.username, offiziell: [], aeromc: null, aeromcFehler: '' };
|
||||||
|
try {
|
||||||
|
const p = await skins.holeProfil(token);
|
||||||
|
if (p.ok) ergebnis.offiziell = p.capes || [];
|
||||||
|
} catch { /* ohne offizielle Liste weiter */ }
|
||||||
|
const aero = await friends.getCapes([konto.uuid]);
|
||||||
|
if (aero.ok) ergebnis.aeromc = aero.capes[String(konto.uuid).replace(/-/g, '').toLowerCase()] || null;
|
||||||
|
else { ergebnis.aeromcFehler = aero.message; ergebnis.aeromcFehlt = !!aero.fehlt; }
|
||||||
|
return ergebnis;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('cape:setOfficial', async (_e, capeId) => {
|
||||||
|
const token = await aktivesToken();
|
||||||
|
if (!token) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||||
|
try {
|
||||||
|
const res = await skins.setzeUmhang(token, capeId || null);
|
||||||
|
if (res.ok) { umhangCache.clear(); broadcast('skin:changed', { quelle: 'umhang' }); }
|
||||||
|
return res;
|
||||||
|
} catch (err) { return { ok: false, message: 'Umhang konnte nicht gesetzt werden: ' + err.message }; }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Der Umhang-Agent muss als echte Datei vorliegen (Java liest nicht aus app.asar):
|
||||||
|
// einmal nach userData kopieren, Dateiname mit Größe, damit ein Update ihn ersetzt.
|
||||||
|
// Rückgabe: "pfad=serveradresse" für -javaagent, oder '' wenn es nicht klappt.
|
||||||
|
function umhangAgent() {
|
||||||
|
try {
|
||||||
|
const quelle = path.join(__dirname, 'assets', 'agent', 'aeromc-capes.jar');
|
||||||
|
const groesse = fs.statSync(quelle).size;
|
||||||
|
const ordner = path.join(app.getPath('userData'), 'agent');
|
||||||
|
const ziel = path.join(ordner, `aeromc-capes-${groesse}.jar`);
|
||||||
|
if (!fs.existsSync(ziel) || fs.statSync(ziel).size !== groesse) {
|
||||||
|
fs.mkdirSync(ordner, { recursive: true });
|
||||||
|
fs.writeFileSync(ziel, fs.readFileSync(quelle));
|
||||||
|
for (const alt of fs.readdirSync(ordner)) {
|
||||||
|
if (alt !== path.basename(ziel) && /^aeromc-capes-\d+\.jar$/.test(alt)) { try { fs.unlinkSync(path.join(ordner, alt)); } catch { /* noch in Benutzung */ } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${ziel}=${friends.BASIS_URL}`;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Umhang-Agent nicht verfügbar:', err.message);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// verständliche Meldung, wenn der Server das neue Plugin noch nicht hat
|
||||||
|
function umhangFehler(res) {
|
||||||
|
if (res && res.code === 'rest_no_route') return { ok: false, message: 'Der Server kennt noch keine Umhänge (Plugin „AeroMC Freunde“ 1.5.0 nötig).' };
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('cape:upload', async (_e, base64) => {
|
||||||
|
const konto = aktivesFreundeKonto();
|
||||||
|
if (!konto) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||||
|
if (!umhangPngAusBase64(base64)) return { ok: false, message: 'Ungültiger Umhang (PNG mit 64×32 bis 1024×512 Pixeln nötig).' };
|
||||||
|
const res = await friends.uploadCape(konto.uuid, base64);
|
||||||
|
if (res.ok) { umhangCache.clear(); broadcast('skin:changed', { quelle: 'umhang' }); }
|
||||||
|
return res.ok ? { ok: true, cape: res.data && res.data.cape } : umhangFehler(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('cape:remove', async () => {
|
||||||
|
const konto = aktivesFreundeKonto();
|
||||||
|
if (!konto) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||||
|
const res = await friends.removeCape(konto.uuid);
|
||||||
|
if (res.ok) { umhangCache.clear(); broadcast('skin:changed', { quelle: 'umhang' }); }
|
||||||
|
return res.ok ? res : umhangFehler(res);
|
||||||
|
});
|
||||||
|
|
||||||
|
// für den Editor: eigener Umhang, sonst der getragene offizielle
|
||||||
|
ipcMain.handle('cape:currentData', async () => {
|
||||||
|
const konto = aktivesFreundeKonto();
|
||||||
|
if (!konto) return { ok: false, message: 'Kein angemeldetes Konto.' };
|
||||||
|
umhangCache.delete(String(konto.uuid).replace(/-/g, '').toLowerCase());
|
||||||
|
const r = await umhangFuer(konto.uuid);
|
||||||
|
if (!r || !r.dataUri) return { ok: false, keiner: true, message: 'Dein Konto hat noch keinen Umhang.' };
|
||||||
|
return { ok: true, dataUri: r.dataUri, quelle: r.quelle };
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bild wählen: art 'umhang' = fertige Umhang-Datei (PNG), 'bild' = beliebiges Motiv zum Einsetzen
|
||||||
|
ipcMain.handle('cape:chooseFile', async (e, art) => {
|
||||||
|
const motiv = art === 'bild';
|
||||||
|
const res = await dialog.showOpenDialog(BrowserWindow.fromWebContents(e.sender) || mainWindow, {
|
||||||
|
title: motiv ? 'Bild für den Umhang wählen' : 'Umhang-Datei öffnen',
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: motiv
|
||||||
|
? [{ name: 'Bilder', extensions: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'] }]
|
||||||
|
: [{ name: 'PNG-Bild', extensions: ['png'] }],
|
||||||
|
});
|
||||||
|
if (res.canceled || !res.filePaths.length) return { ok: false, canceled: true };
|
||||||
|
const pfad = res.filePaths[0];
|
||||||
|
try {
|
||||||
|
const st = fs.statSync(pfad);
|
||||||
|
if (st.size > 15 * 1024 * 1024) return { ok: false, message: 'Die Datei ist zu groß (über 15 MB).' };
|
||||||
|
const endung = path.extname(pfad).slice(1).toLowerCase().replace('jpg', 'jpeg');
|
||||||
|
return { ok: true, pfad, dataUri: `data:image/${endung || 'png'};base64,` + fs.readFileSync(pfad).toString('base64') };
|
||||||
|
} catch (err) { return { ok: false, message: 'Datei nicht lesbar: ' + err.message }; }
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('cape:saveData', async (e, base64) => {
|
||||||
|
const daten = umhangPngAusBase64(base64);
|
||||||
|
if (!daten) return { ok: false, message: 'Ungültige Umhang-Daten.' };
|
||||||
|
const res = await dialog.showSaveDialog(BrowserWindow.fromWebContents(e.sender) || mainWindow, {
|
||||||
|
title: 'Umhang speichern',
|
||||||
|
defaultPath: path.join(app.getPath('pictures'), 'umhang.png'),
|
||||||
|
filters: [{ name: 'PNG-Bild', extensions: ['png'] }],
|
||||||
|
});
|
||||||
|
if (res.canceled || !res.filePath) return { ok: false, canceled: true };
|
||||||
|
try { fs.writeFileSync(res.filePath, daten); return { ok: true, pfad: res.filePath }; }
|
||||||
|
catch (err) { return { ok: false, message: 'Speichern fehlgeschlagen: ' + err.message }; }
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('skin:readData', (_e, dateiPfad) => {
|
ipcMain.handle('skin:readData', (_e, dateiPfad) => {
|
||||||
const geprueft = skins.pruefeDatei(dateiPfad);
|
const geprueft = skins.pruefeDatei(dateiPfad);
|
||||||
if (!geprueft.ok) return geprueft;
|
if (!geprueft.ok) return geprueft;
|
||||||
@@ -3602,6 +3772,7 @@ ipcMain.handle('instances:launch', async (_e, id, accountId, launchOpts) => {
|
|||||||
if (settings.autoModUpdates) modUpdates = await modUpdatesVorDemStart(id).catch(() => null);
|
if (settings.autoModUpdates) modUpdates = await modUpdatesVorDemStart(id).catch(() => null);
|
||||||
|
|
||||||
const opts = {
|
const opts = {
|
||||||
|
javaAgent: settings.aeromcCapes === false ? '' : umhangAgent(),
|
||||||
versionId: inst.minecraft.version,
|
versionId: inst.minecraft.version,
|
||||||
loader: inst.minecraft.loader || 'vanilla',
|
loader: inst.minecraft.loader || 'vanilla',
|
||||||
loaderVersion: inst.minecraft.loaderVersion || '',
|
loaderVersion: inst.minecraft.loaderVersion || '',
|
||||||
|
|||||||
@@ -159,6 +159,14 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
skinReadData: (pfad) => ipcRenderer.invoke('skin:readData', pfad),
|
skinReadData: (pfad) => ipcRenderer.invoke('skin:readData', pfad),
|
||||||
skinSaveData: (base64) => ipcRenderer.invoke('skin:saveData', base64),
|
skinSaveData: (base64) => ipcRenderer.invoke('skin:saveData', base64),
|
||||||
skinApplyData: (base64, variante) => ipcRenderer.invoke('skin:applyData', base64, variante),
|
skinApplyData: (base64, variante) => ipcRenderer.invoke('skin:applyData', base64, variante),
|
||||||
|
capeInfo: () => ipcRenderer.invoke('cape:info'),
|
||||||
|
capeForUuid: (uuid) => ipcRenderer.invoke('cape:forUuid', uuid),
|
||||||
|
capeSetOfficial: (id) => ipcRenderer.invoke('cape:setOfficial', id),
|
||||||
|
capeUpload: (base64) => ipcRenderer.invoke('cape:upload', base64),
|
||||||
|
capeRemove: () => ipcRenderer.invoke('cape:remove'),
|
||||||
|
capeCurrentData: () => ipcRenderer.invoke('cape:currentData'),
|
||||||
|
capeChooseFile: (art) => ipcRenderer.invoke('cape:chooseFile', art),
|
||||||
|
capeSaveData: (base64) => ipcRenderer.invoke('cape:saveData', base64),
|
||||||
authRemove: (id) => ipcRenderer.invoke('auth:remove', id),
|
authRemove: (id) => ipcRenderer.invoke('auth:remove', id),
|
||||||
authLogin: () => ipcRenderer.invoke('auth:login'),
|
authLogin: () => ipcRenderer.invoke('auth:login'),
|
||||||
authLogout: () => ipcRenderer.invoke('auth:logout'),
|
authLogout: () => ipcRenderer.invoke('auth:logout'),
|
||||||
|
|||||||
+105
-9
@@ -1564,7 +1564,9 @@ async function applySkinBackground(uuidOverride) {
|
|||||||
// schmaler Alex-Skin mit breiten Armen falsch aus (grauer Streifen am Arm)
|
// schmaler Alex-Skin mit breiten Armen falsch aus (grauer Streifen am Arm)
|
||||||
let modell = s.bgSkinModel === 'slim' || s.bgSkinModel === 'classic' ? s.bgSkinModel : '';
|
let modell = s.bgSkinModel === 'slim' || s.bgSkinModel === 'classic' ? s.bgSkinModel : '';
|
||||||
if (!modell) { try { modell = await window.api.skinModelFor(uuid); } catch { modell = 'classic'; } }
|
if (!modell) { try { modell = await window.api.skinModelFor(uuid); } catch { modell = 'classic'; } }
|
||||||
const local = await renderSkin3D(`https://crafthead.net/skin/${uuid}`, s.bgSkinPose || 'walking', { modell });
|
let umhang = null;
|
||||||
|
try { const r = await window.api.capeForUuid(uuid); umhang = r ? r.dataUri : null; } catch { /* ohne Umhang */ }
|
||||||
|
const local = await renderSkin3D(`https://crafthead.net/skin/${uuid}`, s.bgSkinPose || 'walking', { modell, umhang });
|
||||||
if (local) { root.style.setProperty('--bg-skin', `url("${local}")`); return; }
|
if (local) { root.style.setProperty('--bg-skin', `url("${local}")`); return; }
|
||||||
|
|
||||||
// 2) Rückfall: externe Render-Dienste
|
// 2) Rückfall: externe Render-Dienste
|
||||||
@@ -1711,6 +1713,7 @@ async function renderSkin3D(quelle, poseKey, opts = {}) {
|
|||||||
viewer.autoRotate = false;
|
viewer.autoRotate = false;
|
||||||
viewer.animation = null;
|
viewer.animation = null;
|
||||||
pose.apply(viewer.playerObject);
|
pose.apply(viewer.playerObject);
|
||||||
|
if (opts.umhang) { try { await viewer.loadCape(opts.umhang); } catch { /* ohne Umhang */ } }
|
||||||
|
|
||||||
viewer.render();
|
viewer.render();
|
||||||
const data = canvas.toDataURL('image/png');
|
const data = canvas.toDataURL('image/png');
|
||||||
@@ -1740,6 +1743,7 @@ const ICONS_SVG = {
|
|||||||
export: '<path d="M12 21V9m0 0l-4 4m4-4l4 4M4 4h16"/>',
|
export: '<path d="M12 21V9m0 0l-4 4m4-4l4 4M4 4h16"/>',
|
||||||
trash: '<path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/>',
|
trash: '<path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/>',
|
||||||
chart: '<path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/>',
|
chart: '<path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/>',
|
||||||
|
search: '<circle cx="11" cy="11" r="7"/><path d="M20 20l-4.2-4.2"/>',
|
||||||
};
|
};
|
||||||
function svgIcon(name) {
|
function svgIcon(name) {
|
||||||
const p = ICONS_SVG[name];
|
const p = ICONS_SVG[name];
|
||||||
@@ -1909,6 +1913,17 @@ function buildCard(inst) {
|
|||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Suchfeld in der Werkzeugleiste: standardmäßig nur eine Lupe
|
||||||
|
function sucheOeffnen() {
|
||||||
|
el('search-wrap').classList.add('offen');
|
||||||
|
el('search').focus();
|
||||||
|
el('search').select();
|
||||||
|
}
|
||||||
|
function sucheSchliessen() {
|
||||||
|
el('search-wrap').classList.remove('offen');
|
||||||
|
if (document.activeElement === el('search')) el('search').blur();
|
||||||
|
}
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
const q = el('search').value.trim().toLowerCase();
|
const q = el('search').value.trim().toLowerCase();
|
||||||
const grid = el('grid');
|
const grid = el('grid');
|
||||||
@@ -2607,7 +2622,7 @@ function schliesseSkinViewer(bildId) {
|
|||||||
* Mausrad zoomt -- kommt von skinview3d selbst). Klappt das Rendern nicht,
|
* Mausrad zoomt -- kommt von skinview3d selbst). Klappt das Rendern nicht,
|
||||||
* wird stattdessen ein Hinweistext gezeigt.
|
* wird stattdessen ein Hinweistext gezeigt.
|
||||||
*/
|
*/
|
||||||
async function zeigeSkinFigur(bildId, leerId, textur, leerText) {
|
async function zeigeSkinFigur(bildId, leerId, textur, leerText, umhang) {
|
||||||
schliesseSkinViewer(bildId);
|
schliesseSkinViewer(bildId);
|
||||||
const canvas = el(bildId);
|
const canvas = el(bildId);
|
||||||
const leer = el(leerId);
|
const leer = el(leerId);
|
||||||
@@ -2644,6 +2659,7 @@ async function zeigeSkinFigur(bildId, leerId, textur, leerText) {
|
|||||||
viewer.autoRotate = false;
|
viewer.autoRotate = false;
|
||||||
viewer.animation = null;
|
viewer.animation = null;
|
||||||
pose.apply(viewer.playerObject);
|
pose.apply(viewer.playerObject);
|
||||||
|
if (umhang) { try { await viewer.loadCape(umhang); } catch { /* ohne Umhang weiter */ } }
|
||||||
viewer.controls.enableRotate = true;
|
viewer.controls.enableRotate = true;
|
||||||
viewer.controls.enableZoom = true;
|
viewer.controls.enableZoom = true;
|
||||||
viewer.controls.enablePan = false;
|
viewer.controls.enablePan = false;
|
||||||
@@ -2679,14 +2695,78 @@ async function openSkin() {
|
|||||||
// Armform zuerst setzen – davon hängt ab, wie die Figur gerendert wird
|
// Armform zuerst setzen – davon hängt ab, wie die Figur gerendert wird
|
||||||
if (p.variante) el('skin-variante').value = p.variante === 'slim' ? 'slim' : 'classic';
|
if (p.variante) el('skin-variante').value = p.variante === 'slim' ? 'slim' : 'classic';
|
||||||
skinTexturAktuell = p.skinUrl || '';
|
skinTexturAktuell = p.skinUrl || '';
|
||||||
|
await ladeUmhangInfo();
|
||||||
await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell,
|
await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell,
|
||||||
'Standard-Skin (keine eigene Datei hinterlegt)');
|
'Standard-Skin (keine eigene Datei hinterlegt)', skinUmhang);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Umhänge im Skin-Fenster ----------
|
||||||
|
let skinUmhang = null; // data:-Adresse des angezeigten Umhangs (AeroMC vor offiziell)
|
||||||
|
|
||||||
|
async function ladeUmhangInfo() {
|
||||||
|
const sel = el('skin-cape-offiziell');
|
||||||
|
const info = await window.api.capeInfo();
|
||||||
|
sel.innerHTML = '<option value="">Kein Umhang</option>';
|
||||||
|
if (!info.ok) {
|
||||||
|
el('skin-cape-aero').textContent = info.message || '–';
|
||||||
|
el('skin-cape-remove').classList.add('hidden');
|
||||||
|
skinUmhang = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const c of info.offiziell) {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = c.id;
|
||||||
|
o.textContent = c.name;
|
||||||
|
if (c.aktiv) o.selected = true;
|
||||||
|
sel.appendChild(o);
|
||||||
|
}
|
||||||
|
sel.disabled = !info.offiziell.length;
|
||||||
|
sel.title = info.offiziell.length ? 'Nur Umhänge, die dein Konto bei Mojang besitzt' : 'Dein Konto besitzt keinen offiziellen Umhang';
|
||||||
|
if (info.aeromc) {
|
||||||
|
el('skin-cape-aero').textContent = `Hochgeladen (${info.aeromc.width}×${info.aeromc.height})`;
|
||||||
|
el('skin-cape-remove').classList.remove('hidden');
|
||||||
|
el('skin-cape-edit').textContent = 'Bearbeiten…';
|
||||||
|
} else {
|
||||||
|
el('skin-cape-aero').textContent = info.aeromcFehlt ? 'Server noch nicht bereit' : info.aeromcFehler ? 'Umhang-Server nicht erreichbar' : 'Noch keiner';
|
||||||
|
el('skin-cape-aero').title = info.aeromcFehler || '';
|
||||||
|
el('skin-cape-remove').classList.add('hidden');
|
||||||
|
el('skin-cape-edit').textContent = 'Gestalten…';
|
||||||
|
}
|
||||||
|
const konto = await window.api.authStatus();
|
||||||
|
const r = konto && konto.profile ? await window.api.capeForUuid(konto.profile.id) : null;
|
||||||
|
skinUmhang = r ? r.dataUri : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waehleOffiziellenUmhang() {
|
||||||
|
const sel = el('skin-cape-offiziell');
|
||||||
|
sel.disabled = true;
|
||||||
|
el('skin-cape-status').textContent = 'Umhang wird gesetzt …';
|
||||||
|
const res = await window.api.capeSetOfficial(sel.value || null);
|
||||||
|
sel.disabled = false;
|
||||||
|
if (res.ok) {
|
||||||
|
el('skin-cape-status').textContent = sel.value ? '✔ Offizieller Umhang gesetzt.' : '✔ Kein offizieller Umhang mehr.';
|
||||||
|
await ladeUmhangInfo();
|
||||||
|
await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell, 'Standard-Skin (keine eigene Datei hinterlegt)', skinUmhang);
|
||||||
|
} else {
|
||||||
|
el('skin-cape-status').textContent = '✖ ' + (res.message || 'Fehlgeschlagen.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function entferneAeroUmhang() {
|
||||||
|
askConfirm('AeroMC-Umhang entfernen?', 'Dein eigener Umhang wird vom AeroMC-Umhang-Server gelöscht. Hast du ihn nicht als PNG gespeichert, ist er danach weg.', 'Entfernen', async () => {
|
||||||
|
const res = await window.api.capeRemove();
|
||||||
|
el('skin-cape-status').textContent = res.ok ? '✔ AeroMC-Umhang entfernt.' : '✖ ' + (res.message || 'Fehlgeschlagen.');
|
||||||
|
if (res.ok) {
|
||||||
|
await ladeUmhangInfo();
|
||||||
|
await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell, 'Standard-Skin (keine eigene Datei hinterlegt)', skinUmhang);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Armform gewechselt -> beide Figuren neu zeichnen
|
// Armform gewechselt -> beide Figuren neu zeichnen
|
||||||
async function skinVarianteGewechselt() {
|
async function skinVarianteGewechselt() {
|
||||||
if (skinTexturAktuell) await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell, '');
|
if (skinTexturAktuell) await zeigeSkinFigur('skin-aktuell', 'skin-aktuell-leer', skinTexturAktuell, '', skinUmhang);
|
||||||
if (skinTexturNeu) await zeigeSkinFigur('skin-neu', 'skin-neu-leer', skinTexturNeu, '');
|
if (skinTexturNeu) await zeigeSkinFigur('skin-neu', 'skin-neu-leer', skinTexturNeu, '', skinUmhang);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function waehleSkinDatei() {
|
async function waehleSkinDatei() {
|
||||||
@@ -2697,7 +2777,7 @@ async function waehleSkinDatei() {
|
|||||||
gewaehlterSkin = res.pfad;
|
gewaehlterSkin = res.pfad;
|
||||||
el('skin-pfad').value = res.pfad;
|
el('skin-pfad').value = res.pfad;
|
||||||
skinTexturNeu = res.dataUri || '';
|
skinTexturNeu = res.dataUri || '';
|
||||||
await zeigeSkinFigur('skin-neu', 'skin-neu-leer', skinTexturNeu, 'Vorschau nicht verfügbar');
|
await zeigeSkinFigur('skin-neu', 'skin-neu-leer', skinTexturNeu, 'Vorschau nicht verfügbar', skinUmhang);
|
||||||
el('skin-status').textContent = `${res.breite}×${res.hoehe} Pixel – sieht gut aus.`;
|
el('skin-status').textContent = `${res.breite}×${res.hoehe} Pixel – sieht gut aus.`;
|
||||||
el('skin-apply').disabled = false;
|
el('skin-apply').disabled = false;
|
||||||
}
|
}
|
||||||
@@ -4918,6 +4998,7 @@ async function openSettings(pane) {
|
|||||||
el('s-friends').checked = s.friendsEnabled !== false;
|
el('s-friends').checked = s.friendsEnabled !== false;
|
||||||
el('s-friends-server').checked = s.friendsShareServer !== false;
|
el('s-friends-server').checked = s.friendsShareServer !== false;
|
||||||
el('s-auto-mod-updates').checked = !!s.autoModUpdates;
|
el('s-auto-mod-updates').checked = !!s.autoModUpdates;
|
||||||
|
el('s-aeromc-capes').checked = s.aeromcCapes !== false;
|
||||||
el('s-autostart').checked = !!s.autostart;
|
el('s-autostart').checked = !!s.autostart;
|
||||||
el('s-auto-java').checked = s.autoJava !== false;
|
el('s-auto-java').checked = s.autoJava !== false;
|
||||||
el('s-autobackup').checked = !!s.autoBackupEnabled;
|
el('s-autobackup').checked = !!s.autoBackupEnabled;
|
||||||
@@ -5051,6 +5132,7 @@ async function saveSettings() {
|
|||||||
friendsEnabled: el('s-friends').checked,
|
friendsEnabled: el('s-friends').checked,
|
||||||
friendsShareServer: el('s-friends-server').checked,
|
friendsShareServer: el('s-friends-server').checked,
|
||||||
autoModUpdates: el('s-auto-mod-updates').checked,
|
autoModUpdates: el('s-auto-mod-updates').checked,
|
||||||
|
aeromcCapes: el('s-aeromc-capes').checked,
|
||||||
autostart: el('s-autostart').checked,
|
autostart: el('s-autostart').checked,
|
||||||
autoJava: el('s-auto-java').checked,
|
autoJava: el('s-auto-java').checked,
|
||||||
autoBackupEnabled: el('s-autobackup').checked,
|
autoBackupEnabled: el('s-autobackup').checked,
|
||||||
@@ -5527,6 +5609,18 @@ function wire() {
|
|||||||
// ohne Wrapper käme das Klick-Ereignis als Bereichsname an
|
// ohne Wrapper käme das Klick-Ereignis als Bereichsname an
|
||||||
el('btn-settings').addEventListener('click', () => openSettings());
|
el('btn-settings').addEventListener('click', () => openSettings());
|
||||||
el('search').addEventListener('input', render);
|
el('search').addEventListener('input', render);
|
||||||
|
el('btn-search').addEventListener('click', () => sucheOeffnen());
|
||||||
|
el('search').addEventListener('keydown', (e) => {
|
||||||
|
if (e.key !== 'Escape') return;
|
||||||
|
// Esc gehört hier der Suche, nicht dem globalen Dialog-Schließen
|
||||||
|
e.stopPropagation();
|
||||||
|
el('search').value = '';
|
||||||
|
render();
|
||||||
|
sucheSchliessen();
|
||||||
|
});
|
||||||
|
// leer verlassen = wieder zur Lupe; mit Suchbegriff bleibt das Feld offen,
|
||||||
|
// damit man sieht, dass die Liste gerade gefiltert ist
|
||||||
|
el('search').addEventListener('blur', () => { if (!el('search').value.trim()) sucheSchliessen(); });
|
||||||
|
|
||||||
el('btn-open-dir').addEventListener('click', () => {
|
el('btn-open-dir').addEventListener('click', () => {
|
||||||
if (currentDetailId) window.api.openInstanceFolder(currentDetailId);
|
if (currentDetailId) window.api.openInstanceFolder(currentDetailId);
|
||||||
@@ -5925,6 +6019,9 @@ function wire() {
|
|||||||
el('login-skin').addEventListener('click', openSkin);
|
el('login-skin').addEventListener('click', openSkin);
|
||||||
el('skin-pick').addEventListener('click', waehleSkinDatei);
|
el('skin-pick').addEventListener('click', waehleSkinDatei);
|
||||||
el('skin-variante').addEventListener('change', skinVarianteGewechselt);
|
el('skin-variante').addEventListener('change', skinVarianteGewechselt);
|
||||||
|
el('skin-cape-offiziell').addEventListener('change', waehleOffiziellenUmhang);
|
||||||
|
el('skin-cape-edit').addEventListener('click', () => window.openSkinEditor({ modus: 'umhang', quelle: 'aktuell' }));
|
||||||
|
el('skin-cape-remove').addEventListener('click', entferneAeroUmhang);
|
||||||
el('skin-apply').addEventListener('click', setzeSkin);
|
el('skin-apply').addEventListener('click', setzeSkin);
|
||||||
// gewählte Datei (sonst der aktuelle Skin des Kontos) im Editor öffnen
|
// gewählte Datei (sonst der aktuelle Skin des Kontos) im Editor öffnen
|
||||||
el('skin-editor').addEventListener('click', () => window.openSkinEditor(gewaehlterSkin
|
el('skin-editor').addEventListener('click', () => window.openSkinEditor(gewaehlterSkin
|
||||||
@@ -6274,8 +6371,7 @@ function wire() {
|
|||||||
// der eine eigene Suche hat (Mods, Modpack-Katalog, Import …)
|
// der eine eigene Suche hat (Mods, Modpack-Katalog, Import …)
|
||||||
if (strg && e.key.toLowerCase() === 'f' && !modalOffen) {
|
if (strg && e.key.toLowerCase() === 'f' && !modalOffen) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
el('search').focus();
|
sucheOeffnen();
|
||||||
el('search').select();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Strg+1 … Strg+5: Schnellstart-Instanz starten (nur im Hauptfenster)
|
// Strg+1 … Strg+5: Schnellstart-Instanz starten (nur im Hauptfenster)
|
||||||
@@ -6535,7 +6631,7 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
window.api.onSkinChanged((info) => {
|
window.api.onSkinChanged((info) => {
|
||||||
if (!POPOUT) refreshSkinBackground();
|
if (!POPOUT) refreshSkinBackground();
|
||||||
// im Editor gesetzt -> offenes Skin-Fenster zeigt sonst noch den alten Stand
|
// im Editor gesetzt -> offenes Skin-Fenster zeigt sonst noch den alten Stand
|
||||||
if (POPOUT === 'skin' && info.quelle === 'editor' && !el('modal-skin').classList.contains('hidden')) openSkin();
|
if (POPOUT === 'skin' && (info.quelle === 'editor' || info.quelle === 'umhang') && !el('modal-skin').classList.contains('hidden')) openSkin();
|
||||||
});
|
});
|
||||||
window.api.onInstancesChanged(() => { if (!POPOUT) reload(); });
|
window.api.onInstancesChanged(() => { if (!POPOUT) reload(); });
|
||||||
try { showAuthHealth(await window.api.authHealth()); } catch { /* noch keine Prüfung */ }
|
try { showAuthHealth(await window.api.authHealth()); } catch { /* noch keine Prüfung */ }
|
||||||
|
|||||||
+713
-164
File diff suppressed because it is too large
Load Diff
+24
-1
@@ -115,7 +115,30 @@ async function holeProfil(accessToken) {
|
|||||||
id: daten.id,
|
id: daten.id,
|
||||||
skinUrl: aktiv ? aufHttps(aktiv.url) : '',
|
skinUrl: aktiv ? aufHttps(aktiv.url) : '',
|
||||||
variante: aktiv ? String(aktiv.variant || '').toLowerCase() : '',
|
variante: aktiv ? String(aktiv.variant || '').toLowerCase() : '',
|
||||||
|
// offizielle Umhänge, die dieses Konto besitzt (Migrator, MINECON …)
|
||||||
|
capes: (daten.capes || []).map((c) => ({
|
||||||
|
id: c.id,
|
||||||
|
name: c.alias || 'Umhang',
|
||||||
|
url: aufHttps(c.url),
|
||||||
|
aktiv: c.state === 'ACTIVE',
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { setzeSkin, setzeSkinZurueck, holeProfil, pruefeDatei, aufHttps };
|
/*
|
||||||
|
* Offiziellen Umhang wählen (nur einer, den das Konto besitzt) bzw. mit
|
||||||
|
* capeId = null keinen tragen. Gilt im Spiel für alle Spieler.
|
||||||
|
*/
|
||||||
|
async function setzeUmhang(accessToken, capeId) {
|
||||||
|
const res = await fetch(API + '/minecraft/profile/capes/active', capeId
|
||||||
|
? {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { Authorization: 'Bearer ' + accessToken, 'User-Agent': UA, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ capeId }),
|
||||||
|
}
|
||||||
|
: { method: 'DELETE', headers: { Authorization: 'Bearer ' + accessToken, 'User-Agent': UA } });
|
||||||
|
if (!res.ok) return { ok: false, message: fehlerText(res.status) };
|
||||||
|
return { ok: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { setzeSkin, setzeSkinZurueck, holeProfil, pruefeDatei, aufHttps, setzeUmhang };
|
||||||
|
|||||||
+2
-1
@@ -78,7 +78,8 @@ const DEFAULT_SETTINGS = {
|
|||||||
desktopNotifications: true, // Windows-Meldung bei fertigem Hintergrund-Download/-Backup
|
desktopNotifications: true, // Windows-Meldung bei fertigem Hintergrund-Download/-Backup
|
||||||
friendsEnabled: true,
|
friendsEnabled: true,
|
||||||
friendsShareServer: true, // Freunde sehen, auf welchem Server man spielt (für "Beitreten")
|
friendsShareServer: true, // Freunde sehen, auf welchem Server man spielt (für "Beitreten")
|
||||||
autoModUpdates: false, // vor jedem Start Mods über Modrinth aktualisieren
|
autoModUpdates: false,
|
||||||
|
aeromcCapes: true, // AeroMC-Umhänge im Spiel anzeigen (Umhang-Agent beim Start) // vor jedem Start Mods über Modrinth aktualisieren
|
||||||
schnellstart: {}, // Strg+1 … Strg+5 -> Instanz-ID ({ "1": "Survival", … })
|
schnellstart: {}, // Strg+1 … Strg+5 -> Instanz-ID ({ "1": "Survival", … })
|
||||||
autoWorldBackup: true, // nach jeder Spielsitzung die zuletzt gespielte Welt sichern (letzte 5 Stände) // Freundesystem: eigene Online-Meldung + auffindbar für Anfragen
|
autoWorldBackup: true, // nach jeder Spielsitzung die zuletzt gespielte Welt sichern (letzte 5 Stände) // Freundesystem: eigene Online-Meldung + auffindbar für Anfragen
|
||||||
friendNicknames: {}, // rein lokal: uuid -> eigener Spitzname für einen Freund
|
friendNicknames: {}, // rein lokal: uuid -> eigener Spitzname für einen Freund
|
||||||
|
|||||||
+17
-1
@@ -65,7 +65,7 @@ body{display:grid;grid-template-rows:auto auto 1fr auto;grid-template-columns:mi
|
|||||||
:root[data-theme="dark"] .empty h2,
|
:root[data-theme="dark"] .empty h2,
|
||||||
:root[data-theme="dark"] .mods-col h4,
|
:root[data-theme="dark"] .mods-col h4,
|
||||||
:root[data-theme="dark"] .field>span{color:var(--text)}
|
:root[data-theme="dark"] .field>span{color:var(--text)}
|
||||||
:root[data-theme="dark"] #btn-console svg{color:#c8c8cc}
|
:root[data-theme="dark"] #btn-console svg,:root[data-theme="dark"] #btn-search svg{color:#c8c8cc}
|
||||||
:root[data-theme="dark"] .acct-av svg{color:#c8c8cc}
|
:root[data-theme="dark"] .acct-av svg{color:#c8c8cc}
|
||||||
:root[data-theme="dark"] .mods-filters select,
|
:root[data-theme="dark"] .mods-filters select,
|
||||||
:root[data-theme="dark"] .mods-search select,
|
:root[data-theme="dark"] .mods-search select,
|
||||||
@@ -103,6 +103,14 @@ body.is-popout>.titelleiste{display:none}
|
|||||||
.tbgrow{flex:1}
|
.tbgrow{flex:1}
|
||||||
.search-wrap input{padding:5px 9px;border:1px solid var(--border);border-radius:3px;background:#fff;color:var(--text);font-size:12px;width:150px}
|
.search-wrap input{padding:5px 9px;border:1px solid var(--border);border-radius:3px;background:#fff;color:var(--text);font-size:12px;width:150px}
|
||||||
.search-wrap input:focus{outline:none;border-color:var(--accent)}
|
.search-wrap input:focus{outline:none;border-color:var(--accent)}
|
||||||
|
/* eingeklappt nur die Lupe, aufgeklappt nur das Feld */
|
||||||
|
.search-wrap{display:flex;align-items:center}
|
||||||
|
.search-wrap:not(.offen) input{display:none}
|
||||||
|
.search-wrap.offen .tb-lupe{display:none}
|
||||||
|
.search-wrap.offen input{animation:suche-auf .16s ease-out}
|
||||||
|
@keyframes suche-auf{from{width:0;opacity:0}}
|
||||||
|
.tbtn.tb-lupe{padding:6px 8px}
|
||||||
|
#btn-search svg{color:#5a6470}
|
||||||
|
|
||||||
/* ---------- Arbeitsbereich ---------- */
|
/* ---------- Arbeitsbereich ---------- */
|
||||||
.workspace{display:grid;grid-template-columns:minmax(0,1fr) 172px;overflow:hidden;min-width:0;background:var(--area)}
|
.workspace{display:grid;grid-template-columns:minmax(0,1fr) 172px;overflow:hidden;min-width:0;background:var(--area)}
|
||||||
@@ -337,6 +345,11 @@ body.is-popout>header.toolbar,body.is-popout>main.workspace,body.is-popout>foote
|
|||||||
.skin-bild img{max-width:100%;max-height:100%;image-rendering:pixelated}
|
.skin-bild img{max-width:100%;max-height:100%;image-rendering:pixelated}
|
||||||
.skin-bild canvas{max-width:100%;max-height:100%;cursor:grab}
|
.skin-bild canvas{max-width:100%;max-height:100%;cursor:grab}
|
||||||
.skin-bild canvas:active{cursor:grabbing}
|
.skin-bild canvas:active{cursor:grabbing}
|
||||||
|
.popout-host .skin-bild{height:240px}
|
||||||
|
.skin-umhang{margin-top:12px;padding-top:10px;border-top:1px solid var(--border2)}
|
||||||
|
.skin-umhang .field-row{margin-top:6px}
|
||||||
|
.skin-umhang .field{margin-bottom:6px}
|
||||||
|
.skin-umhang-aero{display:flex;align-items:center;gap:8px;min-height:30px;flex-wrap:wrap}
|
||||||
.crash-details{margin-top:12px}
|
.crash-details{margin-top:12px}
|
||||||
.crash-details summary{cursor:pointer;font-size:12px;color:var(--dim)}
|
.crash-details summary{cursor:pointer;font-size:12px;color:var(--dim)}
|
||||||
.crash-voll{max-height:320px;overflow:auto;margin-top:8px;padding:10px;background:rgba(0,0,0,.25);border-radius:4px;font-family:var(--mono);font-size:11px;line-height:1.45;white-space:pre-wrap;word-break:break-word}
|
.crash-voll{max-height:320px;overflow:auto;margin-top:8px;padding:10px;background:rgba(0,0,0,.25);border-radius:4px;font-family:var(--mono);font-size:11px;line-height:1.45;white-space:pre-wrap;word-break:break-word}
|
||||||
@@ -702,5 +715,8 @@ img.account-av{object-fit:cover;image-rendering:auto}
|
|||||||
.se-3d canvas:active{cursor:grabbing}
|
.se-3d canvas:active{cursor:grabbing}
|
||||||
.se-2d[data-werkzeug="pipette"] canvas{cursor:copy}
|
.se-2d[data-werkzeug="pipette"] canvas{cursor:copy}
|
||||||
.se-3d-leer{position:absolute}
|
.se-3d-leer{position:absolute}
|
||||||
|
.se-ablage{display:none}
|
||||||
|
.se-u2d canvas{cursor:crosshair}
|
||||||
|
.se-u2d[data-werkzeug="pipette"] canvas{cursor:copy}
|
||||||
.se-fuss{align-items:center}
|
.se-fuss{align-items:center}
|
||||||
.se-status{flex:1;min-width:0;margin-left:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
.se-status{flex:1;min-width:0;margin-left:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Plugin Name: AeroMC Freunde
|
* Plugin Name: AeroMC Freunde
|
||||||
* Description: Backend für das Freundesystem des AeroMC Launchers - meldet, wer gerade online/im Spiel ist, verwaltet Freundschaftsanfragen, Einladungen, Kurznachrichten, per Code geteilte Instanzen und Fehlerberichte aus dem Absturz-Assistenten. Anmeldung über Mojangs Sitzungsprüfung (wie bei Minecraft-Servern) - ohne eigenes Passwort.
|
* Description: Backend für das Freundesystem des AeroMC Launchers - meldet, wer gerade online/im Spiel ist, verwaltet Freundschaftsanfragen, Einladungen, Kurznachrichten, per Code geteilte Instanzen und Fehlerberichte aus dem Absturz-Assistenten sowie eigene Umhänge (auch HD) aus dem Skin-Editor. Anmeldung über Mojangs Sitzungsprüfung (wie bei Minecraft-Servern) - ohne eigenes Passwort.
|
||||||
* Version: 1.4.0
|
* Version: 1.5.0
|
||||||
* Author: M_Viper
|
* Author: M_Viper
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ if (!defined('ABSPATH')) {
|
|||||||
exit; // kein direkter Aufruf der Datei
|
exit; // kein direkter Aufruf der Datei
|
||||||
}
|
}
|
||||||
|
|
||||||
define('AEROMC_FRIENDS_VERSION', '1.4.0');
|
define('AEROMC_FRIENDS_VERSION', '1.5.0');
|
||||||
define('AEROMC_FRIENDS_PRESENCE_TABLE', 'aeromc_presence');
|
define('AEROMC_FRIENDS_PRESENCE_TABLE', 'aeromc_presence');
|
||||||
define('AEROMC_FRIENDS_FRIENDS_TABLE', 'aeromc_friends');
|
define('AEROMC_FRIENDS_FRIENDS_TABLE', 'aeromc_friends');
|
||||||
define('AEROMC_FRIENDS_INVITES_TABLE', 'aeromc_invites');
|
define('AEROMC_FRIENDS_INVITES_TABLE', 'aeromc_invites');
|
||||||
@@ -18,6 +18,7 @@ define('AEROMC_FRIENDS_SESSIONS_TABLE', 'aeromc_sessions');
|
|||||||
define('AEROMC_FRIENDS_SHARES_TABLE', 'aeromc_shares');
|
define('AEROMC_FRIENDS_SHARES_TABLE', 'aeromc_shares');
|
||||||
define('AEROMC_FRIENDS_MESSAGES_TABLE', 'aeromc_messages');
|
define('AEROMC_FRIENDS_MESSAGES_TABLE', 'aeromc_messages');
|
||||||
define('AEROMC_FRIENDS_REPORTS_TABLE', 'aeromc_reports');
|
define('AEROMC_FRIENDS_REPORTS_TABLE', 'aeromc_reports');
|
||||||
|
define('AEROMC_FRIENDS_CAPES_TABLE', 'aeromc_capes');
|
||||||
|
|
||||||
// Übergangsmodus: false = Anfragen ohne Anmeldung werden noch angenommen, aber
|
// Übergangsmodus: false = Anfragen ohne Anmeldung werden noch angenommen, aber
|
||||||
// nur für Spieler, die sich noch NIE über die neue Anmeldung gemeldet haben
|
// nur für Spieler, die sich noch NIE über die neue Anmeldung gemeldet haben
|
||||||
@@ -143,6 +144,22 @@ function aeromc_friends_install() {
|
|||||||
) $charset_collate;";
|
) $charset_collate;";
|
||||||
dbDelta($sql_reports);
|
dbDelta($sql_reports);
|
||||||
|
|
||||||
|
// eigene Umhänge (Bild liegt unter wp-content/uploads/aeromc-umhaenge)
|
||||||
|
$capes_table = $wpdb->prefix . AEROMC_FRIENDS_CAPES_TABLE;
|
||||||
|
$sql_capes = "CREATE TABLE $capes_table (
|
||||||
|
uuid varchar(32) NOT NULL,
|
||||||
|
username varchar(32) NOT NULL,
|
||||||
|
datei varchar(64) NOT NULL DEFAULT '',
|
||||||
|
breite smallint(5) unsigned NOT NULL DEFAULT 0,
|
||||||
|
hoehe smallint(5) unsigned NOT NULL DEFAULT 0,
|
||||||
|
hash varchar(40) NOT NULL DEFAULT '',
|
||||||
|
status varchar(16) NOT NULL DEFAULT 'aktiv',
|
||||||
|
updated_at datetime NOT NULL,
|
||||||
|
PRIMARY KEY (uuid),
|
||||||
|
KEY status (status)
|
||||||
|
) $charset_collate;";
|
||||||
|
dbDelta($sql_capes);
|
||||||
|
|
||||||
update_option('aeromc_friends_db_version', AEROMC_FRIENDS_VERSION);
|
update_option('aeromc_friends_db_version', AEROMC_FRIENDS_VERSION);
|
||||||
}
|
}
|
||||||
register_activation_hook(__FILE__, 'aeromc_friends_install');
|
register_activation_hook(__FILE__, 'aeromc_friends_install');
|
||||||
@@ -157,6 +174,7 @@ add_action('plugins_loaded', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
require_once plugin_dir_path(__FILE__) . 'includes/routes.php';
|
require_once plugin_dir_path(__FILE__) . 'includes/routes.php';
|
||||||
|
require_once plugin_dir_path(__FILE__) . 'includes/capes.php';
|
||||||
if (is_admin()) {
|
if (is_admin()) {
|
||||||
require_once plugin_dir_path(__FILE__) . 'includes/admin.php';
|
require_once plugin_dir_path(__FILE__) . 'includes/admin.php';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,306 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* AeroMC-Umhänge: eigene (auch HD-)Umhänge, die Spieler im Launcher gestalten.
|
||||||
|
*
|
||||||
|
* POST /aeromc/v1/cape { uuid, image } Umhang hochladen/ersetzen (Anmeldung Pflicht)
|
||||||
|
* POST /aeromc/v1/cape/remove { uuid } eigenen Umhang entfernen (Anmeldung Pflicht)
|
||||||
|
* GET /aeromc/v1/cape?uuid= Umhang eines Spielers (öffentlich)
|
||||||
|
* GET /aeromc/v1/capes?uuids=a,b,c mehrere auf einmal, für Launcher und Mod (öffentlich)
|
||||||
|
*
|
||||||
|
* Bilder werden streng geprüft (echtes PNG, Seitenverhältnis 2:1, 64×32 bis
|
||||||
|
* 1024×512) und – falls GD verfügbar ist – neu kodiert, damit keine fremden
|
||||||
|
* Daten im Bild mitreisen. Der Dateiname enthält einen Teil der Prüfsumme, so
|
||||||
|
* ändert sich die Adresse bei jedem neuen Umhang (kein veralteter Zwischenspeicher).
|
||||||
|
* Admins können Umhänge unter "AeroMC Fehler → Umhänge" löschen oder sperren.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
define('AEROMC_FRIENDS_CAPE_DIR', 'aeromc-umhaenge');
|
||||||
|
define('AEROMC_FRIENDS_CAPE_MAX_BYTES', 3000000);
|
||||||
|
define('AEROMC_FRIENDS_CAPE_MIN_GAP', 15); // Sekunden zwischen zwei Uploads
|
||||||
|
define('AEROMC_FRIENDS_CAPE_MAX_UUIDS', 100); // je Sammelabfrage
|
||||||
|
|
||||||
|
function aeromc_friends_capes_table() {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->prefix . AEROMC_FRIENDS_CAPES_TABLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_cape_dir() {
|
||||||
|
$uploads = wp_upload_dir();
|
||||||
|
return [
|
||||||
|
'pfad' => trailingslashit($uploads['basedir']) . AEROMC_FRIENDS_CAPE_DIR,
|
||||||
|
'url' => trailingslashit($uploads['baseurl']) . AEROMC_FRIENDS_CAPE_DIR,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Datei eines Umhangs löschen (nur Namen in unserem Schema). */
|
||||||
|
function aeromc_friends_cape_delete_file($datei) {
|
||||||
|
if (!$datei || !preg_match('/^[a-f0-9]{32}-[a-f0-9]{8}\.png$/', (string) $datei)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$ort = aeromc_friends_cape_dir();
|
||||||
|
$voll = $ort['pfad'] . '/' . $datei;
|
||||||
|
if (file_exists($voll)) {
|
||||||
|
@unlink($voll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Zeile -> öffentliche Angaben */
|
||||||
|
function aeromc_friends_cape_info($r) {
|
||||||
|
$ort = aeromc_friends_cape_dir();
|
||||||
|
return [
|
||||||
|
'url' => $ort['url'] . '/' . $r->datei,
|
||||||
|
'width' => (int) $r->breite,
|
||||||
|
'height' => (int) $r->hoehe,
|
||||||
|
'hash' => $r->hash,
|
||||||
|
'updated' => $r->updated_at,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft ein Umhang-Bild und gibt [daten, breite, hoehe] zurück oder WP_Error.
|
||||||
|
*/
|
||||||
|
function aeromc_friends_cape_check_image($base64) {
|
||||||
|
$daten = base64_decode(preg_replace('/^data:image\/png;base64,/', '', (string) $base64), true);
|
||||||
|
if ($daten === false || strlen($daten) < 60) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültiges Bild.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
if (strlen($daten) > AEROMC_FRIENDS_CAPE_MAX_BYTES) {
|
||||||
|
return new WP_Error('aeromc_too_big', 'Der Umhang ist zu groß (Datei).', ['status' => 413]);
|
||||||
|
}
|
||||||
|
if (strncmp($daten, "\x89PNG\r\n\x1A\n", 8) !== 0 || substr($daten, 12, 4) !== 'IHDR') {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Nur PNG-Bilder.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$kopf = unpack('Nbreite/Nhoehe', substr($daten, 16, 8));
|
||||||
|
$breite = (int) $kopf['breite'];
|
||||||
|
$hoehe = (int) $kopf['hoehe'];
|
||||||
|
if (!in_array($breite, [64, 128, 256, 512, 1024], true) || $breite !== 2 * $hoehe) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ein Umhang muss 64×32, 128×64, 256×128, 512×256 oder 1024×512 Pixel groß sein.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
// neu kodieren: entfernt alles außer den Bildpunkten
|
||||||
|
if (function_exists('imagecreatefromstring')) {
|
||||||
|
$bild = @imagecreatefromstring($daten);
|
||||||
|
if (!$bild || imagesx($bild) !== $breite || imagesy($bild) !== $hoehe) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Das Bild ist beschädigt.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
imagealphablending($bild, false);
|
||||||
|
imagesavealpha($bild, true);
|
||||||
|
ob_start();
|
||||||
|
imagepng($bild, null, 9);
|
||||||
|
$daten = ob_get_clean();
|
||||||
|
imagedestroy($bild);
|
||||||
|
}
|
||||||
|
return [$daten, $breite, $hoehe];
|
||||||
|
}
|
||||||
|
|
||||||
|
add_action('rest_api_init', function () {
|
||||||
|
register_rest_route('aeromc/v1', '/cape', [
|
||||||
|
[
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_cape_upload',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => 'aeromc_friends_route_cape_get',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/cape/remove', [
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => 'aeromc_friends_route_cape_remove',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
register_rest_route('aeromc/v1', '/capes', [
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => 'aeromc_friends_route_capes',
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Anmeldung ist hier immer Pflicht – auch im Übergangsmodus (öffentlich sichtbare Bilder). */
|
||||||
|
function aeromc_friends_cape_auth($request, $uuid) {
|
||||||
|
$sitzung = aeromc_friends_check_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
if (!$sitzung) {
|
||||||
|
return new WP_Error('aeromc_auth', 'Anmeldung erforderlich.', ['status' => 401]);
|
||||||
|
}
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_route_cape_upload($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
if (!$uuid) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$sitzung = aeromc_friends_cape_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_capes_table();
|
||||||
|
$alt = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE uuid = %s", $uuid));
|
||||||
|
if ($alt && $alt->status === 'gesperrt') {
|
||||||
|
return new WP_Error('aeromc_blocked', 'Dein Umhang wurde vom Betreiber gesperrt.', ['status' => 403]);
|
||||||
|
}
|
||||||
|
if ($alt && time() - strtotime($alt->updated_at . ' UTC') < AEROMC_FRIENDS_CAPE_MIN_GAP) {
|
||||||
|
return new WP_Error('aeromc_too_fast', 'Bitte einen Moment warten, bevor du den Umhang erneut hochlädst.', ['status' => 429]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$geprueft = aeromc_friends_cape_check_image($request->get_param('image'));
|
||||||
|
if (is_wp_error($geprueft)) {
|
||||||
|
return $geprueft;
|
||||||
|
}
|
||||||
|
list($daten, $breite, $hoehe) = $geprueft;
|
||||||
|
$hash = sha1($daten);
|
||||||
|
|
||||||
|
$ort = aeromc_friends_cape_dir();
|
||||||
|
if (!wp_mkdir_p($ort['pfad'])) {
|
||||||
|
return new WP_Error('aeromc_error', 'Umhang konnte nicht gespeichert werden.', ['status' => 500]);
|
||||||
|
}
|
||||||
|
if (!file_exists($ort['pfad'] . '/index.html')) {
|
||||||
|
file_put_contents($ort['pfad'] . '/index.html', '');
|
||||||
|
}
|
||||||
|
$datei = $uuid . '-' . substr($hash, 0, 8) . '.png';
|
||||||
|
if (file_put_contents($ort['pfad'] . '/' . $datei, $daten) === false) {
|
||||||
|
return new WP_Error('aeromc_error', 'Umhang konnte nicht gespeichert werden.', ['status' => 500]);
|
||||||
|
}
|
||||||
|
if ($alt && $alt->datei !== $datei) {
|
||||||
|
aeromc_friends_cape_delete_file($alt->datei);
|
||||||
|
}
|
||||||
|
$zeile = [
|
||||||
|
'uuid' => $uuid,
|
||||||
|
'username' => $sitzung->username,
|
||||||
|
'datei' => $datei,
|
||||||
|
'breite' => $breite,
|
||||||
|
'hoehe' => $hoehe,
|
||||||
|
'hash' => $hash,
|
||||||
|
'status' => 'aktiv',
|
||||||
|
'updated_at' => current_time('mysql', true),
|
||||||
|
];
|
||||||
|
$wpdb->replace($table, $zeile);
|
||||||
|
return ['ok' => true, 'cape' => aeromc_friends_cape_info((object) $zeile)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_route_cape_remove($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
if (!$uuid) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$sitzung = aeromc_friends_cape_auth($request, $uuid);
|
||||||
|
if (is_wp_error($sitzung)) {
|
||||||
|
return $sitzung;
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_capes_table();
|
||||||
|
$alt = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE uuid = %s", $uuid));
|
||||||
|
if ($alt) {
|
||||||
|
aeromc_friends_cape_delete_file($alt->datei);
|
||||||
|
if ($alt->status === 'gesperrt') {
|
||||||
|
// Sperre bleibt bestehen, nur das Bild ist weg
|
||||||
|
$wpdb->update($table, ['datei' => '', 'hash' => ''], ['uuid' => $uuid]);
|
||||||
|
} else {
|
||||||
|
$wpdb->delete($table, ['uuid' => $uuid]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ['ok' => true];
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_route_cape_get($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($request->get_param('uuid'));
|
||||||
|
if (!$uuid) {
|
||||||
|
return new WP_Error('aeromc_invalid', 'Ungültige uuid.', ['status' => 400]);
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_capes_table();
|
||||||
|
$r = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE uuid = %s AND status = 'aktiv'", $uuid));
|
||||||
|
return ['ok' => true, 'cape' => $r ? aeromc_friends_cape_info($r) : null];
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_route_capes($request) {
|
||||||
|
global $wpdb;
|
||||||
|
$liste = array_filter(array_map('aeromc_friends_normalize_uuid', explode(',', (string) $request->get_param('uuids'))));
|
||||||
|
$liste = array_slice(array_values(array_unique($liste)), 0, AEROMC_FRIENDS_CAPE_MAX_UUIDS);
|
||||||
|
$ergebnis = [];
|
||||||
|
if ($liste) {
|
||||||
|
$table = aeromc_friends_capes_table();
|
||||||
|
$platz = implode(',', array_fill(0, count($liste), '%s'));
|
||||||
|
$rows = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM $table WHERE status = 'aktiv' AND uuid IN ($platz)", $liste
|
||||||
|
));
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$ergebnis[$r->uuid] = aeromc_friends_cape_info($r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$antwort = new WP_REST_Response(['ok' => true, 'capes' => (object) $ergebnis]);
|
||||||
|
// kurz zwischenspeichern lassen – der Mod fragt beim Betreten eines Servers viele auf einmal ab
|
||||||
|
$antwort->header('Cache-Control', 'public, max-age=60');
|
||||||
|
return $antwort;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Admin: Umhänge prüfen, löschen, sperren ----------
|
||||||
|
if (is_admin()) {
|
||||||
|
add_action('admin_menu', function () {
|
||||||
|
add_submenu_page('aeromc-reports', 'AeroMC Umhänge', 'Umhänge', 'manage_options', 'aeromc-capes', 'aeromc_friends_capes_page');
|
||||||
|
}, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
function aeromc_friends_capes_page() {
|
||||||
|
global $wpdb;
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$table = aeromc_friends_capes_table();
|
||||||
|
if (isset($_POST['aeromc_cape'], $_POST['aeromc_aktion']) && check_admin_referer('aeromc_capes')) {
|
||||||
|
$uuid = aeromc_friends_normalize_uuid($_POST['aeromc_cape']);
|
||||||
|
$r = $uuid ? $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE uuid = %s", $uuid)) : null;
|
||||||
|
if ($r) {
|
||||||
|
$aktion = (string) $_POST['aeromc_aktion'];
|
||||||
|
if ($aktion === 'loeschen') {
|
||||||
|
aeromc_friends_cape_delete_file($r->datei);
|
||||||
|
$wpdb->delete($table, ['uuid' => $uuid]);
|
||||||
|
echo '<div class="notice notice-success"><p>Umhang von ' . esc_html($r->username) . ' gelöscht.</p></div>';
|
||||||
|
} elseif ($aktion === 'sperren') {
|
||||||
|
aeromc_friends_cape_delete_file($r->datei);
|
||||||
|
$wpdb->update($table, ['status' => 'gesperrt', 'datei' => '', 'hash' => ''], ['uuid' => $uuid]);
|
||||||
|
echo '<div class="notice notice-success"><p>' . esc_html($r->username) . ' gesperrt – der Umhang ist entfernt, neue Uploads werden abgelehnt.</p></div>';
|
||||||
|
} elseif ($aktion === 'entsperren') {
|
||||||
|
$wpdb->delete($table, ['uuid' => $uuid]);
|
||||||
|
echo '<div class="notice notice-success"><p>Sperre für ' . esc_html($r->username) . ' aufgehoben.</p></div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$ort = aeromc_friends_cape_dir();
|
||||||
|
$rows = $wpdb->get_results("SELECT * FROM $table ORDER BY updated_at DESC LIMIT 300");
|
||||||
|
echo '<div class="wrap"><h1>AeroMC Umhänge</h1>';
|
||||||
|
echo '<p>Eigene Umhänge, die Spieler im AeroMC-Skin-Editor hochgeladen haben. „Sperren“ entfernt den Umhang und verhindert neue Uploads dieses Spielers.</p>';
|
||||||
|
if (!$rows) {
|
||||||
|
echo '<p>Noch keine Umhänge hochgeladen.</p></div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
echo '<table class="widefat striped"><thead><tr><th style="width:150px">Umhang</th><th>Spieler</th><th>Größe</th><th>Geändert (UTC)</th><th>Status</th><th></th></tr></thead><tbody>';
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$bild = $r->datei
|
||||||
|
? '<img src="' . esc_url($ort['url'] . '/' . $r->datei) . '" alt="" style="width:128px;height:64px;image-rendering:pixelated;background:#ddd;border:1px solid #ccc" />'
|
||||||
|
: '–';
|
||||||
|
echo '<tr><td>' . $bild . '</td><td>' . esc_html($r->username) . '<br /><small>' . esc_html($r->uuid) . '</small></td>'
|
||||||
|
. '<td>' . esc_html($r->breite . '×' . $r->hoehe) . '</td><td>' . esc_html($r->updated_at) . '</td><td>' . esc_html($r->status) . '</td><td>';
|
||||||
|
echo '<form method="post" style="display:flex;gap:6px">';
|
||||||
|
wp_nonce_field('aeromc_capes');
|
||||||
|
echo '<input type="hidden" name="aeromc_cape" value="' . esc_attr($r->uuid) . '" />';
|
||||||
|
if ($r->status === 'gesperrt') {
|
||||||
|
echo '<button class="button" name="aeromc_aktion" value="entsperren">Sperre aufheben</button>';
|
||||||
|
} else {
|
||||||
|
echo '<button class="button" name="aeromc_aktion" value="loeschen" onclick="return confirm(\'Umhang löschen?\')">Löschen</button>';
|
||||||
|
echo '<button class="button button-link-delete" name="aeromc_aktion" value="sperren" onclick="return confirm(\'Spieler sperren? Der Umhang wird entfernt und neue Uploads abgelehnt.\')">Sperren</button>';
|
||||||
|
}
|
||||||
|
echo '</form></td></tr>';
|
||||||
|
}
|
||||||
|
echo '</tbody></table></div>';
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user