Files
AeroMc-Launcher/agent/src/de/aeromc/capes/DesignUmbau.java
T
2026-10-07 07:32:07 +00:00

353 lines
18 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}