From 09cd2809fbe4c3411c9065fd5b8a7fca52cceafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linnea=20Gr=C3=A4f?= Date: Mon, 5 Feb 2024 13:19:59 +0100 Subject: [PATCH] Add an auto discovery plugin for mixins This will automatically find mixins in the mixin plugin, not requiring the manual configuration. This is less friction, especially for newer users. Usually this isn't super advisable, or needs more configuration to avoid server/client mixins being incorrectly applied, but with the target demographic of mostly client mods, I think this is a worthwhile change. --- build.gradle.kts | 2 +- .../init/AutoDiscoveryMixinPlugin.java | 188 ++++++++++++++++++ src/main/resources/mixins.examplemod.json | 9 +- 3 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/example/init/AutoDiscoveryMixinPlugin.java diff --git a/build.gradle.kts b/build.gradle.kts index 7286e59..ddaf995 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -107,7 +107,7 @@ tasks.processResources { inputs.property("version", project.version) inputs.property("mcversion", mcVersion) inputs.property("modid", modid) - inputs.property("mixinGroup", mixinGroup) + inputs.property("basePackage", baseGroup) filesMatching(listOf("mcmod.info", "mixins.$modid.json")) { expand(inputs.properties) diff --git a/src/main/java/com/example/init/AutoDiscoveryMixinPlugin.java b/src/main/java/com/example/init/AutoDiscoveryMixinPlugin.java new file mode 100644 index 0000000..aa2f995 --- /dev/null +++ b/src/main/java/com/example/init/AutoDiscoveryMixinPlugin.java @@ -0,0 +1,188 @@ +package com.example.init; + +import org.spongepowered.asm.lib.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * A mixin plugin to automatically discover all mixins in the current JAR. + *

+ * This mixin plugin automatically scans your entire JAR (or class directory, in case of an in-IDE launch) for classes inside of your + * mixin package and registers those. It does this recursively for sub packages of the mixin package as well. This means you will need + * to only have mixin classes inside of your mixin package, which is good style anyway. + * + * @author Linnea Gräf + */ +public class AutoDiscoveryMixinPlugin implements IMixinConfigPlugin { + private static final List mixinPlugins = new ArrayList<>(); + + public static List getMixinPlugins() { + return mixinPlugins; + } + + private String mixinPackage; + + @Override + public void onLoad(String mixinPackage) { + this.mixinPackage = mixinPackage; + mixinPlugins.add(this); + } + + /** + * Resolves the base class root for a given class URL. This resolves either the JAR root, or the class file root. + * In either case the return value of this + the class name will resolve back to the original class url, or to other + * class urls for other classes. + */ + public URL getBaseUrlForClassUrl(URL classUrl) { + String string = classUrl.toString(); + if (classUrl.getProtocol().equals("jar")) { + try { + return new URL(string.substring(4).split("!")[0]); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + if (string.endsWith(".class")) { + try { + return new URL(string.replace("\\", "/") + .replace(getClass().getCanonicalName() + .replace(".", "/") + ".class", "")); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + return classUrl; + } + + /** + * Get the package that contains all the mixins. This value is set by mixin itself using {@link #onLoad}. + */ + public String getMixinPackage() { + return mixinPackage; + } + + /** + * Get the path inside the class root to the mixin package + */ + public String getMixinBaseDir() { + return mixinPackage.replace(".", "/"); + } + + /** + * A list of all discovered mixins. + */ + private List mixins = null; + + /** + * Try to add mixin class ot the mixins based on the filepath inside of the class root. + * Removes the {@code .class} file suffix, as well as the base mixin package. + *

This method cannot be called after mixin initialization.

+ * + * @param className the name or path of a class to be registered as a mixin. + */ + public void tryAddMixinClass(String className) { + String norm = (className.endsWith(".class") ? className.substring(0, className.length() - ".class".length()) : className) + .replace("\\", "/") + .replace("/", "."); + if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".")) { + mixins.add(norm.substring(getMixinPackage().length() + 1)); + } + } + + /** + * Search through the JAR or class directory to find mixins contained in {@link #getMixinPackage()} + */ + @Override + public List getMixins() { + if (mixins != null) return mixins; + System.out.println("Trying to discover mixins"); + mixins = new ArrayList<>(); + URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation(); + System.out.println("Found classes at " + classUrl); + Path file; + try { + file = Paths.get(getBaseUrlForClassUrl(classUrl).toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + System.out.println("Base directory found at " + file); + if (Files.isDirectory(file)) { + walkDir(file); + } else { + walkJar(file); + } + System.out.println("Found mixins: " + mixins); + + return mixins; + } + + /** + * Search through directory for mixin classes based on {@link #getMixinBaseDir}. + * + * @param classRoot The root directory in which classes are stored for the default package. + */ + private void walkDir(Path classRoot) { + System.out.println("Trying to find mixins from directory"); + try (Stream classes = Files.walk(classRoot.resolve(getMixinBaseDir()))) { + classes.map(it -> classRoot.relativize(it).toString()) + .forEach(this::tryAddMixinClass); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Read through a JAR file, trying to find all mixins inside. + */ + private void walkJar(Path file) { + System.out.println("Trying to find mixins from jar file"); + try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(file))) { + ZipEntry next; + while ((next = zis.getNextEntry()) != null) { + tryAddMixinClass(next.getName()); + zis.closeEntry(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + return true; + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { + + } +} diff --git a/src/main/resources/mixins.examplemod.json b/src/main/resources/mixins.examplemod.json index 570a0d0..5ea0c57 100644 --- a/src/main/resources/mixins.examplemod.json +++ b/src/main/resources/mixins.examplemod.json @@ -1,11 +1,8 @@ { - "package": "${mixinGroup}", + "package": "${basePackage}.mixin", + "plugin": "${basePackage}.init.AutoDiscoveryMixinPlugin", "refmap": "mixins.${modid}.refmap.json", "minVersion": "0.7", "compatibilityLevel": "JAVA_8", - "mixins": [ - ], - "client": [ - "MixinGuiMainMenu" - ] + "__comment": "You do not need to manually register mixins in this template. Check the auto discovery mixin plugin for more info." }