package net.minecraftforkage.setup_plugin; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; 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 OptionalTransformer extends JarTransformer { boolean DEBUG = Boolean.getBoolean("minecraftforkage.OptionalTransformer.debug"); static class OptionalInterfaceRecord { String modid; String iface; boolean striprefs; } static class OptionalMethodRecord { String modid; String methodName; String methodDesc; } private static class OptionalRemovalClassVisitor extends ClassVisitor { public OptionalRemovalClassVisitor() { super(Opcodes.ASM5); } String className; List<OptionalInterfaceRecord> interfaces = new ArrayList<>(); List<OptionalMethodRecord> methods = new ArrayList<>(); @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { className = name; } private class OptionalInterfaceAnnotationVisitor extends AnnotationVisitor { public OptionalInterfaceAnnotationVisitor() { super(Opcodes.ASM5); } OptionalInterfaceRecord record = new OptionalInterfaceRecord(); @Override public void visit(String name, Object value) { switch(name) { case "iface": record.iface = (String)value; break; case "modid": record.modid = (String)value; break; case "striprefs": record.striprefs = (Boolean)value; break; default: throw new RuntimeException("Unknown annotation item "+name+" on Optional.Interface annotation on "+className); } } @Override public void visitEnd() { interfaces.add(record); } } @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { if(desc.startsWith("Lcpw/mods/fml/common/Optional")) { if(desc.equals("Lcpw/mods/fml/common/Optional$Interface;")) { return new OptionalInterfaceAnnotationVisitor(); } else if(desc.equals("Lcpw/mods/fml/common/Optional$InterfaceList;")) { return new AnnotationVisitor(Opcodes.ASM5) { @Override public AnnotationVisitor visitArray(String name) { if(!name.equals("value")) throw new RuntimeException("Unknown annotation item "+name+" on Optional.InterfaceList annotation on "+className); return this; } @Override public AnnotationVisitor visitAnnotation(String name,String desc) { if(name != null || !desc.equals("Lcpw/mods/fml/common/Optional$Interface;")) throw new RuntimeException("Unknown annotation item "+name+" of type "+desc+" on Optional.InterfaceList annotation on "+className); return new OptionalInterfaceAnnotationVisitor(); } }; } else throw new RuntimeException("saw unknown Optional annotation "+desc+" on "+className); } return null; } @Override public MethodVisitor visitMethod(int access, final String methodName, final String methodDesc, String signature, String[] exceptions) { return new MethodVisitor(Opcodes.ASM5) { @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { if(desc.equals("Lcpw/mods/fml/common/Optional$Method;")) { final OptionalMethodRecord record = new OptionalMethodRecord(); record.methodName = methodName; record.methodDesc = methodDesc; return new AnnotationVisitor(Opcodes.ASM5) { @Override public void visit(String name, Object value) { if(name.equals("modid")) record.modid = (String)value; else throw new RuntimeException("Unknown annotation item "+name+" on Optional.Method on "+className+"."+methodName); } @Override public void visitEnd() { methods.add(record); } }; } else if(desc.startsWith("Lcpw/mods/fml/common/Optional")) { throw new RuntimeException("saw unknown Optional annotation "+desc+" on "+className+"."+methodName); } return null; } }; } } @Override public String getID() { return "MinecraftForkage|OptionalInterfaceTransformer"; } @Override public void transform(AbstractZipFile zipFile, PackerContext context) throws Exception { Set<String> installedModIDs = new HashSet<>(); JsonArray installedModsArray = zipFile.readGSON("mcforkage-installed-mods.json", JsonArray.class); for(JsonElement e : installedModsArray) installedModIDs.add(e.getAsJsonObject().get("modid").getAsString()); for(String filename : zipFile.getFileNames()) { if(filename.endsWith(".class")) { ClassWriter cw = new ClassWriter(0); ClassNode cn = new ClassNode(); try (InputStream in = zipFile.read(filename)) { new ClassReader(in).accept(cn, 0); } OptionalRemovalClassVisitor cv = new OptionalRemovalClassVisitor(); cn.accept(cv); if(cv.methods.size() == 0 && cv.interfaces.size() == 0) continue; for(OptionalInterfaceRecord optIntf : cv.interfaces) { if(!installedModIDs.contains(optIntf.modid)) { if(DEBUG) System.out.println(optIntf.modid+" is not installed; removing "+optIntf.iface+" from "+cv.className); if(!cn.interfaces.remove(optIntf.iface.replace('.', '/'))) System.err.println("Can't remove interface "+optIntf.iface+" from "+cv.className+" as it isn't there."); if(optIntf.striprefs) { // Remove any method whose signature mentions optIntf.iface String interfaceDescriptor = "L" + optIntf.iface.replace('.', '/') + ";"; Iterator<MethodNode> it = cn.methods.iterator(); while(it.hasNext()) { MethodNode method = it.next(); if(method.desc.contains(interfaceDescriptor)) it.remove(); } } } else { if(DEBUG) System.out.println(optIntf.modid+" is installed; not removing "+optIntf.iface+" from "+cv.className); } } for(OptionalMethodRecord optMethod : cv.methods) { if(!installedModIDs.contains(optMethod.modid)) { if(DEBUG) System.out.println(optMethod.modid+" is not installed; removing "+optMethod.methodName+optMethod.methodDesc+" from "+cv.className); boolean found = false; Iterator<MethodNode> it = cn.methods.iterator(); while(it.hasNext()) { MethodNode method = it.next(); if(method.name.equals(optMethod.methodName) && method.desc.equals(optMethod.methodDesc)) { it.remove(); found = true; } } if(!found) throw new AssertionError("Method to remove "+optMethod.methodName+optMethod.methodDesc+" not found in "+cv.className); } else { if(DEBUG) System.out.println(optMethod.modid+" is installed; not removing "+optMethod.methodName+" from "+cv.className); } } cn.accept(cw); try (OutputStream out = zipFile.write(filename)) { out.write(cw.toByteArray()); } } } } }