diff --git a/mod/MODRINTH.md b/mod/MODRINTH.md new file mode 100644 index 0000000..3405a3a --- /dev/null +++ b/mod/MODRINTH.md @@ -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. diff --git a/mod/resources/aeromc-capes.mixins.json b/mod/resources/aeromc-capes.mixins.json new file mode 100644 index 0000000..f957033 --- /dev/null +++ b/mod/resources/aeromc-capes.mixins.json @@ -0,0 +1,10 @@ +{ + "required": false, + "minVersion": "0.8", + "package": "de.aeromc.capes.mixin", + "compatibilityLevel": "JAVA_8", + "client": ["SessionServiceMixin", "SkinLaderMixin"], + "injectors": { + "defaultRequire": 0 + } +} diff --git a/mod/resources/fabric.mod.json b/mod/resources/fabric.mod.json new file mode 100644 index 0000000..b1ab2e5 --- /dev/null +++ b/mod/resources/fabric.mod.json @@ -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" + } +} diff --git a/mod/src/de/aeromc/capes/mixin/SessionServiceMixin.java b/mod/src/de/aeromc/capes/mixin/SessionServiceMixin.java new file mode 100644 index 0000000..cf0e272 --- /dev/null +++ b/mod/src/de/aeromc/capes/mixin/SessionServiceMixin.java @@ -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 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 cir) { + aeromc$ersetze(cir, profil); + } + + private static void aeromc$ersetze(CallbackInfoReturnable 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 + } + } +} diff --git a/mod/src/de/aeromc/capes/mixin/SkinLaderMixin.java b/mod/src/de/aeromc/capes/mixin/SkinLaderMixin.java new file mode 100644 index 0000000..c331867 --- /dev/null +++ b/mod/src/de/aeromc/capes/mixin/SkinLaderMixin.java @@ -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; + } + } +} diff --git a/mod/src/de/aeromc/capes/mod/Umhaenge.java b/mod/src/de/aeromc/capes/mod/Umhaenge.java new file mode 100644 index 0000000..658cb07 --- /dev/null +++ b/mod/src/de/aeromc/capes/mod/Umhaenge.java @@ -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); + } +} diff --git a/mod/stubs/net/minecraft/class_1071.java b/mod/stubs/net/minecraft/class_1071.java new file mode 100644 index 0000000..0cb2d9c --- /dev/null +++ b/mod/stubs/net/minecraft/class_1071.java @@ -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 {} diff --git a/scripts/build-agent.js b/scripts/build-agent.js new file mode 100644 index 0000000..dc34585 --- /dev/null +++ b/scripts/build-agent.js @@ -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 }); +} diff --git a/scripts/build-mod.js b/scripts/build-mod.js new file mode 100644 index 0000000..c57a10e --- /dev/null +++ b/scripts/build-mod.js @@ -0,0 +1,83 @@ +'use strict'; + +/* + * build-mod.js – baut den Fabric/Quilt-Mod "AeroMC Umhänge" (dist/aeromc-umhaenge-fabric-.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 }); +} diff --git a/src/assets/agent/aeromc-capes.jar b/src/assets/agent/aeromc-capes.jar new file mode 100644 index 0000000..4abb88c Binary files /dev/null and b/src/assets/agent/aeromc-capes.jar differ