Upload via GUI (194 Dateien)
This commit is contained in:
@@ -3,15 +3,22 @@ package de.aeromc.capes;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
|
||||
/**
|
||||
* AeroMC-Umhang-Agent.
|
||||
* AeroMC-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.
|
||||
* Loader) und kann zwei Dinge, einzeln zuschaltbar:
|
||||
*
|
||||
* Argument: Basisadresse des Umhang-Servers (…/wp-json/aeromc/v1).
|
||||
* Umhänge: klinkt sich in Mojangs authlib ein – über sie lädt Minecraft für
|
||||
* jeden Spieler Skin und Umhang – und setzt dort den AeroMC-Umhang
|
||||
* des Spielers ein, falls er einen hat. Alles Weitere (Herunterladen,
|
||||
* Zwischenspeichern, Anzeigen, auch HD) erledigt Minecraft selbst.
|
||||
* Spieldesign: AeroMC-Ladebildschirm, Fenstertitel und -symbol (siehe Design).
|
||||
*
|
||||
* Argument, Teile durch "|" getrennt:
|
||||
* <Basisadresse des Umhang-Servers> (…/wp-json/aeromc/v1)
|
||||
* capes=0 Umhänge aus
|
||||
* design=<Ordner> Spieldesign an, Grafiken aus diesem Ordner
|
||||
* Ein Argument nur mit Adresse (ältere Launcher) heißt: nur Umhänge.
|
||||
*/
|
||||
public final class Agent {
|
||||
/** Unter diesem Schlüssel liegt der Nachschlage-Dienst in den System-Properties. */
|
||||
@@ -28,15 +35,42 @@ public final class Agent {
|
||||
}
|
||||
|
||||
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);
|
||||
String basis = Nachschlagen.STANDARD_BASIS;
|
||||
boolean umhaenge = true;
|
||||
String design = null;
|
||||
String mods = null;
|
||||
if (argument != null) {
|
||||
for (String teil : argument.split("\\|")) {
|
||||
if (teil.startsWith("http")) basis = teil;
|
||||
else if (teil.equals("capes=0")) umhaenge = false;
|
||||
else if (teil.startsWith("design=") && teil.length() > 7) design = teil.substring(7);
|
||||
else if (teil.startsWith("mods=") && teil.length() > 5) mods = teil.substring(5);
|
||||
}
|
||||
}
|
||||
if (mods != null) {
|
||||
try {
|
||||
Design.installiereMods(mods, inst);
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] Mod-Schalter konnte nicht starten: " + t);
|
||||
}
|
||||
}
|
||||
if (umhaenge) {
|
||||
try {
|
||||
// Die eingefügten Befehle in authlib holen sich den Dienst von hier – nur
|
||||
// über JDK-Typen, damit es mit jedem Classloader und Modulsystem klappt.
|
||||
System.getProperties().put(SCHLUESSEL, new Nachschlagen(basis));
|
||||
inst.addTransformer(new Umbau());
|
||||
System.out.println("[AeroMC] Umhang-Agent aktiv");
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] Umhang-Agent konnte nicht starten: " + t);
|
||||
}
|
||||
}
|
||||
if (design != null) {
|
||||
try {
|
||||
Design.installiere(design, inst);
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] Spieldesign konnte nicht starten: " + t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package de.aeromc.capes;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* AeroMC-Look im Spiel (Minecraft 1.14 … 26.x):
|
||||
*
|
||||
* – Ladebildschirm: AeroMC-Nachtblau statt Mojang-Rot, hellblauer Balken und
|
||||
* AeroMC-Logo statt Mojang-Logo. Das Logo lädt Minecraft am Ressourcenpaket
|
||||
* vorbei direkt aus dem Spiel – deshalb tauscht der Agent den Datenstrom aus.
|
||||
* – Fenster: Titel "AeroMC - Minecraft …" und AeroMC-Symbol.
|
||||
*
|
||||
* Titel-Logo, Untertitel, Sprüche und Knöpfe kommen über ein Ressourcenpaket,
|
||||
* das der Launcher anlegt (src/spieldesign.js).
|
||||
*
|
||||
* Die umgebauten Stellen im Spiel holen sich ihre Helfer – wie beim Umhang –
|
||||
* über die System-Properties und nur über JDK-Typen (java.util.function.Function),
|
||||
* damit es mit jedem Classloader klappt (Vanilla, Fabric, Forge, NeoForge).
|
||||
*/
|
||||
final class Design {
|
||||
static final String LOGO = "aeromc.design.logo";
|
||||
static final String LOGO_ALT = "aeromc.design.logoAlt";
|
||||
static final String TITEL = "aeromc.design.titel";
|
||||
static final String ICON = "aeromc.design.icon";
|
||||
static final String MODS = "aeromc.mods";
|
||||
|
||||
/** Hintergrund des Ladebildschirms (wie das AeroMC-Design im Launcher). */
|
||||
static final int HINTERGRUND = 0x0A0E1A;
|
||||
/** Fortschrittsbalken. */
|
||||
static final int BALKEN = 0x4FD2FF;
|
||||
/** Breite des Balkens als Anteil der Bildschirmbreite (das Logo ist jetzt bildschirmbreit). */
|
||||
static final float BALKEN_ANTEIL = 0.56f;
|
||||
|
||||
private static final int[] ICON_GROESSEN = {16, 32, 48, 64, 128, 256};
|
||||
/** Pixelspeicher der Symbole festhalten, bis GLFW sie übernommen hat. */
|
||||
private static final List<ByteBuffer> FESTHALTEN = new ArrayList<>();
|
||||
|
||||
private Design() {}
|
||||
|
||||
static void installiere(String ordner, Instrumentation inst) {
|
||||
File dir = new File(ordner);
|
||||
Properties p = System.getProperties();
|
||||
p.put(LOGO, strom(new File(dir, "ladelogo.png")));
|
||||
p.put(LOGO_ALT, strom(new File(dir, "ladelogo_alt.png")));
|
||||
p.put(TITEL, (Function<Object, Object>) Design::titel);
|
||||
p.put(ICON, (Function<Object, Object>) bilder -> symbol(dir, bilder));
|
||||
inst.addTransformer(new DesignUmbau());
|
||||
System.out.println("[AeroMC] Spieldesign aktiv");
|
||||
}
|
||||
|
||||
/**
|
||||
* Mod-Schalter: Klick auf „Mods verwalten“ im Pausemenü (Adresse aeromc://mods)
|
||||
* meldet sich beim Launcher, der dann das Mod-Fenster über dem Spiel öffnet.
|
||||
* Rückgabe TRUE = erledigt (Minecraft öffnet keinen Link), sonst FALSE.
|
||||
*/
|
||||
static void installiereMods(String adresse, Instrumentation inst) {
|
||||
System.getProperties().put(MODS, (Function<Object, Object>) link -> {
|
||||
if (link == null || !link.toString().startsWith("aeromc:")) return Boolean.FALSE;
|
||||
Thread t = new Thread(() -> {
|
||||
try {
|
||||
java.net.HttpURLConnection c = (java.net.HttpURLConnection) new java.net.URL(adresse).openConnection();
|
||||
c.setConnectTimeout(2000);
|
||||
c.setReadTimeout(4000);
|
||||
c.getResponseCode();
|
||||
c.disconnect();
|
||||
} catch (Throwable e) {
|
||||
System.out.println("[AeroMC] Launcher fuer den Mod-Schalter nicht erreichbar: " + e);
|
||||
}
|
||||
}, "AeroMC-Mod-Schalter");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
return Boolean.TRUE;
|
||||
});
|
||||
inst.addTransformer(new ModsUmbau());
|
||||
System.out.println("[AeroMC] Mod-Schalter aktiv");
|
||||
}
|
||||
|
||||
/** Ersetzt den Datenstrom des Mojang-Logos durch das AeroMC-Logo (bei Fehlern bleibt das Original). */
|
||||
private static Function<Object, Object> strom(File datei) {
|
||||
return original -> {
|
||||
try {
|
||||
return new ByteArrayInputStream(Files.readAllBytes(datei.toPath()));
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] Ladelogo nicht lesbar: " + t);
|
||||
return original;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Object titel(Object original) {
|
||||
if (original == null) return null;
|
||||
String s = original.toString();
|
||||
return s.startsWith("AeroMC") ? original : "AeroMC - " + s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut aus den mitgelieferten Rohdaten (icon_N.rgba) einen neuen
|
||||
* GLFWImage.Buffer. LWJGL wird über den Classloader des übergebenen Buffers
|
||||
* angesprochen – der Agent selbst kennt LWJGL nicht.
|
||||
*/
|
||||
private static Object symbol(File dir, Object original) {
|
||||
if (original == null) return null;
|
||||
try {
|
||||
List<int[]> groessen = new ArrayList<>();
|
||||
List<byte[]> daten = new ArrayList<>();
|
||||
for (int n : ICON_GROESSEN) {
|
||||
File f = new File(dir, "icon_" + n + ".rgba");
|
||||
if (!f.isFile()) continue;
|
||||
byte[] b = Files.readAllBytes(f.toPath());
|
||||
if (b.length != n * n * 4) continue;
|
||||
groessen.add(new int[] {n, n});
|
||||
daten.add(b);
|
||||
}
|
||||
if (daten.isEmpty()) return original;
|
||||
ClassLoader cl = original.getClass().getClassLoader();
|
||||
Class<?> bild = Class.forName("org.lwjgl.glfw.GLFWImage", true, cl);
|
||||
Object puffer = bild.getMethod("malloc", int.class).invoke(null, daten.size());
|
||||
Method hole = puffer.getClass().getMethod("get", int.class);
|
||||
Method setze = bild.getMethod("set", int.class, int.class, ByteBuffer.class);
|
||||
for (int i = 0; i < daten.size(); i++) {
|
||||
ByteBuffer px = ByteBuffer.allocateDirect(daten.get(i).length);
|
||||
px.put(daten.get(i)).flip();
|
||||
synchronized (FESTHALTEN) { FESTHALTEN.add(px); }
|
||||
setze.invoke(hole.invoke(puffer, i), groessen.get(i)[0], groessen.get(i)[1], px);
|
||||
}
|
||||
return puffer;
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] Fenstersymbol nicht gesetzt: " + t);
|
||||
return original;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
package de.aeromc.capes;
|
||||
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.ProtectionDomain;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.InsnList;
|
||||
import org.objectweb.asm.tree.InsnNode;
|
||||
import org.objectweb.asm.tree.IntInsnNode;
|
||||
import org.objectweb.asm.tree.LdcInsnNode;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
import org.objectweb.asm.tree.TypeInsnNode;
|
||||
import org.objectweb.asm.tree.VarInsnNode;
|
||||
|
||||
/**
|
||||
* Baut beim Laden um (siehe Design):
|
||||
*
|
||||
* 1. Ladebildschirm – erkannt am Text "textures/gui/title/mojangstudios.png"
|
||||
* (ab 1.16) bzw. "…/mojang.png" (1.14/1.15), denn die Klassennamen sind je
|
||||
* Version und Loader anders (verschleiert, Intermediary, SRG, Mojang-Namen):
|
||||
* ab 1.16: color(255, 239, 50, 61) -> AeroMC-Nachtblau
|
||||
* color(alpha, 255, 255, 255) -> Balkenfarbe
|
||||
* 1.14/1.15: weiße Füllung (0xFFFFFF / -1) -> Nachtblau,
|
||||
* Balken-Rot (226, 40, 55) -> Balkenfarbe
|
||||
* 2. Logo-Textur – die innere Klasse des Ladebildschirms ("…$…") liest das
|
||||
* Logo mit NativeImage.read(InputStream); davor wird der Strom getauscht.
|
||||
* 3. LWJGL-GLFW – Fenstertitel (glfwCreateWindow/glfwSetWindowTitle) und
|
||||
* Fenstersymbol (glfwSetWindowIcon).
|
||||
*
|
||||
* Eingefügt bzw. ersetzt wird nur geradliniger Code ohne Sprünge mit gleicher
|
||||
* Stapelwirkung – die vorhandenen Stack-Map-Frames bleiben gültig.
|
||||
*/
|
||||
final class DesignUmbau implements ClassFileTransformer {
|
||||
private static final byte[] MERKMAL = "textures/gui/title/mojang".getBytes(StandardCharsets.UTF_8);
|
||||
private static final String LOGO_NEU = "textures/gui/title/mojangstudios.png";
|
||||
private static final String LOGO_ALT = "textures/gui/title/mojang.png";
|
||||
private static final byte[] TITEL_MERKMAL = "textures/gui/title/minecraft.png".getBytes(StandardCharsets.UTF_8);
|
||||
private static final int KEIN_WERT = Integer.MIN_VALUE;
|
||||
|
||||
/** Name der Ladebildschirm-Klasse, sobald sie gesehen wurde. */
|
||||
private volatile String ladeKlasse;
|
||||
private volatile boolean ladeAlt;
|
||||
|
||||
@Override
|
||||
public byte[] transform(ClassLoader loader, String name, Class<?> umdefiniert, ProtectionDomain pd, byte[] klasse) {
|
||||
if (name == null || umdefiniert != null) return null;
|
||||
try {
|
||||
if ("org/lwjgl/glfw/GLFW".equals(name)) return glfw(klasse);
|
||||
if (name.startsWith("java/") || name.startsWith("jdk/") || name.startsWith("sun/")
|
||||
|| name.startsWith("de/aeromc/") || name.startsWith("org/lwjgl/")) return null;
|
||||
String lk = ladeKlasse;
|
||||
if (lk != null && name.startsWith(lk + "$")) return logoStrom(klasse, ladeAlt);
|
||||
if (enthaelt(klasse, MERKMAL)) return ladeBildschirm(name, klasse);
|
||||
if (enthaelt(klasse, TITEL_MERKMAL)) return titelLogo(name, klasse);
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] Spieldesign: " + name + " nicht angepasst: " + t);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------- 1. Ladebildschirm ----------
|
||||
|
||||
private byte[] ladeBildschirm(String name, byte[] klasse) {
|
||||
ClassNode cn = lies(klasse);
|
||||
boolean neu = hatText(cn, LOGO_NEU), alt = hatText(cn, LOGO_ALT);
|
||||
if (!neu && !alt) return null;
|
||||
ladeKlasse = name;
|
||||
ladeAlt = alt && !neu;
|
||||
int r = Design.HINTERGRUND >> 16 & 255, g = Design.HINTERGRUND >> 8 & 255, b = Design.HINTERGRUND & 255;
|
||||
int br = Design.BALKEN >> 16 & 255, bg = Design.BALKEN >> 8 & 255, bb = Design.BALKEN & 255;
|
||||
int hintergrund = 0, balken = 0, vollbild = 0, balkenBreite = 0;
|
||||
for (MethodNode m : cn.methods) {
|
||||
List<AbstractInsnNode> a = befehle(m);
|
||||
for (int i = 0; i < a.size(); i++) {
|
||||
if (!ladeAlt) {
|
||||
// color(255, 239, 50, 61) – Mojang-Rot
|
||||
if (i + 4 < a.size() && wert(a.get(i)) == 255 && wert(a.get(i + 1)) == 239
|
||||
&& wert(a.get(i + 2)) == 50 && wert(a.get(i + 3)) == 61 && aufruf(a.get(i + 4), "(IIII)I")) {
|
||||
setze(m, a, i + 1, r); setze(m, a, i + 2, g); setze(m, a, i + 3, b);
|
||||
hintergrund++;
|
||||
}
|
||||
// color(alpha, 255, 255, 255) – weißer Balken
|
||||
if (i + 4 < a.size() && a.get(i).getOpcode() == Opcodes.ILOAD && wert(a.get(i + 1)) == 255
|
||||
&& wert(a.get(i + 2)) == 255 && wert(a.get(i + 3)) == 255 && aufruf(a.get(i + 4), "(IIII)I")) {
|
||||
setze(m, a, i + 1, br); setze(m, a, i + 2, bg); setze(m, a, i + 3, bb);
|
||||
balken++;
|
||||
}
|
||||
// Logo-Fläche auf Vollbild: Höhe = min(Breite·0,75; Höhe)·0,25 und Breite = 4·Höhe
|
||||
// wird zu Höhe = min(Breite·9/16; Höhe) und Breite = 16/9·Höhe (Bild im 16:9-Format)
|
||||
AbstractInsnNode n = a.get(i);
|
||||
if (n instanceof LdcInsnNode && ((LdcInsnNode) n).cst instanceof Double) {
|
||||
double d = (Double) ((LdcInsnNode) n).cst;
|
||||
double ersatzWert = d == 0.75 ? 9.0 / 16.0 : d == 0.25 ? 1.0 : d == 4.0 ? 16.0 / 9.0 : -1;
|
||||
if (ersatzWert > 0) {
|
||||
LdcInsnNode e = new LdcInsnNode(ersatzWert);
|
||||
m.instructions.set(n, e);
|
||||
a.set(i, e);
|
||||
vollbild++;
|
||||
}
|
||||
}
|
||||
// Fortschrittsbalken: Minecraft macht ihn so breit wie das Logo – jetzt also
|
||||
// bildschirmbreit; beim Aufruf auf einen Teil der Breite in der Mitte kürzen
|
||||
if (n instanceof MethodInsnNode && ((MethodInsnNode) n).owner.equals(name)
|
||||
&& ((MethodInsnNode) n).desc.endsWith("IIIIF)V")) {
|
||||
m.instructions.insertBefore(n, balkenKuerzen(m));
|
||||
balkenBreite++;
|
||||
}
|
||||
} else {
|
||||
AbstractInsnNode n = a.get(i);
|
||||
// weiße Füllung beim Ein-/Ausblenden: 0xFFFFFF | alpha << 24
|
||||
if (n instanceof LdcInsnNode && Integer.valueOf(0xFFFFFF).equals(((LdcInsnNode) n).cst)) {
|
||||
m.instructions.set(n, ersatz(Design.HINTERGRUND, a, i));
|
||||
hintergrund++;
|
||||
}
|
||||
// fill(0, 0, breite, hoehe, -1) – weißer Hintergrund
|
||||
if (i + 5 < a.size() && n.getOpcode() == Opcodes.ICONST_0 && a.get(i + 1).getOpcode() == Opcodes.ICONST_0
|
||||
&& a.get(i + 2).getOpcode() == Opcodes.ILOAD && a.get(i + 3).getOpcode() == Opcodes.ILOAD
|
||||
&& a.get(i + 4).getOpcode() == Opcodes.ICONST_M1 && aufruf(a.get(i + 5), "(IIIII)V")) {
|
||||
m.instructions.set(a.get(i + 4), ersatz(0xFF000000 | Design.HINTERGRUND, a, i + 4));
|
||||
hintergrund++;
|
||||
}
|
||||
// Balken-Rot (226, 40, 55) als Gleitkommazahlen
|
||||
if (n instanceof LdcInsnNode && ((LdcInsnNode) n).cst instanceof Float) {
|
||||
float f = (Float) ((LdcInsnNode) n).cst;
|
||||
float neuWert = f == 226f ? br : f == 40f ? bg : f == 55f ? bb : -1f;
|
||||
if (neuWert >= 0) {
|
||||
LdcInsnNode e = new LdcInsnNode(neuWert);
|
||||
m.instructions.set(n, e);
|
||||
a.set(i, e);
|
||||
balken++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.println("[AeroMC] Ladebildschirm angepasst (" + (ladeAlt ? "1.14/1.15" : "ab 1.16")
|
||||
+ ", Hintergrund " + hintergrund + ", Balken " + balken + ", Vollbild " + vollbild + ", Balkenbreite " + balkenBreite + ")");
|
||||
return schreibe(cn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vor dem Aufruf drawProgressBar(…, x1, y1, x2, y2, f): die vier Zahlen und f in
|
||||
* neue lokale Variablen legen, x1/x2 um die Mitte auf BALKEN_ANTEIL der Breite
|
||||
* zusammenziehen und alles wieder auf den Stapel legen.
|
||||
*/
|
||||
private static InsnList balkenKuerzen(MethodNode m) {
|
||||
int b = m.maxLocals;
|
||||
m.maxLocals += 7;
|
||||
int x1 = b, y1 = b + 1, x2 = b + 2, y2 = b + 3, f = b + 4, mitte = b + 5, halb = b + 6;
|
||||
InsnList l = new InsnList();
|
||||
l.add(new VarInsnNode(Opcodes.FSTORE, f));
|
||||
l.add(new VarInsnNode(Opcodes.ISTORE, y2));
|
||||
l.add(new VarInsnNode(Opcodes.ISTORE, x2));
|
||||
l.add(new VarInsnNode(Opcodes.ISTORE, y1));
|
||||
l.add(new VarInsnNode(Opcodes.ISTORE, x1));
|
||||
// mitte = (x1 + x2) / 2
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, x1));
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, x2));
|
||||
l.add(new InsnNode(Opcodes.IADD));
|
||||
l.add(new InsnNode(Opcodes.ICONST_2));
|
||||
l.add(new InsnNode(Opcodes.IDIV));
|
||||
l.add(new VarInsnNode(Opcodes.ISTORE, mitte));
|
||||
// halb = (int) ((x2 - x1) * anteil / 2)
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, x2));
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, x1));
|
||||
l.add(new InsnNode(Opcodes.ISUB));
|
||||
l.add(new InsnNode(Opcodes.I2F));
|
||||
l.add(new LdcInsnNode(Design.BALKEN_ANTEIL / 2f));
|
||||
l.add(new InsnNode(Opcodes.FMUL));
|
||||
l.add(new InsnNode(Opcodes.F2I));
|
||||
l.add(new VarInsnNode(Opcodes.ISTORE, halb));
|
||||
// (mitte - halb, y1, mitte + halb, y2, f)
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, mitte));
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, halb));
|
||||
l.add(new InsnNode(Opcodes.ISUB));
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, y1));
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, mitte));
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, halb));
|
||||
l.add(new InsnNode(Opcodes.IADD));
|
||||
l.add(new VarInsnNode(Opcodes.ILOAD, y2));
|
||||
l.add(new VarInsnNode(Opcodes.FLOAD, f));
|
||||
return l;
|
||||
}
|
||||
|
||||
// ---------- 1b. Titel-Logo im Hauptmenü ----------
|
||||
// Minecraft zeichnet vom Logo nur einen 44 Punkte hohen Streifen – zu knapp für
|
||||
// Pyramide, großen Schriftzug und Untertitel. Hier wird er auf 64 Punkte erweitert:
|
||||
// bis 1.19.4: blit(u 0, v 0, 155, 44) + blit(u 0, v 45, 155, 44) -> Höhe 64, zweites Stück ab v 65
|
||||
// ab 1.20: blit(…, 256, 44, Textur 256, 64) -> Höhe 64
|
||||
private byte[] titelLogo(String name, byte[] klasse) {
|
||||
ClassNode cn = lies(klasse);
|
||||
int n = 0;
|
||||
for (MethodNode m : cn.methods) {
|
||||
List<AbstractInsnNode> a = befehle(m);
|
||||
for (int i = 0; i + 4 < a.size(); i++) {
|
||||
int w0 = wert(a.get(i)), w1 = wert(a.get(i + 1)), w2 = wert(a.get(i + 2)), w3 = wert(a.get(i + 3));
|
||||
boolean aufrufDanach = a.get(i + 4) instanceof MethodInsnNode;
|
||||
if (w0 == 0 && w1 == 0 && w2 == 155 && w3 == 44 && aufrufDanach) {
|
||||
setze(m, a, i + 3, 64); n++;
|
||||
} else if (w0 == 0 && w1 == 45 && w2 == 155 && w3 == 44 && aufrufDanach) {
|
||||
setze(m, a, i + 1, 65); setze(m, a, i + 3, 64); n++;
|
||||
} else if (w0 == 256 && w1 == 44 && w2 == 256 && w3 == 64) {
|
||||
setze(m, a, i + 1, 64); n++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (n == 0) return null;
|
||||
System.out.println("[AeroMC] Titel-Logo vergroessert (" + n + ")");
|
||||
return schreibe(cn);
|
||||
}
|
||||
|
||||
// ---------- 2. Logo-Textur ----------
|
||||
|
||||
private byte[] logoStrom(byte[] klasse, boolean alt) {
|
||||
ClassNode cn = lies(klasse);
|
||||
int n = 0;
|
||||
for (MethodNode m : cn.methods) {
|
||||
for (AbstractInsnNode i : befehle(m)) {
|
||||
if (i.getOpcode() != Opcodes.INVOKESTATIC) continue;
|
||||
MethodInsnNode mi = (MethodInsnNode) i;
|
||||
if (!mi.desc.startsWith("(Ljava/io/InputStream;)L") || mi.owner.startsWith("java/")) continue;
|
||||
// Stapel: [strom] -> [strom, helfer] -> [helfer, strom] -> apply -> [neuer strom]
|
||||
InsnList neu = new InsnList();
|
||||
helfer(neu, alt ? Design.LOGO_ALT : Design.LOGO);
|
||||
neu.add(new InsnNode(Opcodes.SWAP));
|
||||
neu.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, "java/util/function/Function", "apply",
|
||||
"(Ljava/lang/Object;)Ljava/lang/Object;", true));
|
||||
neu.add(new TypeInsnNode(Opcodes.CHECKCAST, "java/io/InputStream"));
|
||||
m.instructions.insertBefore(i, neu);
|
||||
n++;
|
||||
}
|
||||
}
|
||||
if (n == 0) return null;
|
||||
System.out.println("[AeroMC] Ladelogo ersetzt");
|
||||
return schreibe(cn);
|
||||
}
|
||||
|
||||
// ---------- 3. Fenstertitel und -symbol ----------
|
||||
|
||||
private byte[] glfw(byte[] klasse) {
|
||||
ClassNode cn = lies(klasse);
|
||||
int n = 0;
|
||||
for (MethodNode m : cn.methods) {
|
||||
String schluessel = null, typ = null;
|
||||
if ("glfwCreateWindow".equals(m.name) && "(IILjava/lang/CharSequence;JJ)J".equals(m.desc)) {
|
||||
schluessel = Design.TITEL; typ = "java/lang/CharSequence";
|
||||
} else if ("glfwSetWindowTitle".equals(m.name) && "(JLjava/lang/CharSequence;)V".equals(m.desc)) {
|
||||
schluessel = Design.TITEL; typ = "java/lang/CharSequence";
|
||||
} else if ("glfwSetWindowIcon".equals(m.name) && "(JLorg/lwjgl/glfw/GLFWImage$Buffer;)V".equals(m.desc)) {
|
||||
schluessel = Design.ICON; typ = "org/lwjgl/glfw/GLFWImage$Buffer";
|
||||
}
|
||||
if (schluessel == null) continue;
|
||||
// am Anfang: argument = helfer.apply(argument) (Argument liegt in beiden Fällen in Slot 2)
|
||||
InsnList neu = new InsnList();
|
||||
helfer(neu, schluessel);
|
||||
neu.add(new VarInsnNode(Opcodes.ALOAD, 2));
|
||||
neu.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, "java/util/function/Function", "apply",
|
||||
"(Ljava/lang/Object;)Ljava/lang/Object;", true));
|
||||
neu.add(new TypeInsnNode(Opcodes.CHECKCAST, typ));
|
||||
neu.add(new VarInsnNode(Opcodes.ASTORE, 2));
|
||||
m.instructions.insert(neu);
|
||||
n++;
|
||||
}
|
||||
if (n == 0) return null;
|
||||
System.out.println("[AeroMC] Fenstertitel und -symbol vorbereitet (" + n + ")");
|
||||
return schreibe(cn);
|
||||
}
|
||||
|
||||
// ---------- Hilfen ----------
|
||||
|
||||
/** legt (Function) System.getProperties().get(schluessel) auf den Stapel */
|
||||
private static void helfer(InsnList l, String schluessel) {
|
||||
l.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System", "getProperties", "()Ljava/util/Properties;", false));
|
||||
l.add(new LdcInsnNode(schluessel));
|
||||
l.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/util/Properties", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", false));
|
||||
l.add(new TypeInsnNode(Opcodes.CHECKCAST, "java/util/function/Function"));
|
||||
}
|
||||
|
||||
private static ClassNode lies(byte[] klasse) {
|
||||
ClassNode cn = new ClassNode();
|
||||
new ClassReader(klasse).accept(cn, 0);
|
||||
return cn;
|
||||
}
|
||||
|
||||
private static byte[] schreibe(ClassNode cn) {
|
||||
ClassWriter w = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
cn.accept(w);
|
||||
return w.toByteArray();
|
||||
}
|
||||
|
||||
/** echte Befehle eines Methodenrumpfs (ohne Labels, Zeilennummern, Frames) */
|
||||
private static List<AbstractInsnNode> befehle(MethodNode m) {
|
||||
List<AbstractInsnNode> l = new ArrayList<>();
|
||||
for (AbstractInsnNode i = m.instructions.getFirst(); i != null; i = i.getNext()) {
|
||||
if (i.getOpcode() >= 0) l.add(i);
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
private static boolean hatText(ClassNode cn, String text) {
|
||||
for (MethodNode m : cn.methods) {
|
||||
for (AbstractInsnNode i : befehle(m)) {
|
||||
if (i instanceof LdcInsnNode && text.equals(((LdcInsnNode) i).cst)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Zahlenwert eines Konstanten-Befehls, sonst KEIN_WERT */
|
||||
private static int wert(AbstractInsnNode i) {
|
||||
int op = i.getOpcode();
|
||||
if (op >= Opcodes.ICONST_M1 && op <= Opcodes.ICONST_5) return op - Opcodes.ICONST_0;
|
||||
if (op == Opcodes.BIPUSH || op == Opcodes.SIPUSH) return ((IntInsnNode) i).operand;
|
||||
if (i instanceof LdcInsnNode && ((LdcInsnNode) i).cst instanceof Integer) return (Integer) ((LdcInsnNode) i).cst;
|
||||
return KEIN_WERT;
|
||||
}
|
||||
|
||||
private static boolean aufruf(AbstractInsnNode i, String desc) {
|
||||
return (i.getOpcode() == Opcodes.INVOKESTATIC || i.getOpcode() == Opcodes.INVOKEVIRTUAL)
|
||||
&& desc.equals(((MethodInsnNode) i).desc);
|
||||
}
|
||||
|
||||
private static void setze(MethodNode m, List<AbstractInsnNode> a, int index, int v) {
|
||||
m.instructions.set(a.get(index), ersatz(v, a, index));
|
||||
}
|
||||
|
||||
/** neuer Konstanten-Befehl; ersetzt auch den Eintrag in der Befehlsliste */
|
||||
private static AbstractInsnNode ersatz(int v, List<AbstractInsnNode> a, int index) {
|
||||
AbstractInsnNode n = v >= -32768 && v <= 32767 ? new IntInsnNode(Opcodes.SIPUSH, v) : new LdcInsnNode(v);
|
||||
a.set(index, n);
|
||||
return n;
|
||||
}
|
||||
|
||||
private static boolean enthaelt(byte[] daten, byte[] muster) {
|
||||
outer:
|
||||
for (int i = 0, ende = daten.length - muster.length; i <= ende; i++) {
|
||||
for (int k = 0; k < muster.length; k++) {
|
||||
if (daten[i + k] != muster[k]) continue outer;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package de.aeromc.capes;
|
||||
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.ProtectionDomain;
|
||||
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
import org.objectweb.asm.Type;
|
||||
import org.objectweb.asm.tree.AbstractInsnNode;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.objectweb.asm.tree.FieldInsnNode;
|
||||
import org.objectweb.asm.tree.FrameNode;
|
||||
import org.objectweb.asm.tree.InsnList;
|
||||
import org.objectweb.asm.tree.InsnNode;
|
||||
import org.objectweb.asm.tree.JumpInsnNode;
|
||||
import org.objectweb.asm.tree.LabelNode;
|
||||
import org.objectweb.asm.tree.LdcInsnNode;
|
||||
import org.objectweb.asm.tree.MethodInsnNode;
|
||||
import org.objectweb.asm.tree.MethodNode;
|
||||
import org.objectweb.asm.tree.TypeInsnNode;
|
||||
import org.objectweb.asm.tree.VarInsnNode;
|
||||
|
||||
/**
|
||||
* Mod-Schalter im Pausemenü (Minecraft 1.14 … 26.x, jeder Loader):
|
||||
* Der Knopf „Feedback geben“ wird zum Knopf „Mods verwalten“.
|
||||
*
|
||||
* 1. Seine Adresse (aka.ms/javafeedback bzw. snapshotfeedback) wird zu
|
||||
* "aeromc://mods" – je nach Version steht sie im Pausemenü selbst
|
||||
* (bis 1.20) oder in CommonLinks (ab 1.21).
|
||||
* 2. Die Lambdas, über die Minecraft einen Link öffnet (im Pausemenü bzw. in
|
||||
* ConfirmLinkScreen), fragen vorher den Helfer: Ist es "aeromc://mods",
|
||||
* öffnet der Launcher das Mod-Fenster und Minecraft zeigt keine Rückfrage.
|
||||
* Alle anderen Links laufen unverändert weiter.
|
||||
*
|
||||
* Beschriftung bzw. Symbol des Knopfs kommen aus dem Ressourcenpaket.
|
||||
*/
|
||||
final class ModsUmbau implements ClassFileTransformer {
|
||||
static final String ZIEL = "aeromc://mods";
|
||||
private static final String[] ADRESSEN = {"https://aka.ms/javafeedback?ref=game", "https://aka.ms/snapshotfeedback?ref=game"};
|
||||
private static final byte[] MERKMAL_FEEDBACK = "aka.ms/".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] MERKMAL_LINK = "chat.link.confirm".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
@Override
|
||||
public byte[] transform(ClassLoader loader, String name, Class<?> umdefiniert, ProtectionDomain pd, byte[] klasse) {
|
||||
if (name == null || umdefiniert != null || name.startsWith("java/") || name.startsWith("jdk/")
|
||||
|| name.startsWith("sun/") || name.startsWith("de/aeromc/") || name.startsWith("org/lwjgl/")) return null;
|
||||
try {
|
||||
boolean feedback = enthaelt(klasse, MERKMAL_FEEDBACK);
|
||||
boolean link = enthaelt(klasse, MERKMAL_LINK);
|
||||
if (!feedback && !link) return null;
|
||||
ClassNode cn = new ClassNode();
|
||||
new ClassReader(klasse).accept(cn, 0);
|
||||
int adressen = 0, waechter = 0;
|
||||
for (MethodNode m : cn.methods) {
|
||||
for (AbstractInsnNode i = m.instructions.getFirst(); i != null; i = i.getNext()) {
|
||||
if (i instanceof LdcInsnNode && istFeedbackAdresse(((LdcInsnNode) i).cst)) {
|
||||
((LdcInsnNode) i).cst = ZIEL;
|
||||
adressen++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (adressen == 0 && !link) return null;
|
||||
for (MethodNode m : cn.methods) {
|
||||
if ((m.access & Opcodes.ACC_SYNTHETIC) == 0 || Type.getReturnType(m.desc) != Type.VOID_TYPE) continue;
|
||||
int slot = adressParameter(m);
|
||||
boolean konstante = slot < 0 && enthaeltZiel(m);
|
||||
if (slot < 0 && !konstante) continue;
|
||||
if (frameAmAnfang(m)) continue; // nicht sicher erweiterbar
|
||||
InsnList w = new InsnList();
|
||||
w.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/lang/System", "getProperties", "()Ljava/util/Properties;", false));
|
||||
w.add(new LdcInsnNode(Design.MODS));
|
||||
w.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/util/Properties", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", false));
|
||||
w.add(new TypeInsnNode(Opcodes.CHECKCAST, "java/util/function/Function"));
|
||||
if (slot >= 0) w.add(new VarInsnNode(Opcodes.ALOAD, slot)); else w.add(new LdcInsnNode(ZIEL));
|
||||
w.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, "java/util/function/Function", "apply", "(Ljava/lang/Object;)Ljava/lang/Object;", true));
|
||||
w.add(new FieldInsnNode(Opcodes.GETSTATIC, "java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;"));
|
||||
LabelNode weiter = new LabelNode();
|
||||
w.add(new JumpInsnNode(Opcodes.IF_ACMPNE, weiter));
|
||||
w.add(new InsnNode(Opcodes.RETURN));
|
||||
w.add(weiter);
|
||||
w.add(new FrameNode(Opcodes.F_SAME, 0, null, 0, null));
|
||||
m.instructions.insert(w);
|
||||
waechter++;
|
||||
}
|
||||
if (adressen == 0 && waechter == 0) return null;
|
||||
System.out.println("[AeroMC] Mod-Schalter vorbereitet (Adressen " + adressen + ", Links " + waechter + ")");
|
||||
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
cn.accept(cw);
|
||||
return cw.toByteArray();
|
||||
} catch (Throwable t) {
|
||||
System.out.println("[AeroMC] Mod-Schalter: " + name + " nicht angepasst: " + t);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean istFeedbackAdresse(Object cst) {
|
||||
for (String a : ADRESSEN) if (a.equals(cst)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Slot des ersten Parameters vom Typ String oder URI, sonst -1 */
|
||||
private static int adressParameter(MethodNode m) {
|
||||
int slot = (m.access & Opcodes.ACC_STATIC) != 0 ? 0 : 1;
|
||||
for (Type t : Type.getArgumentTypes(m.desc)) {
|
||||
String d = t.getDescriptor();
|
||||
if ("Ljava/lang/String;".equals(d) || "Ljava/net/URI;".equals(d)) return slot;
|
||||
slot += t.getSize();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static boolean enthaeltZiel(MethodNode m) {
|
||||
for (AbstractInsnNode i = m.instructions.getFirst(); i != null; i = i.getNext()) {
|
||||
if (i instanceof LdcInsnNode && ZIEL.equals(((LdcInsnNode) i).cst)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** steht vor dem ersten echten Befehl schon ein Frame (Sprungziel am Anfang)? */
|
||||
private static boolean frameAmAnfang(MethodNode m) {
|
||||
for (AbstractInsnNode i = m.instructions.getFirst(); i != null; i = i.getNext()) {
|
||||
if (i instanceof FrameNode) return true;
|
||||
if (i.getOpcode() >= 0) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean enthaelt(byte[] daten, byte[] muster) {
|
||||
outer:
|
||||
for (int i = 0, ende = daten.length - muster.length; i <= ende; i++) {
|
||||
for (int k = 0; k < muster.length; k++) if (daten[i + k] != muster[k]) continue outer;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+80
-80
@@ -1,80 +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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Enumeration;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.commons.ClassRemapper;
|
||||
import org.objectweb.asm.commons.Remapper;
|
||||
|
||||
/**
|
||||
* Baut die Agent-Datei: eigene Klassen + ASM, dabei org/objectweb/asm nach
|
||||
* de/aeromc/capes/asm umbenannt. So kann das ASM von Fabric/Forge (das auf
|
||||
* demselben Klassenpfad liegt) nie mit unserem verwechselt werden.
|
||||
*
|
||||
* Aufruf: java Shade <ziel.jar> <klassenordner> <asm.jar> [weitere ASM-Jars, z. B. asm-tree]
|
||||
*/
|
||||
public class Shade {
|
||||
static final Remapper UMBENENNEN = new Remapper() {
|
||||
@Override
|
||||
public String map(String name) {
|
||||
return name.startsWith("org/objectweb/asm/") ? "de/aeromc/capes/asm/" + name.substring(18) : name;
|
||||
}
|
||||
};
|
||||
|
||||
static byte[] umbenennen(byte[] klasse) {
|
||||
ClassReader r = new ClassReader(klasse);
|
||||
ClassWriter w = new ClassWriter(0);
|
||||
r.accept(new ClassRemapper(w, UMBENENNEN), 0);
|
||||
return w.toByteArray();
|
||||
}
|
||||
|
||||
static byte[] lies(InputStream in) throws Exception {
|
||||
ByteArrayOutputStream b = new ByteArrayOutputStream();
|
||||
byte[] p = new byte[8192];
|
||||
int n;
|
||||
while ((n = in.read(p)) > 0) b.write(p, 0, n);
|
||||
return b.toByteArray();
|
||||
}
|
||||
|
||||
public static void main(String[] a) throws Exception {
|
||||
Manifest mf = new Manifest();
|
||||
Attributes at = mf.getMainAttributes();
|
||||
at.put(Attributes.Name.MANIFEST_VERSION, "1.0");
|
||||
at.putValue("Premain-Class", "de.aeromc.capes.Agent");
|
||||
at.putValue("Agent-Class", "de.aeromc.capes.Agent");
|
||||
at.putValue("Can-Retransform-Classes", "false");
|
||||
at.putValue("Implementation-Title", "AeroMC Agent (Umhaenge, Spieldesign)");
|
||||
try (JarOutputStream out = new JarOutputStream(new FileOutputStream(a[0]), mf)) {
|
||||
// eigene Klassen
|
||||
File basis = new File(a[1]);
|
||||
for (File f : Files.walk(basis.toPath()).map(p -> p.toFile()).filter(f -> f.getName().endsWith(".class")).toArray(File[]::new)) {
|
||||
String name = basis.toPath().relativize(f.toPath()).toString().replace('\\', '/');
|
||||
out.putNextEntry(new JarEntry(name));
|
||||
out.write(umbenennen(Files.readAllBytes(f.toPath())));
|
||||
out.closeEntry();
|
||||
}
|
||||
// ASM (umbenannt), ohne Modul-Infos und Signaturen
|
||||
for (int j = 2; j < a.length; j++) try (JarFile asm = new JarFile(a[j])) {
|
||||
Enumeration<JarEntry> e = asm.entries();
|
||||
while (e.hasMoreElements()) {
|
||||
JarEntry je = e.nextElement();
|
||||
String n = je.getName();
|
||||
if (!n.startsWith("org/objectweb/asm/") || !n.endsWith(".class")) continue;
|
||||
byte[] neu = umbenennen(lies(asm.getInputStream(je)));
|
||||
out.putNextEntry(new JarEntry("de/aeromc/capes/asm/" + n.substring(18)));
|
||||
out.write(neu);
|
||||
out.closeEntry();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user