Skip to content

Commit

Permalink
Add an auto discovery plugin for mixins
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nea89o committed Feb 5, 2024
1 parent d9c6bd7 commit 09cd280
Show file tree
Hide file tree
Showing 3 changed files with 192 additions and 7 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
188 changes: 188 additions & 0 deletions src/main/java/com/example/init/AutoDiscoveryMixinPlugin.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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<AutoDiscoveryMixinPlugin> mixinPlugins = new ArrayList<>();

public static List<AutoDiscoveryMixinPlugin> 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<String> 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.
* <p><b>This method cannot be called after mixin initialization.</p>
*
* @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<String> 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<Path> 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<String> myTargets, Set<String> otherTargets) {

}
}
9 changes: 3 additions & 6 deletions src/main/resources/mixins.examplemod.json
Original file line number Diff line number Diff line change
@@ -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."
}

0 comments on commit 09cd280

Please sign in to comment.