353 lines
18 KiB
Java
353 lines
18 KiB
Java
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;
|
||
}
|
||
}
|