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 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) Design::titel); p.put(ICON, (Function) 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) 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 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 groessen = new ArrayList<>(); List 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; } } }