Fixing OptiFine, until something better comes.
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}