package net.minecraftforkage.setup_plugin; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.ClassNode; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import net.minecraftforkage.instsetup.AbstractZipFile; import net.minecraftforkage.instsetup.JarTransformer; import net.minecraftforkage.instsetup.PackerContext; public class FMLModSearchTransformer extends JarTransformer { private static class ProxyFieldInjectionData { String clientSideClass; String serverSideClass; } private static class ModInstanceInjectionData { String mod; } private static class FieldInjectionEntry { String className; String fieldName; String type; Object data; FieldInjectionEntry(String clazz, String field, String type, Object extraData) { this.className = clazz; this.fieldName = field; this.type = type; this.data = extraData; } } private List<Object> mods = new ArrayList<>(); private Map<String, String> modClassesForAutoInstanceSelection = new HashMap<>(); // class name -> mod ID // one of the few places in FML where the word "inject" is actually correct private List<FieldInjectionEntry> fieldsToInject = new ArrayList<>(); private class ModSearchClassVisitor extends ClassVisitor { public ModSearchClassVisitor() { super(Opcodes.ASM5); } Map<String, Object> initDataObject = new HashMap<>(); Map<String, Object> modObject = new HashMap<>(); { modObject.put("initData", initDataObject); } boolean isMod = false; boolean isModForAutoInstanceSelection = false; String className; @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { this.className = name.replace('/', '.'); } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { if(desc.equals("Lcpw/mods/fml/common/Mod;")) { return new AnnotationVisitor(Opcodes.ASM5) { Map<String, Object> fields = new HashMap<>(); @Override public void visit(String name, Object value) { fields.put(name, value); } @Override public void visitEnd() { ArrayList<Object> dependencies = new ArrayList<>(); ArrayList<Object> sortingRules = new ArrayList<>(); String dependencyString = (String)fields.remove("dependencies"); if(dependencyString != null) { for(String part : dependencyString.split(";")) { part = part.trim(); // XXX: FML compat, remove eventually if(part.equals("")) continue; int i = part.indexOf(":"); if(i < 0) throw new RuntimeException("Mod "+fields.get("modid")+" has invalid dependency string "+dependencyString); String type = part.substring(0, i); String what = part.substring(i+1); Map<String, Object> parsedOtherMod = new HashMap<>(); i = what.indexOf('@'); if(i >= 0) { parsedOtherMod.put("mod", what.substring(0, i)); parsedOtherMod.put("versionRange", what.substring(i+1)); } else { parsedOtherMod.put("mod", what); } Map<String, Object> dependency = new HashMap<>(); dependency.put("on", parsedOtherMod); if(type.startsWith("required-")) { type = type.substring(9); dependency.put("optional", false); } else { dependency.put("optional", true); } dependencies.add(dependency); Map<String, Object> sortingRule = new HashMap<>(); sortingRule.put("mod", parsedOtherMod.get("mod")); sortingRule.put("type", type); if(!type.equals("after") && !type.equals("before")) throw new RuntimeException("Mod "+fields.get("modid")+" has invalid dependency string "+dependencyString); sortingRules.add(sortingRule); } } modObject.put("modContainerClass", "cpw.mods.fml.common.FMLContainer"); modObject.put("modid", fields.remove("modid")); initDataObject.put("modAnnotation", fields); initDataObject.put("modClass", className); modObject.put("dependencies", dependencies); modObject.put("sortingRules", sortingRules); isMod = true; isModForAutoInstanceSelection = true; } }; } if(desc.equals("Lcpw/mods/fml/common/API;") && className.endsWith(".package-info")) { return new AnnotationVisitor(Opcodes.ASM5) { Map<String, Object> fields = new HashMap<>(); @Override public void visit(String name, Object value) { fields.put(name, value); } @Override public void visitEnd() { modObject.put("modid", fields.remove("provides")); initDataObject.put("modAnnotation", fields); initDataObject.put("package", className.substring(0, className.lastIndexOf('.'))); if(fields.containsKey("owner") && !fields.get("owner").equals(modObject.get("modid"))) { Map<String, Object> ownerSortingRule = new HashMap<>(); ownerSortingRule.put("mod", fields.get("owner")); ownerSortingRule.put("type", "after"); modObject.put("sortingRules", Arrays.asList(ownerSortingRule)); } else { modObject.put("sortingRules", Collections.emptyList()); } modObject.put("modContainerClass", "cpw.mods.fml.common.ModAPIManager$APIContainer"); modObject.put("dependencies", Collections.emptyList()); isMod = true; } }; } return null; } @Override public void visitEnd() { if(isMod) { mods.add(modObject); if(isModForAutoInstanceSelection) modClassesForAutoInstanceSelection.put(className, (String)modObject.get("modid")); } } @Override public FieldVisitor visitField(int access, final String fieldName, String desc, String signature, Object value) { return new FieldVisitor(Opcodes.ASM5) { @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { if(desc.equals("Lcpw/mods/fml/common/SidedProxy;")) { return new AnnotationVisitor(Opcodes.ASM5) { ProxyFieldInjectionData data = new ProxyFieldInjectionData(); @Override public void visit(String name, Object value) { if(name.equals("clientSide")) data.clientSideClass = (String)value; if(name.equals("serverSide")) data.serverSideClass = (String)value; } @Override public void visitEnd() { fieldsToInject.add(new FieldInjectionEntry(className, fieldName, "sided-proxy", data)); } }; } if(desc.equals("Lcpw/mods/fml/common/Mod$Instance;")) { return new AnnotationVisitor(Opcodes.ASM5) { ModInstanceInjectionData data = new ModInstanceInjectionData(); @Override public void visit(String name, Object value) { if(name.equals("value")) data.mod = (String)value; } @Override public void visitEnd() { fieldsToInject.add(new FieldInjectionEntry(className, fieldName, "mod-instance", data)); } }; } if(desc.equals("Lcpw/mods/fml/common/Mod$Metadata;")) { return new AnnotationVisitor(Opcodes.ASM5) { ModInstanceInjectionData data = new ModInstanceInjectionData(); @Override public void visit(String name, Object value) { if(name.equals("value")) data.mod = (String)value; } @Override public void visitEnd() { fieldsToInject.add(new FieldInjectionEntry(className, fieldName, "mod-metadata", data)); } }; } return null; } }; } } @Override public String getID() { return "MinecraftForkage|FMLModFinder"; } @Override public Stage getStage() { return Stage.MOD_IDENTIFICATION_STAGE; } @Override public void transform(AbstractZipFile zipFile, PackerContext context) throws Exception { for(String filename : zipFile.getFileNames()) { if(filename.endsWith(".class")) { try (InputStream in = zipFile.read(filename)) { new ClassReader(in).accept(new ModSearchClassVisitor(), ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); } } } Map<String, String> classToSourceMap = zipFile.readProperties("mcforkage-class-to-source-map.properties"); // fill in mod value for mod-instance injections where it's unknown // - it needs to be any mod from the same original JAR file for(FieldInjectionEntry obj : fieldsToInject) if(obj.data instanceof ModInstanceInjectionData) { ModInstanceInjectionData ifi = (ModInstanceInjectionData)obj.data; if(ifi.mod == null) { if(!classToSourceMap.containsKey(obj.className)) throw new RuntimeException(obj.className+"."+obj.fieldName+" is marked @Instance(), but from an unknown source so we can't find a mod ID to fill it with"); ifi.mod = findModBySource(classToSourceMap, classToSourceMap.get(obj.className)); if(ifi.mod == null) throw new RuntimeException(obj.className+"."+obj.fieldName+" is marked @Instance(), but from a source with no mods so we can't find a mod ID to fill it with"); } } zipFile.appendGSONArray("mcforkage-installed-mods.json", mods); zipFile.appendGSONArray("mcforkage-fields-to-inject.json", fieldsToInject); } private String findModBySource(Map<String, String> classToSourceMap, String source) { for(String modClass : modClassesForAutoInstanceSelection.keySet()) if(source.equals(classToSourceMap.get(modClass))) return modClassesForAutoInstanceSelection.get(modClass); return null; } }