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 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 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 befehle(MethodNode m) { List 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 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 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; } }