Skip to content
5 changes: 3 additions & 2 deletions dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ dependencies {
shadowImplementation name: 'OC-JNLua-Natives', version: '20220928.1', ext: 'jar'

api("com.github.GTNewHorizons:AE2FluidCraft-Rework:1.5.70-gtnh:dev")
compileOnly("com.github.GTNewHorizons:Angelica:2.1.16:api") {transitive = false}
api("com.github.GTNewHorizons:Applied-Energistics-2-Unofficial:rv3-beta-885-GTNH:dev")
implementation("com.github.GTNewHorizons:GTNHLib:0.9.47:dev") {transitive = false}

compileOnly("com.github.GTNewHorizons:Angelica:2.1.16:api") {transitive = false}
compileOnly("com.github.GTNewHorizons:Avaritiaddons:1.9.4-GTNH:dev") {transitive = false}
compileOnly("com.github.GTNewHorizons:BloodMagic:1.8.14:dev") {transitive = false}
compileOnly("com.github.GTNewHorizons:BuildCraft:7.1.57:dev") {transitive = false}
Expand All @@ -20,7 +22,6 @@ dependencies {
compileOnly("com.github.GTNewHorizons:ForgeMultipart:1.7.3:dev") {transitive = false}
compileOnly("com.github.GTNewHorizons:Galacticraft:3.4.20-GTNH:dev") {transitive = false}
compileOnly("com.github.GTNewHorizons:GT5-Unofficial:5.09.52.405:dev") {transitive = false}
compileOnly("com.github.GTNewHorizons:GTNHLib:0.9.47:dev") {transitive = false}
compileOnly("com.github.GTNewHorizons:ModularUI:1.3.2:dev") {transitive = false}
compileOnly("com.github.GTNewHorizons:ModularUI2:2.3.52-1.7.10:dev") {transitive = false}
compileOnly("com.github.GTNewHorizons:NotEnoughItems:2.8.86-GTNH:dev") {transitive = false}
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/li/cil/oc/OpenComputers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import org.apache.logging.log4j.Logger

@Mod(modid = OpenComputers.ID, name = OpenComputers.Name,
version = OpenComputers.Version,
modLanguage = "scala", useMetadata = true /*@MCVERSIONDEP@*/)
modLanguage = "scala", useMetadata = true /*@MCVERSIONDEP@*/, dependencies = "required-after:gtnhlib@[0.6.9,);")
object OpenComputers {
final val ID = "OpenComputers"

Expand Down
49 changes: 34 additions & 15 deletions src/main/scala/li/cil/oc/common/asm/ASMHelpers.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package li.cil.oc.common.asm;

import com.gtnewhorizon.gtnhlib.asm.ASMUtil;
import cpw.mods.fml.common.asm.transformers.deobf.FMLDeobfuscatingRemapper;
import net.minecraft.launchwrapper.LaunchClassLoader;
import org.apache.commons.lang3.ArrayUtils;
Expand All @@ -14,12 +15,14 @@

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.function.Predicate;

public final class ASMHelpers {

private static final Logger log = LogManager.getLogger("OpenComputers");
private static final boolean dumpASMClass = Boolean.getBoolean("opencomputers.dumpClass");

@Nullable
public static byte[] insertInto(LaunchClassLoader loader, ClassNode classNode, String[] methodNames, String[] methodDescs, Predicate<InsnList> inserter) {
Expand Down Expand Up @@ -73,45 +76,61 @@ protected String getCommonSuperClass(final String type1, final String type2) {
}

@Nullable
public static ClassNode classNodeFor(LaunchClassLoader loader, String internalName) {
public static byte[] classBytesFor(LaunchClassLoader loader, String internalName) {
try {
// internalName is slash-form, e.g. "net/minecraft/tileentity/TileEntity"
String namePlain = internalName.replace('/', '.');

byte[] bytes = loader.getClassBytes(namePlain);
final byte[] bytes = loader.getClassBytes(namePlain);
if (bytes != null) {
return newClassNode(bytes);
return bytes;
}

String nameObfed = FMLDeobfuscatingRemapper.INSTANCE.unmap(internalName).replace('/', '.');
bytes = loader.getClassBytes(nameObfed);
if (bytes == null) {
String nameObf = FMLDeobfuscatingRemapper.INSTANCE.unmap(internalName).replace('/', '.');
if (nameObf.equals(namePlain)) {
return null;
}
return newClassNode(bytes);

return loader.getClassBytes(nameObf);
} catch (IOException ignored) {
return null;
}
}

@Nullable
public static ClassNode classNodeFor(LaunchClassLoader loader, String internalName) {
final byte[] bytes = classBytesFor(loader, internalName);
if (bytes == null) {
return null;
}

return newClassNode(bytes);
}

@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 void dumpClass(String className, byte[] originalBytes, byte[] transformedBytes, Class<?> transformer) {
if (dumpASMClass) {
final String fileName = transformer.getSimpleName().toUpperCase() + File.separatorChar + className.replace('.', File.separatorChar);
ASMUtil.saveAsRawClassFile(originalBytes, fileName + "_PRE", fileName + "_PRE");
ASMUtil.saveAsRawClassFile(transformedBytes, fileName + "_POST", fileName + "_POST");
}
}

public static MethodNode copyMethodNode(MethodNode methodNode) {
MethodNode newMethodNode = new MethodNode(
methodNode.access, methodNode.name, methodNode.desc, methodNode.signature,
methodNode.exceptions == null ? null : methodNode.exceptions.toArray(new String[0])
);
methodNode.accept(newMethodNode); // clones instructions/labels/etc
return newMethodNode;
}

public static boolean isAssignable(LaunchClassLoader loader, ClassNode parent, ClassNode child) {
if (parent == null || child == null) return false;
if (isFinal(parent)) return false;
Expand Down
51 changes: 25 additions & 26 deletions src/main/scala/li/cil/oc/common/asm/ClassTransformer.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@
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;
import com.gtnewhorizon.gtnhlib.asm.ClassConstantPoolParser;

public class ClassTransformer implements IClassTransformer {

private static final Logger log = LogManager.getLogger("OpenComputers");
private final LaunchClassLoader loader =
(LaunchClassLoader) ClassTransformer.class.getClassLoader();

private final ClassConstantPoolParser simpleComponentParser =
new ClassConstantPoolParser("li/cil/oc/api/network/SimpleComponent");

public static boolean hadErrors = false;
public static boolean hadSimpleComponentErrors = false;

Expand All @@ -26,6 +28,7 @@ public byte[] transform(String name, String transformedName, byte[] basicClass)
if (transformedName.equals("net.minecraft.entity.EntityLiving")) {
byte[] patched = TransformerEntityLiving.transform(loader, basicClass);
if (patched != null) {
ASMHelpers.dumpClass(transformedName, basicClass, patched, TransformerEntityLiving.class);
return patched;
}

Expand All @@ -36,6 +39,7 @@ public byte[] transform(String name, String transformedName, byte[] basicClass)
if (transformedName.equals("net.minecraft.client.renderer.entity.RenderLiving")) {
byte[] patched = TransformerRenderLiving.transform(loader, basicClass);
if (patched != null) {
ASMHelpers.dumpClass(transformedName, basicClass, patched, TransformerRenderLiving.class);
return patched;
}

Expand All @@ -48,47 +52,42 @@ public byte[] transform(String name, String transformedName, byte[] basicClass)
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.")) {
name.startsWith("org.apache.")) {
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 (name.startsWith("li.cil.oc.common")) {
byte[] patched = TransformerInjectInterfaces.transform(loader, name, basicClass);
if (patched != null) {
ASMHelpers.dumpClass(transformedName, basicClass, patched, TransformerInjectInterfaces.class);
return patched;
}
}

return basicClass;
}

if (hasSimpleComponent && !hasSkipAnnotation) {
boolean hasSimpleComponent = simpleComponentParser.find(basicClass);
if (hasSimpleComponent) {
try {
transformedClass = TransformerInjectEnvironmentImplementation.transform(loader, classNode);
log.info("Successfully injected component logic into class {}.", name);
byte[] patched = TransformerInjectEnvironmentImplementation.transform(loader, basicClass);
if (patched != null) {
ASMHelpers.dumpClass(transformedName, basicClass, patched, TransformerInjectEnvironmentImplementation.class);
log.info("Successfully injected component logic into class {}.", name);
return patched;
}
} catch (Throwable e) {
log.warn("Failed injecting component logic into class {}.", name, e);
hadSimpleComponentErrors = true;
return basicClass;
}
}

return transformedClass;
} catch (Throwable t) {
log.warn("Something went wrong!", t);
hadErrors = true;
return basicClass;
}

return basicClass;
}
}
2 changes: 2 additions & 0 deletions src/main/scala/li/cil/oc/common/asm/Injectable.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
* Instead of stripping interfaces if they are not present, it will inject them
* when they <em>are</em> present. This helps with some strange cases where
* stripping does not work as it should.
* <br>
* NOTE: For performance reasons injecting only applies for classes in li/cil/oc/common folder
*/
public final class Injectable {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,47 @@
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.AnnotationNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
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 {
@Nullable
private static ClassNode template = null;

@Nullable
public static byte[] transform(LaunchClassLoader loader, byte[] classBytes) throws Exception {
ClassNode classNode = ASMHelpers.newClassNode(classBytes);
log.trace("Injecting methods from Environment interface into {}.", classNode.name);

if (!isTileEntity(loader, classNode)) {
if (classNode.visibleAnnotations != null) {
for (AnnotationNode annotation : classNode.visibleAnnotations) {
if (annotation != null && annotation.desc.equals("Lli/cil/oc/api/network/SimpleComponent$SkipInjection;")) {
log.trace("Detected @SimpleComponent.SkipInjection annotation, skipping the class");
return null;
}
}
}

if (!isTileEntity(loader, classNode.name, classNode.superName)) {
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!");
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);
Expand All @@ -50,11 +68,15 @@ public static byte[] transform(LaunchClassLoader loader, ClassNode classNode) th
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 boolean isTileEntity(LaunchClassLoader loader, String className, String superName) {
if (ArrayUtils.contains(ObfNames.CLASS_TILE_ENTITY, className)) return true;
if (superName == null || superName.equals("java/lang/Object")) return false;

final byte[] bytes = ASMHelpers.classBytesFor(loader, superName);
if (bytes == null) return false;

ClassReader classReader = new ClassReader(bytes);
return isTileEntity(loader, superName, classReader.getSuperName());
}

private static void injectMethodIfMissing(ClassNode classNode, ClassNode template, String methodName, String desc,
Expand All @@ -72,22 +94,22 @@ private static void injectMethodIfMissing(ClassNode classNode, ClassNode templat
throw new AssertionError("Template missing method " + methodName + desc);
}

classNode.methods.add(methodNode);
classNode.methods.add(ASMHelpers.copyMethodNode(methodNode));
}

private static void replaceTileMethod(LaunchClassLoader loader, ClassNode classNode, ClassNode template,
String methodNamePlain, String methodNameSrg, String desc) throws Exception {

FMLDeobfuscatingRemapper mapper = FMLDeobfuscatingRemapper.INSTANCE;
final FMLDeobfuscatingRemapper mapper = FMLDeobfuscatingRemapper.INSTANCE;

Predicate<MethodNode> 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);
if (!methodNamePlain.equals(method.name)) {
final String methodNameDeObf = mapper.mapMethodName(ObfNames.CLASS_TILE_ENTITY[1], method.name, method.desc);
if (!methodNameSrg.equals(methodNameDeObf)) return false;
}

return samePlain || sameDeObf;
final String descDeObf = mapper.mapMethodDesc(method.desc);
return desc.equals(descDeObf);
};

for (MethodNode method : classNode.methods) {
Expand Down Expand Up @@ -117,7 +139,7 @@ private static void replaceTileMethod(LaunchClassLoader loader, ClassNode classN
if (delegator == null) {
throw new AssertionError("Couldn't find '" + (methodNamePlain + SimpleComponentImpl.PostFix) + "' in template implementation.");
}
classNode.methods.add(delegator);
classNode.methods.add(ASMHelpers.copyMethodNode(delegator));
}

MethodNode override = null;
Expand All @@ -130,7 +152,7 @@ private static void replaceTileMethod(LaunchClassLoader loader, ClassNode classN
if (override == null) {
throw new AssertionError("Couldn't find '" + methodNamePlain + "' in template implementation.");
}
classNode.methods.add(override);
classNode.methods.add(ASMHelpers.copyMethodNode(override));
}

private static void ensureNonFinalInHierarchy(LaunchClassLoader loader, String internalSuperName,
Expand Down
Loading
Loading