diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..b3dfee7afd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 diff --git a/src/main/scala/li/cil/oc/common/asm/ASMHelpers.java b/src/main/scala/li/cil/oc/common/asm/ASMHelpers.java new file mode 100644 index 0000000000..a377e7fc5c --- /dev/null +++ b/src/main/scala/li/cil/oc/common/asm/ASMHelpers.java @@ -0,0 +1,145 @@ +package li.cil.oc.common.asm; + +import cpw.mods.fml.common.asm.transformers.deobf.FMLDeobfuscatingRemapper; +import net.minecraft.launchwrapper.LaunchClassLoader; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.MethodNode; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.util.function.Predicate; + +public final class ASMHelpers { + + private static final Logger log = LogManager.getLogger("OpenComputers"); + + @Nullable + public static byte[] insertInto(LaunchClassLoader loader, ClassNode classNode, String[] methodNames, String[] methodDescs, Predicate inserter) { + MethodNode methodNode = null; + for (MethodNode method : classNode.methods) { + if (ArrayUtils.contains(methodNames, method.name) && ArrayUtils.contains(methodDescs, method.desc)) { + methodNode = method; + break; + } + } + + if (methodNode == null) { + log.warn("Failed patching {}.{}, method not found.", classNode.name, methodNames[0]); + return null; + } + + if (!inserter.test(methodNode.instructions)) { + log.warn("Failed patching {}.{}, injection point not found.", classNode.name, methodNames[0]); + return null; + } + + log.info("Successfully patched {}.{}.", classNode.name, methodNames[0]); + return writeClass(loader, classNode, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + } + + @Nonnull + public static byte[] writeClass(LaunchClassLoader loader, ClassNode classNode, int flags) { + ClassWriter writer = new ClassWriter(flags) { + // Implementation without class loads, avoids https://github.com/MinecraftForge/FML/issues/655 + @Override + protected String getCommonSuperClass(final String type1, final String type2) { + ClassNode node1 = classNodeFor(loader, type1); + ClassNode node2 = classNodeFor(loader, type2); + + if (isAssignable(loader, node1, node2)) return node1.name; + if (isAssignable(loader, node2, node1)) return node2.name; + if (isInterface(node1) || isInterface(node2)) return "java/lang/Object"; + + ClassNode parent = node1; + while (parent != null && parent.superName != null && !isAssignable(loader, parent, node2)) { + parent = classNodeFor(loader, parent.superName); + } + + if (parent == null) return "java/lang/Object"; + return parent.name; + } + }; + + classNode.accept(writer); + return writer.toByteArray(); + } + + @Nullable + public static ClassNode classNodeFor(LaunchClassLoader loader, String internalName) { + try { + // internalName is slash-form, e.g. "net/minecraft/tileentity/TileEntity" + String namePlain = internalName.replace('/', '.'); + + byte[] bytes = loader.getClassBytes(namePlain); + if (bytes != null) { + return newClassNode(bytes); + } + + String nameObfed = FMLDeobfuscatingRemapper.INSTANCE.unmap(internalName).replace('/', '.'); + bytes = loader.getClassBytes(nameObfed); + if (bytes == null) { + return null; + } + return newClassNode(bytes); + } catch (IOException ignored) { + return null; + } + } + + @Nonnull + public static ClassNode newClassNode(byte[] data) { + ClassNode node = new ClassNode(); + new ClassReader(data).accept(node, 0); + return node; + } + + public static boolean classExists(LaunchClassLoader loader, String name) { + try { + if (loader.getClassBytes(name) != null) return true; + if (loader.getClassBytes(FMLDeobfuscatingRemapper.INSTANCE.unmap(name)) != null) return true; + + return loader.findClass(name.replace('/', '.')) != null; + } catch (IOException | ClassNotFoundException ignored) { + return false; + } + } + + public static boolean isAssignable(LaunchClassLoader loader, ClassNode parent, ClassNode child) { + if (parent == null || child == null) return false; + if (isFinal(parent)) return false; + + if (parent.name.equals("java/lang/Object")) return true; + if (parent.name.equals(child.name)) return true; + if (parent.name.equals(child.superName)) return true; + if (child.interfaces.contains(parent.name)) return true; + + if (child.superName != null) { + return isAssignable(loader, parent, classNodeFor(loader, child.superName)); + } + + return false; + } + + public static boolean isFinal(ClassNode node) { + return (node.access & Opcodes.ACC_FINAL) != 0; + } + + public static boolean isInterface(ClassNode node) { + return node != null && (node.access & Opcodes.ACC_INTERFACE) != 0; + } + + public static MethodNode findMethod(ClassNode classNode, String name, String desc) { + for (MethodNode method : classNode.methods) { + if (name.equals(method.name) && desc.equals(method.desc)) return method; + } + return null; + } +} diff --git a/src/main/scala/li/cil/oc/common/asm/ClassTransformer.java b/src/main/scala/li/cil/oc/common/asm/ClassTransformer.java new file mode 100644 index 0000000000..6d686c338f --- /dev/null +++ b/src/main/scala/li/cil/oc/common/asm/ClassTransformer.java @@ -0,0 +1,94 @@ +package li.cil.oc.common.asm; + +import net.minecraft.launchwrapper.IClassTransformer; +import net.minecraft.launchwrapper.LaunchClassLoader; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; + +public class ClassTransformer implements IClassTransformer { + + private static final Logger log = LogManager.getLogger("OpenComputers"); + private final LaunchClassLoader loader = + (LaunchClassLoader) ClassTransformer.class.getClassLoader(); + + public static boolean hadErrors = false; + public static boolean hadSimpleComponentErrors = false; + + @Override + public byte[] transform(String name, String transformedName, byte[] basicClass) { + if (basicClass == null) { + return null; + } + + try { + if (transformedName.equals("net.minecraft.entity.EntityLiving")) { + byte[] patched = TransformerEntityLiving.transform(loader, basicClass); + if (patched != null) { + return patched; + } + + hadErrors = true; + return basicClass; + } + + if (transformedName.equals("net.minecraft.client.renderer.entity.RenderLiving")) { + byte[] patched = TransformerRenderLiving.transform(loader, basicClass); + if (patched != null) { + return patched; + } + + hadErrors = true; + return basicClass; + } + + if (name.startsWith("scala.") || + name.startsWith("net.minecraft.") || + name.startsWith("net.minecraftforge.") || + name.startsWith("cpw.mods.fml.") || + // We're using apache's ArrayUtils here, so we need to avoid circular transforms of this class + name.startsWith("org.apache.") || + name.startsWith("li.cil.oc.common.asm.") || + name.startsWith("li.cil.oc.integration.")) { + return basicClass; + } + + byte[] transformedClass = basicClass; + + if (name.startsWith("li.cil.oc.")) { + transformedClass = TransformerStripMissingClasses.transform(loader, name, transformedClass); + transformedClass = TransformerInjectInterfaces.transform(loader, name, transformedClass); + } + + ClassNode classNode = ASMHelpers.newClassNode(transformedClass); + boolean hasSimpleComponent = classNode.interfaces.contains("li/cil/oc/api/network/SimpleComponent"); + boolean hasSkipAnnotation = false; + + if (classNode.visibleAnnotations != null) { + for (AnnotationNode annotation : classNode.visibleAnnotations) { + if (annotation != null && annotation.desc.equals("Lli/cil/oc/api/network/SimpleComponent$SkipInjection;")) { + hasSkipAnnotation = true; + break; + } + } + } + + if (hasSimpleComponent && !hasSkipAnnotation) { + try { + transformedClass = TransformerInjectEnvironmentImplementation.transform(loader, classNode); + log.info("Successfully injected component logic into class {}.", name); + } catch (Throwable e) { + log.warn("Failed injecting component logic into class {}.", name, e); + hadSimpleComponentErrors = true; + } + } + + return transformedClass; + } catch (Throwable t) { + log.warn("Something went wrong!", t); + hadErrors = true; + return basicClass; + } + } +} diff --git a/src/main/scala/li/cil/oc/common/asm/ClassTransformer.scala b/src/main/scala/li/cil/oc/common/asm/ClassTransformer.scala deleted file mode 100644 index 310dbca442..0000000000 --- a/src/main/scala/li/cil/oc/common/asm/ClassTransformer.scala +++ /dev/null @@ -1,416 +0,0 @@ -package li.cil.oc.common.asm - -import cpw.mods.fml.common.asm.transformers.deobf.FMLDeobfuscatingRemapper -import li.cil.oc.common.asm.template.SimpleComponentImpl -import li.cil.oc.integration.Mods -import net.minecraft.launchwrapper.IClassTransformer -import net.minecraft.launchwrapper.LaunchClassLoader -import org.apache.logging.log4j.LogManager -import org.objectweb.asm.ClassReader -import org.objectweb.asm.ClassWriter -import org.objectweb.asm.Opcodes -import org.objectweb.asm.tree._ - -import scala.annotation.tailrec -import scala.collection.convert.WrapAsJava._ -import scala.collection.convert.WrapAsScala._ - -object ObfNames { - final val Class_EntityHanging = Array("net/minecraft/entity/EntityHanging", "ss") - final val Class_EntityLiving = Array("net/minecraft/entity/EntityLiving", "sw") - final val Class_RenderLiving = Array("net/minecraft/client/renderer/entity/RenderLiving", "bok") - final val Class_TileEntity = Array("net/minecraft/tileentity/TileEntity", "aor") - final val Field_leashNBTTag = Array("leashNBTTag", "field_110170_bx", "bx") - final val Field_leashedToEntity = Array("leashedToEntity", "field_110168_bw", "bw") - final val Method_recreateLeash = Array("recreateLeash", "func_110165_bF", "bP") - final val Method_recreateLeashDesc = Array("()V") - final val Method_renderHanging = Array("func_110827_b", "b") - final val Method_renderHangingDesc = Array("(Lsw;DDDFF)V", "(Lnet/minecraft/entity/EntityLiving;DDDFF)V") - final val Method_validate = Array("validate", "func_145829_t") - final val Method_invalidate = Array("invalidate", "func_145843_s") - final val Method_onChunkUnload = Array("onChunkUnload", "func_76623_d") - final val Method_readFromNBT = Array("readFromNBT", "func_145839_a") - final val Method_writeToNBT = Array("writeToNBT", "func_145841_b") -} - -object ClassTransformer { - var hadErrors = false - var hadSimpleComponentErrors = false -} - -class ClassTransformer extends IClassTransformer { - private val loader = classOf[ClassTransformer].getClassLoader.asInstanceOf[LaunchClassLoader] - private val log = LogManager.getLogger("OpenComputers") - - override def transform(name: String, transformedName: String, basicClass: Array[Byte]): Array[Byte] = { - if (basicClass == null || name.startsWith("scala.")) return basicClass - var transformedClass = basicClass - try { - if (!name.startsWith("net.minecraft.") - && !name.startsWith("net.minecraftforge.") - && !name.startsWith("li.cil.oc.common.asm.") - && !name.startsWith("li.cil.oc.integration.")) { - if (name.startsWith("li.cil.oc.")) { - // Strip foreign interfaces from scala generated classes. This is - // primarily intended to clean up mix-ins / synthetic classes - // generated by Scala. - val classNode = newClassNode(transformedClass) - val missingInterfaces = classNode.interfaces.filter(!classExists(_)) - for (interfaceName <- missingInterfaces) { - log.trace(s"Stripping interface $interfaceName from class $name because it is missing.") - } - classNode.interfaces.removeAll(missingInterfaces) - - val missingClasses = classNode.innerClasses.filter(clazz => clazz.outerName != null && !classExists(clazz.outerName)) - for (innerClass <- missingClasses) { - log.trace(s"Stripping inner class ${innerClass.name} from class $name because its type ${innerClass.outerName} is missing.") - } - classNode.innerClasses.removeAll(missingClasses) - - val incompleteMethods = classNode.methods.filter(method => missingFromSignature(method.desc).nonEmpty) - for (method <- incompleteMethods) { - val missing = missingFromSignature(method.desc).mkString(", ") - log.trace(s"Stripping method ${method.name} from class $name because the following types in its signature are missing: $missing") - } - classNode.methods.removeAll(incompleteMethods) - - // Inject available interfaces where requested. - if (classNode.visibleAnnotations != null) { - def injectInterface(annotation: AnnotationNode): Unit = { - val values = annotation.values.grouped(2).map(buffer => buffer.head -> buffer.last).toMap - (values.get("value"), values.get("modid")) match { - case (Some(interfaceName: String), Some(modid: String)) => - Mods.All.find(_.id == modid) match { - case Some(mod) => - if (mod.isAvailable) { - val interfaceDesc = interfaceName.replaceAllLiterally(".", "/") - val node = classNodeFor(interfaceDesc) - if (node == null) { - log.warn(s"Interface $interfaceName not found, skipping injection.") - } - else { - val missing = node.methods.filterNot(im => classNode.methods.exists(cm => im.name == cm.name && im.desc == cm.desc)).map(method => s"Missing implementation of ${method.name + method.desc}") - if (missing.isEmpty) { - log.info(s"Injecting interface $interfaceName into $name.") - classNode.interfaces.add(interfaceDesc) - } - else { - log.warn(s"Missing implementations for interface $interfaceName, skipping injection.") - missing.foreach(log.warn) - ClassTransformer.hadErrors = true - } - } - } - else { - log.info(s"Skipping interface $interfaceName from missing mod $modid.") - mod.disablePower() - } - case _ => - log.warn(s"Skipping interface $interfaceName from unknown mod $modid.") - ClassTransformer.hadErrors = true - } - case _ => - } - } - classNode.visibleAnnotations.find(_.desc == "Lli/cil/oc/common/asm/Injectable$Interface;") match { - case Some(annotation) => - injectInterface(annotation) - case _ => - } - classNode.visibleAnnotations.find(_.desc == "Lli/cil/oc/common/asm/Injectable$InterfaceList;") match { - case Some(annotation) => - val values = annotation.values.grouped(2).map(buffer => buffer.head -> buffer.last).toMap - values.get("value") match { - case Some(interfaceList: java.lang.Iterable[AnnotationNode]@unchecked) => - interfaceList.foreach(injectInterface) - case _ => - } - case _ => - } - } - - transformedClass = writeClass(classNode) - } - { - val classNode = newClassNode(transformedClass) - if (classNode.interfaces.contains("li/cil/oc/api/network/SimpleComponent") && - (classNode.visibleAnnotations == null || !classNode.visibleAnnotations. - exists(annotation => annotation != null && annotation.desc == "Lli/cil/oc/api/network/SimpleComponent$SkipInjection;"))) { - try { - transformedClass = injectEnvironmentImplementation(classNode) - log.info(s"Successfully injected component logic into class $name.") - } - catch { - case e: Throwable => - log.warn(s"Failed injecting component logic into class $name.", e) - ClassTransformer.hadSimpleComponentErrors = true - } - } - } - } - - // Inject some code into the EntityLiving classes recreateLeash method to allow - // proper loading of leashes tied to entities using the leash upgrade. This is - // necessary because entities only save the entity they are leashed to if that - // entity is an EntityLivingBase - which drones, for example, are not, for good - // reason. We work around this by re-leashing them in the load method of the - // leash upgrade. The leashed entity would then still unleash itself and, more - // problematically drop a leash item. To avoid this, we extend the - // if (this.isLeashed && this.field_110170_bx != null) - // check to read - // if (this.isLeashed && this.field_110170_bx != null && this.leashedToEntity == null) - // which should not interfere with any existing logic, but avoid leashing - // restored manually in the load phase to not be broken again. - if (ObfNames.Class_EntityLiving.contains(name.replace('.', '/'))) { - val classNode = newClassNode(transformedClass) - insertInto(classNode, ObfNames.Method_recreateLeash, ObfNames.Method_recreateLeashDesc, instructions => instructions.toArray.sliding(3, 1).exists { - case Array(varNode: VarInsnNode, fieldNode: FieldInsnNode, jumpNode: JumpInsnNode) - if varNode.getOpcode == Opcodes.ALOAD && varNode.`var` == 0 && - fieldNode.getOpcode == Opcodes.GETFIELD && ObfNames.Field_leashNBTTag.contains(fieldNode.name) && - jumpNode.getOpcode == Opcodes.IFNULL => - classNode.fields.find(field => ObfNames.Field_leashedToEntity.contains(field.name)) match { - case Some(field) => - val toInject = new InsnList() - toInject.add(new VarInsnNode(Opcodes.ALOAD, 0)) - toInject.add(new FieldInsnNode(Opcodes.GETFIELD, classNode.name, field.name, field.desc)) - toInject.add(new JumpInsnNode(Opcodes.IFNONNULL, jumpNode.label)) - instructions.insert(jumpNode, toInject) - true - case _ => - false - } - case _ => - false - }) match { - case Some(data) => transformedClass = data - case _ => - } - } - - // Little change to the renderer used to render leashes to center it on drones. - // This injects the code - // if (entity instanceof Drone) { - // d5 = 0.0; - // d6 = 0.0; - // d7 = -0.75; - // } - // before the `instanceof EntityHanging` check in func_110827_b. - if (ObfNames.Class_RenderLiving.contains(name.replace('.', '/'))) { - val classNode = newClassNode(transformedClass) - insertInto(classNode, ObfNames.Method_renderHanging, ObfNames.Method_renderHangingDesc, instructions => instructions.toArray.sliding(3, 1).exists { - case Array(varNode: VarInsnNode, typeNode: TypeInsnNode, jumpNode: JumpInsnNode) - if varNode.getOpcode == Opcodes.ALOAD && varNode.`var` == 10 && - typeNode.getOpcode == Opcodes.INSTANCEOF && ObfNames.Class_EntityHanging.contains(typeNode.desc) && - jumpNode.getOpcode == Opcodes.IFEQ => - val toInject = new InsnList() - toInject.add(new VarInsnNode(Opcodes.ALOAD, 10)) - toInject.add(new TypeInsnNode(Opcodes.INSTANCEOF, "li/cil/oc/common/entity/Drone")) - val skip = new LabelNode() - toInject.add(new JumpInsnNode(Opcodes.IFEQ, skip)) - toInject.add(new LdcInsnNode(Double.box(0.0))) - toInject.add(new VarInsnNode(Opcodes.DSTORE, 16)) - toInject.add(new LdcInsnNode(Double.box(0.0))) - toInject.add(new VarInsnNode(Opcodes.DSTORE, 18)) - toInject.add(new LdcInsnNode(Double.box(-0.75))) - toInject.add(new VarInsnNode(Opcodes.DSTORE, 20)) - toInject.add(skip) - instructions.insertBefore(varNode, toInject) - true - case _ => - false - }) match { - case Some(data) => transformedClass = data - case _ => - } - } - - transformedClass - } - catch { - case t: Throwable => - log.warn("Something went wrong!", t) - ClassTransformer.hadErrors = true - basicClass - } - } - - private def insertInto(classNode: ClassNode, methodNames: Array[String], methodDescs: Array[String], inserter: (InsnList) => Boolean): Option[Array[Byte]] = { - classNode.methods.find(method => methodNames.contains(method.name) && methodDescs.contains(method.desc)) match { - case Some(methodNode) => - if (inserter(methodNode.instructions)) { - log.info(s"Successfully patched ${classNode.name}.${methodNames(0)}.") - Option(writeClass(classNode, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES)) - } - else { - log.warn(s"Failed patching ${classNode.name}.${methodNames(0)}, injection point not found.") - ClassTransformer.hadErrors = true - None - } - case _ => - log.warn(s"Failed patching ${classNode.name}.${methodNames(0)}, method not found.") - ClassTransformer.hadErrors = true - None - } - } - - private def classExists(name: String) = { - loader.getClassBytes(name) != null || - loader.getClassBytes(FMLDeobfuscatingRemapper.INSTANCE.unmap(name)) != null || - (try loader.findClass(name.replace('/', '.')) != null catch { - case _: ClassNotFoundException => false - }) - } - - private def missingFromSignature(desc: String) = { - """L([^;]+);""".r.findAllMatchIn(desc).map(_.group(1)).filter(!classExists(_)) - } - - def injectEnvironmentImplementation(classNode: ClassNode): Array[Byte] = { - log.trace(s"Injecting methods from Environment interface into ${classNode.name}.") - if (!isTileEntity(classNode)) { - throw new InjectionFailedException("Found SimpleComponent on something that isn't a tile entity, ignoring.") - } - - val template = classNodeFor("li/cil/oc/common/asm/template/SimpleEnvironment") - if (template == null) { - throw new InjectionFailedException("Could not find SimpleComponent template!") - } - - def inject(methodName: String, signature: String, required: Boolean = false) { - def filter(method: MethodNode) = method.name == methodName && method.desc == signature - if (classNode.methods.exists(filter)) { - if (required) { - throw new InjectionFailedException(s"Could not inject method '$methodName$signature' because it was already present!") - } - } - else template.methods.find(filter) match { - case Some(method) => classNode.methods.add(method) - case _ => throw new AssertionError() - } - } - inject("node", "()Lli/cil/oc/api/network/Node;", required = true) - inject("onConnect", "(Lli/cil/oc/api/network/Node;)V") - inject("onDisconnect", "(Lli/cil/oc/api/network/Node;)V") - inject("onMessage", "(Lli/cil/oc/api/network/Message;)V") - - log.trace("Injecting / wrapping overrides for required tile entity methods.") - def replace(methodName: String, methodNameSrg: String, desc: String) { - val mapper = FMLDeobfuscatingRemapper.INSTANCE - def filter(method: MethodNode) = { - val descDeObf = mapper.mapMethodDesc(method.desc) - val methodNameDeObf = mapper.mapMethodName(ObfNames.Class_TileEntity(1), method.name, method.desc) - val areSamePlain = method.name + descDeObf == methodName + desc - val areSameDeObf = methodNameDeObf + descDeObf == methodNameSrg + desc - areSamePlain || areSameDeObf - } - if (classNode.methods.exists(method => method.name == methodName + SimpleComponentImpl.PostFix && mapper.mapMethodDesc(method.desc) == desc)) { - throw new InjectionFailedException(s"Delegator method name '${methodName + SimpleComponentImpl.PostFix}' is already in use.") - } - classNode.methods.find(filter) match { - case Some(method) => - log.trace(s"Found original implementation of '$methodName', wrapping.") - method.name = methodName + SimpleComponentImpl.PostFix - case _ => - log.trace(s"No original implementation of '$methodName', will inject override.") - @tailrec def ensureNonFinalIn(name: String) { - if (name != null) { - val node = classNodeFor(name) - if (node != null) { - node.methods.find(filter) match { - case Some(method) => - if ((method.access & Opcodes.ACC_FINAL) != 0) { - throw new InjectionFailedException(s"Method '$methodName' is final in superclass ${node.name.replace('/', '.')}.") - } - case _ => - } - ensureNonFinalIn(node.superName) - } - } - } - ensureNonFinalIn(classNode.superName) - template.methods.find(_.name == methodName + SimpleComponentImpl.PostFix) match { - case Some(method) => classNode.methods.add(method) - case _ => throw new AssertionError(s"Couldn't find '${methodName + SimpleComponentImpl.PostFix}' in template implementation.") - } - } - template.methods.find(filter) match { - case Some(method) => classNode.methods.add(method) - case _ => throw new AssertionError(s"Couldn't find '$methodName' in template implementation.") - } - } - replace(ObfNames.Method_validate(0), ObfNames.Method_validate(1), "()V") - replace(ObfNames.Method_invalidate(0), ObfNames.Method_invalidate(1), "()V") - replace(ObfNames.Method_onChunkUnload(0), ObfNames.Method_onChunkUnload(1), "()V") - replace(ObfNames.Method_readFromNBT(0), ObfNames.Method_readFromNBT(1), "(Lnet/minecraft/nbt/NBTTagCompound;)V") - replace(ObfNames.Method_writeToNBT(0), ObfNames.Method_writeToNBT(1), "(Lnet/minecraft/nbt/NBTTagCompound;)V") - - log.trace("Injecting interface.") - classNode.interfaces.add("li/cil/oc/common/asm/template/SimpleComponentImpl") - - writeClass(classNode, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES) - } - - @tailrec final def isTileEntity(classNode: ClassNode): Boolean = { - if (classNode == null) false - else { - log.trace(s"Checking if class ${classNode.name} is a TileEntity...") - ObfNames.Class_TileEntity.contains(classNode.name) || - (classNode.superName != null && isTileEntity(classNodeFor(classNode.superName))) - } - } - - @tailrec final def isAssignable(parent: ClassNode, child: ClassNode): Boolean = parent != null && child != null && !isFinal(parent) && { - parent.name == "java/lang/Object" || - parent.name == child.name || - parent.name == child.superName || - child.interfaces.contains(parent.name) || - (child.superName != null && isAssignable(parent, classNodeFor(child.superName))) - } - - def isFinal(node: ClassNode): Boolean = (node.access & Opcodes.ACC_FINAL) != 0 - - def isInterface(node: ClassNode): Boolean = node != null && (node.access & Opcodes.ACC_INTERFACE) != 0 - - def classNodeFor(name: String) = { - val namePlain = name.replace('/', '.') - val bytes = loader.getClassBytes(namePlain) - if (bytes != null) newClassNode(bytes) - else { - val nameObfed = FMLDeobfuscatingRemapper.INSTANCE.unmap(name).replace('/', '.') - val bytes = loader.getClassBytes(nameObfed) - if (bytes == null) null - else newClassNode(bytes) - } - } - - def newClassNode(data: Array[Byte]) = { - val classNode = new ClassNode() - new ClassReader(data).accept(classNode, 0) - classNode - } - - def writeClass(classNode: ClassNode, flags: Int = ClassWriter.COMPUTE_MAXS) = { - val writer = new ClassWriter(flags) { - // Implementation without class loads, avoids https://github.com/MinecraftForge/FML/issues/655 - override def getCommonSuperClass(type1: String, type2: String): String = { - val node1 = classNodeFor(type1) - val node2 = classNodeFor(type2) - if (isAssignable(node1, node2)) node1.name - else if (isAssignable(node2, node1)) node2.name - else if (isInterface(node1) || isInterface(node2)) "java/lang/Object" - else { - var parent = Option(node1).map(_.superName).map(classNodeFor).orNull - while (parent != null && parent.superName != null && !isAssignable(parent, node2)) { - parent = classNodeFor(parent.superName) - } - if (parent == null) "java/lang/Object" else parent.name - } - } - } - classNode.accept(writer) - writer.toByteArray - } - - class InjectionFailedException(message: String) extends Exception(message) - -} diff --git a/src/main/scala/li/cil/oc/common/asm/ObfNames.java b/src/main/scala/li/cil/oc/common/asm/ObfNames.java new file mode 100644 index 0000000000..b4fd522a64 --- /dev/null +++ b/src/main/scala/li/cil/oc/common/asm/ObfNames.java @@ -0,0 +1,23 @@ +package li.cil.oc.common.asm; + +public final class ObfNames { + private ObfNames() {} + + public static final String[] CLASS_ENTITY_HANGING = { "net/minecraft/entity/EntityHanging", "ss" }; + public static final String[] CLASS_TILE_ENTITY = { "net/minecraft/tileentity/TileEntity", "aor" }; + + public static final String[] FIELD_LEASH_NBT_TAG = { "leashNBTTag", "field_110170_bx", "bx" }; + public static final String[] FIELD_LEASHED_TO_ENTITY = { "leashedToEntity", "field_110168_bw", "bw" }; + + public static final String[] METHOD_RECREATE_LEASH = { "recreateLeash", "func_110165_bF", "bP" }; + public static final String[] METHOD_RECREATE_LEASH_DESC = { "()V" }; + + public static final String[] METHOD_RENDER_HANGING = { "func_110827_b", "b" }; + public static final String[] METHOD_RENDER_HANGING_DESC = { "(Lsw;DDDFF)V", "(Lnet/minecraft/entity/EntityLiving;DDDFF)V" }; + + public static final String[] METHOD_VALIDATE = { "validate", "func_145829_t" }; + public static final String[] METHOD_INVALIDATE = { "invalidate", "func_145843_s" }; + public static final String[] METHOD_ON_CHUNK_UNLOAD = { "onChunkUnload", "func_76623_d" }; + public static final String[] METHOD_READ_FROM_NBT = { "readFromNBT", "func_145839_a" }; + public static final String[] METHOD_WRITE_TO_NBT = { "writeToNBT", "func_145841_b" }; +} diff --git a/src/main/scala/li/cil/oc/common/asm/TransformerEntityLiving.java b/src/main/scala/li/cil/oc/common/asm/TransformerEntityLiving.java new file mode 100644 index 0000000000..698af54f24 --- /dev/null +++ b/src/main/scala/li/cil/oc/common/asm/TransformerEntityLiving.java @@ -0,0 +1,77 @@ +package li.cil.oc.common.asm; + +import net.minecraft.launchwrapper.LaunchClassLoader; +import org.apache.commons.lang3.ArrayUtils; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.VarInsnNode; + +import javax.annotation.Nullable; + +public final class TransformerEntityLiving { + + // Inject some code into the EntityLiving classes recreateLeash method to allow + // proper loading of leashes tied to entities using the leash upgrade. This is + // necessary because entities only save the entity they are leashed to if that + // entity is an EntityLivingBase - which drones, for example, are not, for good + // reason. We work around this by re-leashing them in the load method of the + // leash upgrade. The leashed entity would then still unleash itself and, more + // problematically drop a leash item. To avoid this, we extend the + // if (this.isLeashed && this.field_110170_bx != null) + // check to read + // if (this.isLeashed && this.field_110170_bx != null && this.leashedToEntity == null) + // which should not interfere with any existing logic, but avoid leashing + // restored manually in the load phase to not be broken again. + @Nullable + public static byte[] transform(LaunchClassLoader loader, byte[] classBytes) { + ClassNode classNode = ASMHelpers.newClassNode(classBytes); + + return ASMHelpers.insertInto( + loader, + classNode, + ObfNames.METHOD_RECREATE_LEASH, + ObfNames.METHOD_RECREATE_LEASH_DESC, + instructions -> { + + FieldNode leashedToEntityField = null; + for (FieldNode field : classNode.fields) { + if (ArrayUtils.contains(ObfNames.FIELD_LEASHED_TO_ENTITY, field.name)) { + leashedToEntityField = field; + break; + } + } + if (leashedToEntityField == null) return false; + + for (AbstractInsnNode node = instructions.getFirst(); node != null; node = node.getNext()) { + if (node instanceof VarInsnNode && + node.getNext() instanceof FieldInsnNode && + node.getNext().getNext() instanceof JumpInsnNode) { + + VarInsnNode varNode = (VarInsnNode) node; + FieldInsnNode fieldNode = (FieldInsnNode) node.getNext(); + JumpInsnNode jumpNode = (JumpInsnNode) node.getNext().getNext(); + + if (varNode.getOpcode() == Opcodes.ALOAD && varNode.var == 0 && + fieldNode.getOpcode() == Opcodes.GETFIELD && + ArrayUtils.contains(ObfNames.FIELD_LEASH_NBT_TAG, fieldNode.name) && + jumpNode.getOpcode() == Opcodes.IFNULL) { + + InsnList toInject = new InsnList(); + toInject.add(new VarInsnNode(Opcodes.ALOAD, 0)); + toInject.add(new FieldInsnNode(Opcodes.GETFIELD, classNode.name, leashedToEntityField.name, leashedToEntityField.desc)); + toInject.add(new JumpInsnNode(Opcodes.IFNONNULL, jumpNode.label)); + instructions.insert(jumpNode, toInject); + return true; + } + } + } + + return false; + }); + } +} diff --git a/src/main/scala/li/cil/oc/common/asm/TransformerInjectEnvironmentImplementation.java b/src/main/scala/li/cil/oc/common/asm/TransformerInjectEnvironmentImplementation.java new file mode 100644 index 0000000000..ec7460a92e --- /dev/null +++ b/src/main/scala/li/cil/oc/common/asm/TransformerInjectEnvironmentImplementation.java @@ -0,0 +1,154 @@ +package li.cil.oc.common.asm; + +import cpw.mods.fml.common.asm.transformers.deobf.FMLDeobfuscatingRemapper; +import li.cil.oc.common.asm.template.SimpleComponentImpl; +import net.minecraft.launchwrapper.LaunchClassLoader; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; + +import javax.annotation.Nonnull; +import java.util.function.Predicate; + +public final class TransformerInjectEnvironmentImplementation { + + private static final Logger log = LogManager.getLogger("OpenComputers"); + + @Nonnull + public static byte[] transform(LaunchClassLoader loader, ClassNode classNode) throws Exception { + log.trace("Injecting methods from Environment interface into {}.", classNode.name); + + if (!isTileEntity(loader, classNode)) { + throw new Exception("Found SimpleComponent on something that isn't a tile entity, ignoring."); + } + + ClassNode template = ASMHelpers.classNodeFor(loader, "li/cil/oc/common/asm/template/SimpleEnvironment"); + if (template == null) { + throw new Exception("Could not find SimpleComponent template!"); + } + + injectMethodIfMissing(classNode, template, "node", "()Lli/cil/oc/api/network/Node;", true); + injectMethodIfMissing(classNode, template, "onConnect", "(Lli/cil/oc/api/network/Node;)V", false); + injectMethodIfMissing(classNode, template, "onDisconnect", "(Lli/cil/oc/api/network/Node;)V", false); + injectMethodIfMissing(classNode, template, "onMessage", "(Lli/cil/oc/api/network/Message;)V", false); + + log.trace("Injecting / wrapping overrides for required tile entity methods."); + + replaceTileMethod(loader, classNode, template, ObfNames.METHOD_VALIDATE[0], ObfNames.METHOD_VALIDATE[1], "()V"); + replaceTileMethod(loader, classNode, template, ObfNames.METHOD_INVALIDATE[0], ObfNames.METHOD_INVALIDATE[1], "()V"); + replaceTileMethod(loader, classNode, template, ObfNames.METHOD_ON_CHUNK_UNLOAD[0], ObfNames.METHOD_ON_CHUNK_UNLOAD[1], "()V"); + replaceTileMethod(loader, classNode, template, ObfNames.METHOD_READ_FROM_NBT[0], ObfNames.METHOD_READ_FROM_NBT[1], "(Lnet/minecraft/nbt/NBTTagCompound;)V"); + replaceTileMethod(loader, classNode, template, ObfNames.METHOD_WRITE_TO_NBT[0], ObfNames.METHOD_WRITE_TO_NBT[1], "(Lnet/minecraft/nbt/NBTTagCompound;)V"); + + log.trace("Injecting interface."); + classNode.interfaces.add("li/cil/oc/common/asm/template/SimpleComponentImpl"); + + return ASMHelpers.writeClass(loader, classNode, ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + } + + private static boolean isTileEntity(LaunchClassLoader loader, ClassNode classNode) { + if (classNode == null) return false; + log.trace("Checking if class {} is a TileEntity...", classNode.name); + if (ArrayUtils.contains(ObfNames.CLASS_TILE_ENTITY, classNode.name)) return true; + return classNode.superName != null && isTileEntity(loader, ASMHelpers.classNodeFor(loader, classNode.superName)); + } + + private static void injectMethodIfMissing(ClassNode classNode, ClassNode template, String methodName, String desc, + boolean required) throws Exception { + + if (ASMHelpers.findMethod(classNode, methodName, desc) != null) { + if (required) { + throw new Exception("Could not inject method '" + methodName + desc + "' because it was already present!"); + } + return; + } + + MethodNode methodNode = ASMHelpers.findMethod(template, methodName, desc); + if (methodNode == null) { + throw new AssertionError("Template missing method " + methodName + desc); + } + + classNode.methods.add(methodNode); + } + + private static void replaceTileMethod(LaunchClassLoader loader, ClassNode classNode, ClassNode template, + String methodNamePlain, String methodNameSrg, String desc) throws Exception { + + FMLDeobfuscatingRemapper mapper = FMLDeobfuscatingRemapper.INSTANCE; + + Predicate filter = method -> { + String descDeObf = mapper.mapMethodDesc(method.desc); + String methodNameDeObf = mapper.mapMethodName(ObfNames.CLASS_TILE_ENTITY[1], method.name, method.desc); + + boolean samePlain = (method.name + descDeObf).equals(methodNamePlain + desc); + boolean sameDeObf = (methodNameDeObf + descDeObf).equals(methodNameSrg + desc); + + return samePlain || sameDeObf; + }; + + for (MethodNode method : classNode.methods) { + if ((methodNamePlain + SimpleComponentImpl.PostFix).equals(method.name) + && mapper.mapMethodDesc(method.desc).equals(desc)) { + throw new Exception("Delegator method name '" + (methodNamePlain + SimpleComponentImpl.PostFix) + "' is already in use."); + } + } + + MethodNode original = null; + for (MethodNode method : classNode.methods) { + if (filter.test(method)) { + original = method; + break; + } + } + + if (original != null) { + log.trace("Found original implementation of '{}', wrapping.", methodNamePlain); + original.name = methodNamePlain + SimpleComponentImpl.PostFix; + } else { + log.trace("No original implementation of '{}', will inject override.", methodNamePlain); + + ensureNonFinalInHierarchy(loader, classNode.superName, filter, methodNamePlain); + + MethodNode delegator = ASMHelpers.findMethod(template, methodNamePlain + SimpleComponentImpl.PostFix, desc); + if (delegator == null) { + throw new AssertionError("Couldn't find '" + (methodNamePlain + SimpleComponentImpl.PostFix) + "' in template implementation."); + } + classNode.methods.add(delegator); + } + + MethodNode override = null; + for (MethodNode m : template.methods) { + if (filter.test(m)) { + override = m; + break; + } + } + if (override == null) { + throw new AssertionError("Couldn't find '" + methodNamePlain + "' in template implementation."); + } + classNode.methods.add(override); + } + + private static void ensureNonFinalInHierarchy(LaunchClassLoader loader, String internalSuperName, + Predicate filter, String methodNamePlain) throws Exception { + + if (internalSuperName == null) return; + + ClassNode node = ASMHelpers.classNodeFor(loader, internalSuperName); + if (node == null) return; + + for (MethodNode method : node.methods) { + if (filter.test(method) && (method.access & Opcodes.ACC_FINAL) != 0) { + throw new Exception( + "Method '" + methodNamePlain + "' is final in superclass " + node.name.replace('/', '.') + "." + ); + } + } + + ensureNonFinalInHierarchy(loader, node.superName, filter, methodNamePlain); + } +} diff --git a/src/main/scala/li/cil/oc/common/asm/TransformerInjectInterfaces.java b/src/main/scala/li/cil/oc/common/asm/TransformerInjectInterfaces.java new file mode 100644 index 0000000000..ee10b5ec7f --- /dev/null +++ b/src/main/scala/li/cil/oc/common/asm/TransformerInjectInterfaces.java @@ -0,0 +1,111 @@ +package li.cil.oc.common.asm; + +import li.cil.oc.integration.Mods; +import net.minecraft.launchwrapper.LaunchClassLoader; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.tree.AnnotationNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.MethodNode; + +public final class TransformerInjectInterfaces { + + private static final Logger log = LogManager.getLogger("OpenComputers"); + + // Inject available interfaces where requested. + public static byte[] transform(LaunchClassLoader loader, String name, byte[] classBytes) { + ClassNode classNode = ASMHelpers.newClassNode(classBytes); + + if (classNode.visibleAnnotations != null) { + for (AnnotationNode annotation : classNode.visibleAnnotations) { + if (annotation.desc.equals("Lli/cil/oc/common/asm/Injectable$Interface;")) { + injectInterface(loader, name, classNode, annotation); + break; + } + + if (annotation.desc.equals("Lli/cil/oc/common/asm/Injectable$InterfaceList;")) { + Object value = getAnnotationField(annotation, "value"); + if (value instanceof Iterable) { + for (Object item : (Iterable) value) { + if (item instanceof AnnotationNode) { + injectInterface(loader, name, classNode, (AnnotationNode) item); + } + } + } + break; + } + } + } + + return ASMHelpers.writeClass(loader, classNode, ClassWriter.COMPUTE_MAXS); + } + + private static void injectInterface(LaunchClassLoader loader, String ownerName, ClassNode classNode, + AnnotationNode annotation) { + + Object interfaceObj = getAnnotationField(annotation, "value"); + Object modIdObj = getAnnotationField(annotation, "modid"); + + if (!(interfaceObj instanceof String) || !(modIdObj instanceof String)) { + return; + } + + String interfaceName = (String) interfaceObj; + String modId = (String) modIdObj; + + for (scala.collection.Iterator it = Mods.All().iterator(); it.hasNext(); ) { + Mods.ModBase mod = it.next(); + if (!mod.id().equals(modId)) { + continue; + } + + if (!mod.isAvailable()) { + log.info("Skipping interface {} from missing mod {}.", interfaceName, modId); + mod.disablePower(); + return; + } + + String interfaceNameInternal = interfaceName.replace('.', '/'); + ClassNode node = ASMHelpers.classNodeFor(loader, interfaceNameInternal); + if (node == null) { + log.warn("Interface {} not found, skipping injection.", interfaceName); + return; + } + + boolean isMissing = false; + for (MethodNode nodeMethod : node.methods) { + boolean found = false; + for (MethodNode classNodeMethod : classNode.methods) { + if (nodeMethod.name.equals(classNodeMethod.name) && nodeMethod.desc.equals(classNodeMethod.desc)) { + found = true; + break; + } + } + if (found) continue; + + if (!isMissing) { + log.warn("Missing implementations for interface {}, skipping injection.", interfaceName); + ClassTransformer.hadErrors = true; + isMissing = true; + } + log.warn("Missing implementation of " + nodeMethod.name + nodeMethod.desc); + } + + if (!isMissing) { + log.info("Injecting interface {} into {}.", interfaceName, ownerName); + classNode.interfaces.add(interfaceNameInternal); + } + } + } + + private static Object getAnnotationField(AnnotationNode annotation, String field) { + for (int i = 0; i < annotation.values.size(); i += 2) { + String key = (String) annotation.values.get(i); + if (key.equals(field)) { + return annotation.values.get(i + 1); + } + } + return null; + } +} diff --git a/src/main/scala/li/cil/oc/common/asm/TransformerRenderLiving.java b/src/main/scala/li/cil/oc/common/asm/TransformerRenderLiving.java new file mode 100644 index 0000000000..b03fd0d307 --- /dev/null +++ b/src/main/scala/li/cil/oc/common/asm/TransformerRenderLiving.java @@ -0,0 +1,77 @@ +package li.cil.oc.common.asm; + +import net.minecraft.launchwrapper.LaunchClassLoader; +import org.apache.commons.lang3.ArrayUtils; +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.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LdcInsnNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.VarInsnNode; + +import javax.annotation.Nullable; + +public final class TransformerRenderLiving { + + // Little change to the renderer used to render leashes to center it on drones. + // This injects the code + // if (entity instanceof Drone) { + // d5 = 0.0; + // d6 = 0.0; + // d7 = -0.75; + // } + // before the `instanceof EntityHanging` check in func_110827_b. + @Nullable + public static byte[] transform(LaunchClassLoader loader, byte[] classBytes) { + ClassNode classNode = ASMHelpers.newClassNode(classBytes); + + return ASMHelpers.insertInto( + loader, + classNode, + ObfNames.METHOD_RENDER_HANGING, + ObfNames.METHOD_RENDER_HANGING_DESC, + instructions -> { + + for (AbstractInsnNode node = instructions.getFirst(); node != null; node = node.getNext()) { + if (node instanceof VarInsnNode && + node.getNext() instanceof TypeInsnNode && + node.getNext().getNext() instanceof JumpInsnNode) { + + VarInsnNode varNode = (VarInsnNode) node; + TypeInsnNode typeNode = (TypeInsnNode) node.getNext(); + JumpInsnNode jumpNode = (JumpInsnNode) node.getNext().getNext(); + + if (varNode.getOpcode() == Opcodes.ALOAD && varNode.var == 10 + && typeNode.getOpcode() == Opcodes.INSTANCEOF + && ArrayUtils.contains(ObfNames.CLASS_ENTITY_HANGING, typeNode.desc) + && jumpNode.getOpcode() == Opcodes.IFEQ) { + + InsnList toInject = new InsnList(); + + toInject.add(new VarInsnNode(Opcodes.ALOAD, 10)); + toInject.add(new TypeInsnNode(Opcodes.INSTANCEOF, "li/cil/oc/common/entity/Drone")); + + LabelNode skip = new LabelNode(); + toInject.add(new JumpInsnNode(Opcodes.IFEQ, skip)); + + toInject.add(new LdcInsnNode(0.0)); + toInject.add(new VarInsnNode(Opcodes.DSTORE, 16)); + toInject.add(new LdcInsnNode(0.0)); + toInject.add(new VarInsnNode(Opcodes.DSTORE, 18)); + toInject.add(new LdcInsnNode(-0.75)); + toInject.add(new VarInsnNode(Opcodes.DSTORE, 20)); + + toInject.add(skip); + + instructions.insertBefore(varNode, toInject); + return true; + } + } + } + return false; + }); + } +} diff --git a/src/main/scala/li/cil/oc/common/asm/TransformerStripMissingClasses.java b/src/main/scala/li/cil/oc/common/asm/TransformerStripMissingClasses.java new file mode 100644 index 0000000000..b4feef0814 --- /dev/null +++ b/src/main/scala/li/cil/oc/common/asm/TransformerStripMissingClasses.java @@ -0,0 +1,83 @@ +package li.cil.oc.common.asm; + +import net.minecraft.launchwrapper.LaunchClassLoader; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InnerClassNode; +import org.objectweb.asm.tree.MethodNode; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; + +public final class TransformerStripMissingClasses { + + private static final Logger log = LogManager.getLogger("OpenComputers"); + + // Strip foreign interfaces from scala generated classes. This is + // primarily intended to clean up mix-ins / synthetic classes + // generated by Scala. + @Nonnull + public static byte[] transform(LaunchClassLoader loader, String name, byte[] classBytes) { + ClassNode classNode = ASMHelpers.newClassNode(classBytes); + + List missingInterfaces = new ArrayList<>(); + for (String interfaceName : classNode.interfaces) { + if (!ASMHelpers.classExists(loader, interfaceName)) { + log.trace("Stripping interface {} from class {} because it is missing.", interfaceName, name); + missingInterfaces.add(interfaceName); + } + } + classNode.interfaces.removeAll(missingInterfaces); + + List missingClasses = new ArrayList<>(); + for (InnerClassNode innerClass : classNode.innerClasses) { + if (innerClass.outerName != null && !ASMHelpers.classExists(loader, innerClass.outerName)) { + log.trace("Stripping inner class {} from class {} because its type {} is missing.", innerClass.name, name, innerClass.outerName); + missingClasses.add(innerClass); + } + } + classNode.innerClasses.removeAll(missingClasses); + + List incompleteMethods = new ArrayList<>(); + for (MethodNode method : classNode.methods) { + List missing = missingFromSignature(loader, method.desc); + if (!missing.isEmpty()) { + log.trace("Stripping method {} from class {} because missing types in signature: {}", + method.name, name, String.join(", ", missing)); + incompleteMethods.add(method); + } + } + classNode.methods.removeAll(incompleteMethods); + + return ASMHelpers.writeClass(loader, classNode, ClassWriter.COMPUTE_MAXS); + } + + private static List missingFromSignature(LaunchClassLoader loader, String methodDesc) { + List missing = new ArrayList<>(); + Type methodType = Type.getMethodType(methodDesc); + + for (Type arg : methodType.getArgumentTypes()) { + collectMissingType(loader, arg, missing); + } + collectMissingType(loader, methodType.getReturnType(), missing); + + return missing; + } + + private static void collectMissingType(LaunchClassLoader loader, Type type, List missing) { + while (type.getSort() == Type.ARRAY) { + type = type.getElementType(); + } + + if (type.getSort() == Type.OBJECT) { + String internal = type.getInternalName(); + if (!ASMHelpers.classExists(loader, internal)) { + missing.add(internal); + } + } + } +} diff --git a/src/main/scala/li/cil/oc/common/launch/TransformerLoader.scala b/src/main/scala/li/cil/oc/common/launch/TransformerLoader.scala index 8afa876c99..77bfcdc7bf 100644 --- a/src/main/scala/li/cil/oc/common/launch/TransformerLoader.scala +++ b/src/main/scala/li/cil/oc/common/launch/TransformerLoader.scala @@ -5,7 +5,6 @@ import java.util import cpw.mods.fml.relauncher.IFMLLoadingPlugin import cpw.mods.fml.relauncher.IFMLLoadingPlugin.MCVersion import cpw.mods.fml.relauncher.IFMLLoadingPlugin.TransformerExclusions -import li.cil.oc.common.asm.ClassTransformer @TransformerExclusions(Array("li.cil.oc.common.asm")) @MCVersion("1.7.10") @@ -14,7 +13,7 @@ class TransformerLoader extends IFMLLoadingPlugin { override def getModContainerClass = "li.cil.oc.common.launch.CoreModContainer" - override def getASMTransformerClass = Array(classOf[ClassTransformer].getName) + override def getASMTransformerClass = Array("li.cil.oc.common.asm.ClassTransformer") override def getAccessTransformerClass = null