Fixing OptiFine, until something better comes.
at 1.0 303 lines 13 kB view raw
1package dev.redstudio.optinotfine.asm; 2 3import dev.redstudio.optinotfine.config.OptiNotFineConfig; 4import net.minecraft.client.resources.AbstractResourcePack; 5import net.minecraft.launchwrapper.IClassTransformer; 6import net.minecraftforge.fml.relauncher.FMLLaunchHandler; 7import org.objectweb.asm.*; 8import org.objectweb.asm.tree.*; 9 10import java.lang.reflect.Field; 11import java.util.function.BiPredicate; 12import java.util.function.Function; 13 14/// Transformer for OptiNotFine. 15/// 16/// @author Luna Mira Lage (Desoroxxx) 17/// @since 1.0 18public final class OptiNotFineTransformer implements IClassTransformer { 19 20 private static final String RESOURCE_PACK_FILE_FIELD_NAME = FMLLaunchHandler.isDeobfuscatedEnvironment() ? "resourcePackFile" : "field_110597_b"; 21 22 public static final String[][] BUFFER_ALLOCATED_PATHS = { 23 {"jdk.internal.access.SharedSecrets", "getJavaNioAccess", "getDirectBufferPool", "getMemoryUsed"}, 24 {"jdk.internal.misc.SharedSecrets", "getJavaNioAccess", "getDirectBufferPool", "getMemoryUsed"}, 25 {"sun.misc.SharedSecrets", "getJavaNioAccess", "getDirectBufferPool", "getMemoryUsed"} 26 }; 27 28 @Override 29 public byte[] transform(final String name, final String transformedName, final byte[] basicClass) { 30 if (basicClass == null) 31 return null; 32 33 switch (transformedName) { 34 case "net.minecraftforge.fml.client.FMLClientHandler": 35 return transformFMLClientHandler(basicClass); 36 case "net.minecraft.client.resources.AbstractResourcePack": 37 return transformAbstractResourcePack(basicClass); 38 case "net.optifine.util.NativeMemory": 39 return transformNativeMemory(basicClass); 40 } 41 42 if (!OptiNotFineConfig.stopLogSpam) 43 return basicClass; 44 45 switch (transformedName) { 46 case "net.optifine.shaders.SMCLog": 47 return transformSMCLog(basicClass); 48 case "net.optifine.config.ConnectedParser": 49 return transformConnectedParser(basicClass); 50 case "net.optifine.shaders.ItemAliases": 51 return transformAliases(basicClass, "loadItemAliases", "item"); 52 case "net.optifine.shaders.BlockAliases": 53 return transformAliases(basicClass, "loadBlockAliases", "block"); 54 case "net.optifine.shaders.EntityAliases": 55 return transformAliases(basicClass, "loadEntityAliases", "entity"); 56 case "net.optifine.shaders.config.MacroExpressionResolver": 57 return stripConfigWarn(basicClass, "getExpression", "(Ljava/lang/String;)Lnet/optifine/expr/IExpression;"); 58 case "net.optifine.shaders.config.ShaderPackParser": 59 return stripConfigWarn(basicClass, "collectShaderOptions", "(Lnet/optifine/shaders/IShaderPack;Ljava/lang/String;Ljava/util/Map;)V"); 60 } 61 62 return basicClass; 63 } 64 65 /// Add our branding to the main menu screen 66 private static byte[] transformFMLClientHandler(final byte[] basicClass) { 67 return transform(basicClass, classWriter -> targetMethod(classWriter, "getAdditionalBrandingInformation", methodVisitor -> 68 new MethodVisitor(Opcodes.ASM5, methodVisitor) { 69 @Override 70 public void visitLdcInsn(final Object value) { 71 super.visitLdcInsn(value.equals("Optifine %s") ? "OptiNotFine 1.0-Dev-1 on %s" : value); 72 } 73 })); 74 } 75 76 /// Add a missing getter method to [AbstractResourcePack]. 77 /// 78 /// That getter method was added by Cleanroom but is accidentally removed due to OptiFine's binary patching. 79 /// 80 /// **Warning:** We currently do not check whether this method already exists as we have a guarantee that it does not. 81 /// The reason for that is since our transformer runs only when OptiFine is here, the method is definitely removed. 82 private static byte[] transformAbstractResourcePack(final byte[] basicClass) { 83 return transform(basicClass, classWriter -> new ClassVisitor(Opcodes.ASM5, classWriter) { 84 @Override 85 public void visitEnd() { 86 final MethodVisitor methodVisitor = visitMethod(Opcodes.ACC_PUBLIC, "getResourcePackFile", "()Ljava/io/File;", null, null); 87 88 methodVisitor.visitCode(); 89 methodVisitor.visitVarInsn(Opcodes.ALOAD, 0); 90 methodVisitor.visitFieldInsn(Opcodes.GETFIELD, "net/minecraft/client/resources/AbstractResourcePack", RESOURCE_PACK_FILE_FIELD_NAME, "Ljava/io/File;"); 91 methodVisitor.visitInsn(Opcodes.ARETURN); 92 methodVisitor.visitMaxs(1, 1); 93 methodVisitor.visitEnd(); 94 95 super.visitEnd(); 96 } 97 }); 98 } 99 100 /// Add `jdk.internal.access.SharedSecrets` to the paths OptiFine will search to see the native memory usage. 101 /// 102 /// We can safely access this because [Imagine Breaker](https://github.com/Rongmario/ImagineBreaker) will remove the module system. 103 private static byte[] transformNativeMemory(final byte[] basicClass) { 104 final ClassNode classNode = new ClassNode(); 105 new ClassReader(basicClass).accept(classNode, 0); 106 107 final String fieldName = findFieldName(BUFFER_ALLOCATED_PATHS); 108 109 for (final MethodNode methodNode : classNode.methods) { 110 if (!methodNode.name.equals("<clinit>")) 111 continue; 112 113 for (AbstractInsnNode instruction = methodNode.instructions.getFirst(); instruction != null; instruction = instruction.getNext()) { 114 if (!isTargetCall(instruction)) 115 continue; 116 117 replaceArrayArgument(methodNode.instructions, instruction, fieldName); 118 break; 119 } 120 121 break; 122 } 123 124 final ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); 125 classNode.accept(classWriter); 126 return classWriter.toByteArray(); 127 } 128 129 private static String findFieldName(final Object fieldValue) { 130 try { 131 for (final Field field : OptiNotFineTransformer.class.getFields()) 132 if (field.get(null) == fieldValue) 133 return field.getName(); 134 } catch (final IllegalAccessException illegalAccessException) { 135 throw new RuntimeException(illegalAccessException); 136 } 137 138 throw new IllegalStateException("Field not found for the given value, something is very wrong"); 139 } 140 141 private static boolean isTargetCall(final AbstractInsnNode instructionNode) { 142 if (!(instructionNode instanceof MethodInsnNode)) 143 return false; 144 145 final MethodInsnNode methodInstruction = (MethodInsnNode) instructionNode; 146 147 return methodInstruction.getOpcode() == Opcodes.INVOKESTATIC && methodInstruction.owner.equals("net/optifine/util/NativeMemory") && methodInstruction.name.equals("makeLongSupplier"); 148 } 149 150 private static void replaceArrayArgument(final InsnList instructions, final AbstractInsnNode methodCall, final String fieldName) { 151 for (AbstractInsnNode current = methodCall.getPrevious(); current != null; current = current.getPrevious()) { 152 if (!(current instanceof TypeInsnNode)) 153 continue; 154 155 final TypeInsnNode typeInstruction = (TypeInsnNode) current; 156 157 if (typeInstruction.getOpcode() != Opcodes.ANEWARRAY || !typeInstruction.desc.equals("[Ljava/lang/String;")) 158 continue; 159 160 removeInstructionsBetween(instructions, current.getPrevious(), methodCall); 161 instructions.insertBefore(methodCall, new FieldInsnNode(Opcodes.GETSTATIC, Type.getInternalName(OptiNotFineTransformer.class), fieldName, "[[Ljava/lang/String;")); 162 return; 163 } 164 165 throw new IllegalStateException("Could not find array argument for method call"); 166 } 167 168 private static void removeInstructionsBetween(final InsnList instructions, final AbstractInsnNode start, final AbstractInsnNode end) { 169 if (start == null) 170 return; 171 172 AbstractInsnNode current = start; 173 while (current != end) { 174 final AbstractInsnNode next = current.getNext(); 175 instructions.remove(current); 176 current = next; 177 } 178 } 179 180 /// Changes the logger call of `SMCLog.info` from `info` to `debug` as most of it is. 181 private static byte[] transformSMCLog(final byte[] basicClass) { 182 final String owner = "org/apache/logging/log4j/Logger"; 183 return transform(basicClass, classWriter -> 184 targetMethod(classWriter, "info", methodVisitor -> 185 methodCallReplacer(methodVisitor, Opcodes.INVOKEINTERFACE, owner, "info", (methodVisitor1, descriptor, isInterface) -> 186 methodVisitor1.visitMethodInsn(Opcodes.INVOKEINTERFACE, owner, "debug", descriptor, isInterface)) 187 )); 188 } 189 190 /// Strip a few specific `Config#warn()`. 191 private static byte[] transformConnectedParser(final byte[] basicClass) { 192 return transform(basicClass, classWriter -> new ClassVisitor(Opcodes.ASM5, classWriter) { 193 @Override 194 public MethodVisitor visitMethod(final int access, final String name, final String descriptor, final String signature, final String[] exceptions) { 195 final MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); 196 197 final String triggerString; 198 if (name.equals("parseItems")) { 199 triggerString = "Item not found: "; 200 } else if (name.equals("parseBlockPart")) { 201 triggerString = "Block not found for name: "; 202 } else { 203 return methodVisitor; 204 } 205 206 return triggerThenStripWarn(methodVisitor, triggerString, Opcodes.INVOKEVIRTUAL, "net/optifine/config/ConnectedParser", 2); 207 } 208 }); 209 } 210 211 /// Strip a specific `Config#warn()`. 212 private static byte[] transformAliases(final byte[] basicClass, final String methodName, final String warnPrefix) { 213 final String trigger = "[Shaders] Invalid " + warnPrefix + " ID mapping: "; 214 215 return transform(basicClass, classWriter -> 216 targetMethod(classWriter, methodName, methodVisitor -> 217 triggerThenStripWarn(methodVisitor, trigger, Opcodes.INVOKESTATIC, "Config", 1) 218 )); 219 } 220 221 private static MethodVisitor triggerThenStripWarn(final MethodVisitor parent, final String triggerString, final int targetOpcode, final String warnOwner, final int popCount) { 222 return new MethodVisitor(Opcodes.ASM5, parent) { 223 private boolean stripNextWarn = false; 224 225 @Override 226 public void visitLdcInsn(final Object value) { 227 if (triggerString.equals(value)) 228 stripNextWarn = true; 229 230 super.visitLdcInsn(value); 231 } 232 233 @Override 234 public void visitMethodInsn(final int opcode, final String owner, final String name, final String descriptor, final boolean isInterface) { 235 if (stripNextWarn && opcode == targetOpcode && owner.equals(warnOwner) && name.equals("warn")) { 236 stripNextWarn = false; 237 238 for (int i = 0; i < popCount; i++) 239 super.visitInsn(Opcodes.POP); 240 241 return; 242 } 243 244 super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); 245 } 246 }; 247 } 248 249 /// Strip any `Config#warn()` calls from a method. 250 private static byte[] stripConfigWarn(final byte[] basicClass, final String targetMethod, final String targetDescriptor) { 251 return transform(basicClass, classWriter -> 252 targetMethod(classWriter, (name, descriptor) -> name.equals(targetMethod) && descriptor.equals(targetDescriptor), methodVisitor -> 253 methodCallReplacer(methodVisitor, Opcodes.INVOKESTATIC, "Config", "warn", (methodVisitor1, descriptor, isInterface) -> 254 methodVisitor1.visitInsn(Opcodes.POP) 255 ))); 256 } 257 258 private static MethodVisitor methodCallReplacer(final MethodVisitor parent, final int targetOpcode, final String targetOwner, final String targetName, final MethodCallReplacement replacement) { 259 return new MethodVisitor(Opcodes.ASM5, parent) { 260 @Override 261 public void visitMethodInsn(final int opcode, final String owner, final String name, final String descriptor, final boolean isInterface) { 262 if (opcode == targetOpcode && owner.equals(targetOwner) && name.equals(targetName)) { 263 replacement.apply(this, descriptor, isInterface); 264 } else { 265 super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); 266 } 267 } 268 }; 269 } 270 271 @FunctionalInterface 272 interface MethodCallReplacement { 273 void apply(final MethodVisitor methodVisitor, final String descriptor, final boolean isInterface); 274 } 275 276 private static byte[] transform(final byte[] basicClass, final Function<ClassWriter, ClassVisitor> visitorFactory) { 277 final ClassReader classReader = new ClassReader(basicClass); 278 final ClassWriter classWriter = new ClassWriter(classReader, 0); 279 280 classReader.accept(visitorFactory.apply(classWriter), 0); 281 282 return classWriter.toByteArray(); 283 } 284 285 private static ClassVisitor targetMethod(final ClassWriter classWriter, final String methodName, final MethodVisitorFactory factory) { 286 return targetMethod(classWriter, (name, descriptor) -> name.equals(methodName), factory); 287 } 288 289 private static ClassVisitor targetMethod(final ClassWriter classWriter, final BiPredicate<String, String> matcher, final MethodVisitorFactory factory) { 290 return new ClassVisitor(Opcodes.ASM5, classWriter) { 291 @Override 292 public MethodVisitor visitMethod(final int access, final String name, final String descriptor, final String signature, final String[] exceptions) { 293 final MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions); 294 return matcher.test(name, descriptor) ? factory.create(methodVisitor) : methodVisitor; 295 } 296 }; 297 } 298 299 @FunctionalInterface 300 interface MethodVisitorFactory { 301 MethodVisitor create(final MethodVisitor parent); 302 } 303}