diff --git a/agent/src/de/aeromc/capes/Agent.java b/agent/src/de/aeromc/capes/Agent.java index 6593332..68dd511 100644 --- a/agent/src/de/aeromc/capes/Agent.java +++ b/agent/src/de/aeromc/capes/Agent.java @@ -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); + } } } } diff --git a/agent/src/de/aeromc/capes/Design.java b/agent/src/de/aeromc/capes/Design.java new file mode 100644 index 0000000..2d51c28 --- /dev/null +++ b/agent/src/de/aeromc/capes/Design.java @@ -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 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; + } + } +} diff --git a/agent/src/de/aeromc/capes/DesignUmbau.java b/agent/src/de/aeromc/capes/DesignUmbau.java new file mode 100644 index 0000000..a7847df --- /dev/null +++ b/agent/src/de/aeromc/capes/DesignUmbau.java @@ -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 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; + } +} diff --git a/agent/src/de/aeromc/capes/ModsUmbau.java b/agent/src/de/aeromc/capes/ModsUmbau.java new file mode 100644 index 0000000..fe6fb4f --- /dev/null +++ b/agent/src/de/aeromc/capes/ModsUmbau.java @@ -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; + } +} diff --git a/agent/tools/Shade.java b/agent/tools/Shade.java index 32e52cb..b5bff3a 100644 --- a/agent/tools/Shade.java +++ b/agent/tools/Shade.java @@ -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 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 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(); + } + } + } + } +} diff --git a/installer/Installer.cs b/installer/Installer.cs index b45863f..7a7b68e 100644 --- a/installer/Installer.cs +++ b/installer/Installer.cs @@ -181,9 +181,35 @@ public static class Installer k.SetValue("NoRepair", 1, RegistryValueKind.DWord); } + progress.Report((0.98, "Installation wird abgeschlossen …")); + Vorwaermen(exe); + progress.Report((1.0, "Fertig")); } + /// + /// Startet AeroMC einmal kurz mit "--aeromc-vorwaermen" – die App beendet sich + /// damit sofort wieder (siehe src/main.js). Windows Defender prüft eine neue, + /// noch unbekannte EXE beim ersten Start mehrere Sekunden lang (gemessen: ~8 s); + /// das passiert so während des Fortschrittsbalkens und nicht erst beim Klick + /// auf "Fertigstellen". Das Ergebnis merkt sich Windows, der echte Start ist + /// danach sofort da. + /// + private static void Vorwaermen(string exe) + { + try + { + using var p = Process.Start(new ProcessStartInfo(exe, "--aeromc-vorwaermen") + { + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = Path.GetDirectoryName(exe)!, + }); + if (p != null && !p.WaitForExit(30000)) { try { p.Kill(); } catch { } } + } + catch { /* nur eine Beschleunigung – Fehler sind egal */ } + } + private static List TopLevelNames(ZipArchive zip) => zip.Entries .Select(e => e.FullName.Replace('\\', '/').Split('/')[0]) diff --git a/installer/MainWindow.xaml.cs b/installer/MainWindow.xaml.cs index b7606ca..6a55996 100644 --- a/installer/MainWindow.xaml.cs +++ b/installer/MainWindow.xaml.cs @@ -300,7 +300,16 @@ public partial class MainWindow : Window await RunInstall(DirBox.Text.Trim(), DesktopLinkBox.IsChecked == true, update: false); break; case PageNumProgress: - if (LaunchBox.IsChecked == true) StartApp(); + // Start im Hintergrund: Process.Start kann blockieren, solange Windows + // die EXE prüft – das Fenster darf dabei nie einfrieren + if (LaunchBox.IsChecked == true) + { + _busy = true; + NextButton.IsEnabled = false; + NextButton.Content = "AeroMC startet …"; + await Task.Run(StartApp); + _busy = false; + } Close(); break; } diff --git a/installer/obj/AeroMCSetup.csproj.nuget.dgspec.json b/installer/obj/AeroMCSetup.csproj.nuget.dgspec.json index 31aecad..fc86f45 100644 --- a/installer/obj/AeroMCSetup.csproj.nuget.dgspec.json +++ b/installer/obj/AeroMCSetup.csproj.nuget.dgspec.json @@ -5,7 +5,7 @@ }, "projects": { "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj": { - "version": "1.2.1", + "version": "1.2.3", "restore": { "projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj", "projectName": "AeroMC-Setup", diff --git a/installer/obj/Release/net48/AeroMC-Setup.exe b/installer/obj/Release/net48/AeroMC-Setup.exe index a468912..4efa8c1 100644 Binary files a/installer/obj/Release/net48/AeroMC-Setup.exe and b/installer/obj/Release/net48/AeroMC-Setup.exe differ diff --git a/installer/obj/Release/net48/AeroMCSetup.AssemblyInfo.cs b/installer/obj/Release/net48/AeroMCSetup.AssemblyInfo.cs index 53e4a4b..7d9e514 100644 --- a/installer/obj/Release/net48/AeroMCSetup.AssemblyInfo.cs +++ b/installer/obj/Release/net48/AeroMCSetup.AssemblyInfo.cs @@ -14,11 +14,11 @@ using System.Reflection; [assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] [assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper. Alle Rechte vorbehalten.")] [assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert AeroMC")] -[assembly: System.Reflection.AssemblyFileVersionAttribute("1.2.1.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.2.1")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.2.3.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.2.3")] [assembly: System.Reflection.AssemblyProductAttribute("AeroMC Setup")] [assembly: System.Reflection.AssemblyTitleAttribute("AeroMC Setup")] -[assembly: System.Reflection.AssemblyVersionAttribute("1.2.1.0")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.2.3.0")] [assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")] // Von der MSBuild WriteCodeFragment-Klasse generiert. diff --git a/installer/obj/Release/net48/AeroMCSetup.AssemblyInfoInputs.cache b/installer/obj/Release/net48/AeroMCSetup.AssemblyInfoInputs.cache index 54445a8..c4d0b92 100644 --- a/installer/obj/Release/net48/AeroMCSetup.AssemblyInfoInputs.cache +++ b/installer/obj/Release/net48/AeroMCSetup.AssemblyInfoInputs.cache @@ -1 +1 @@ -a17a2bc3b05635d3c29adc5c6884bde0a77826489d54b55a9b7744bfaa1ef206 +111db8ee3dc7193961107c73afd8913fff02b92fd4e49b8b7e4bf051181600bd diff --git a/installer/obj/project.assets.json b/installer/obj/project.assets.json index 8f5d57f..1c28bfb 100644 --- a/installer/obj/project.assets.json +++ b/installer/obj/project.assets.json @@ -418,7 +418,7 @@ "C:\\Users\\Viper\\.nuget\\packages\\": {} }, "project": { - "version": "1.2.1", + "version": "1.2.3", "restore": { "projectUniqueName": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj", "projectName": "AeroMC-Setup", diff --git a/installer/obj/project.nuget.cache b/installer/obj/project.nuget.cache index 9e61586..2a18427 100644 --- a/installer/obj/project.nuget.cache +++ b/installer/obj/project.nuget.cache @@ -1,6 +1,6 @@ { "version": 2, - "dgSpecHash": "zBMXx+S6DFU=", + "dgSpecHash": "5nf4o8NXPS0=", "success": true, "projectFilePath": "C:\\Users\\Viper\\Desktop\\AeroMC-Launcher\\installer\\AeroMCSetup.csproj", "expectedPackageFiles": [