From 9a00c42ede2a41d5196060a624568d8af19a68b8 Mon Sep 17 00:00:00 2001 From: Chocohead Date: Sat, 12 Jun 2021 03:18:54 +0100 Subject: [PATCH] Try recover more lambdas Optifine changes Specifically aimed at the keyboard handling ones Fixes #238 Fixes #244 --- gradle.properties | 2 +- .../mixin/GameRendererNewMixin.java | 47 +++++ .../mixin/SpriteAtlasTextureNewMixin.java | 37 ++++ .../optifabric/mod/OptifabricSetup.java | 5 +- .../optifabric/mod/OptifineSetup.java | 37 ++-- .../mod/RetainingMappingsProvider.java | 83 ++++++++ .../optifabric/patcher/ClassCache.java | 20 +- ...mbdaRebuiler.java => LambdaRebuilder.java} | 151 ++++++++++--- .../optifabric/patcher/MethodComparison.java | 13 +- .../optifabric/patcher/StaticFuzzer.java | 199 ++++++++++++++++++ .../modmuss50/optifabric/util/ASMUtils.java | 8 + .../optifabric/util/ThrowingFunction.java | 21 ++ ...ric.compat.architectury-AB.new-mixins.json | 9 + 13 files changed, 581 insertions(+), 51 deletions(-) create mode 100644 src/main/java/me/modmuss50/optifabric/compat/architectury/mixin/GameRendererNewMixin.java create mode 100644 src/main/java/me/modmuss50/optifabric/compat/architectury/mixin/SpriteAtlasTextureNewMixin.java create mode 100644 src/main/java/me/modmuss50/optifabric/mod/RetainingMappingsProvider.java rename src/main/java/me/modmuss50/optifabric/patcher/{LambdaRebuiler.java => LambdaRebuilder.java} (59%) create mode 100644 src/main/java/me/modmuss50/optifabric/patcher/StaticFuzzer.java create mode 100644 src/main/java/me/modmuss50/optifabric/util/ThrowingFunction.java create mode 100644 src/main/resources/optifabric.compat.architectury-AB.new-mixins.json diff --git a/gradle.properties b/gradle.properties index ace65938..9fc694ae 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,6 +8,6 @@ loader_version=0.10.8 fabric_version=0.31.0+1.16 fabric_asm_version=v2.2 -mod_version = 1.11.1 +mod_version = 1.11.2 maven_group = me.modmuss50 archives_base_name = optifabric diff --git a/src/main/java/me/modmuss50/optifabric/compat/architectury/mixin/GameRendererNewMixin.java b/src/main/java/me/modmuss50/optifabric/compat/architectury/mixin/GameRendererNewMixin.java new file mode 100644 index 00000000..ff17b4d1 --- /dev/null +++ b/src/main/java/me/modmuss50/optifabric/compat/architectury/mixin/GameRendererNewMixin.java @@ -0,0 +1,47 @@ +package me.modmuss50.optifabric.compat.architectury.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.At.Shift; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import net.minecraft.client.render.GameRenderer; +import net.minecraft.client.util.Window; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.math.Matrix4f; + +import me.modmuss50.optifabric.compat.InterceptingMixin; +import me.modmuss50.optifabric.compat.PlacatingSurrogate; +import me.modmuss50.optifabric.compat.Shim; + +@Mixin(GameRenderer.class) +@InterceptingMixin("dev/architectury/mixin/fabric/client/MixinGameRenderer") +abstract class GameRendererNewMixin { + @Shim + private native void renderScreenPre(float tickDelta, long startTime, boolean tick, CallbackInfo call, int mouseX, int mouseY, MatrixStack matrices); + + @PlacatingSurrogate + private void renderScreenPre(float tickDelta, long startTime, boolean tick, CallbackInfo call, int mouseX, int mouseY, Window window) { + } + + @Inject(method = "render(FJZ)V", locals = LocalCapture.CAPTURE_FAILHARD, cancellable = true, + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;render(Lnet/minecraft/client/util/math/MatrixStack;IIF)V", ordinal = 0)) + private void renderScreenPre(float tickDelta, long startTime, boolean tick, CallbackInfo call, int mouseX, int mouseY, Window window, Matrix4f projection, MatrixStack matrices) { + renderScreenPre(tickDelta, startTime, tick, call, mouseX, mouseY, matrices); + } + + @Shim + private native void renderScreenPost(float tickDelta, long startTime, boolean tick, CallbackInfo call, int mouseX, int mouseY, MatrixStack matrices); + + @PlacatingSurrogate + private void renderScreenPost(float tickDelta, long startTime, boolean tick, CallbackInfo call, int mouseX, int mouseY, Window window) { + } + + @Inject(method = "render(FJZ)V", locals = LocalCapture.CAPTURE_FAILHARD, + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screen/Screen;render(Lnet/minecraft/client/util/math/MatrixStack;IIF)V", shift = Shift.AFTER, ordinal = 0)) + private void renderScreenPost(float tickDelta, long startTime, boolean tick, CallbackInfo call, int mouseX, int mouseY, Window window, Matrix4f projection, MatrixStack matrices) { + renderScreenPost(tickDelta, startTime, tick, call, mouseX, mouseY, matrices); + } +} \ No newline at end of file diff --git a/src/main/java/me/modmuss50/optifabric/compat/architectury/mixin/SpriteAtlasTextureNewMixin.java b/src/main/java/me/modmuss50/optifabric/compat/architectury/mixin/SpriteAtlasTextureNewMixin.java new file mode 100644 index 00000000..422e8a5c --- /dev/null +++ b/src/main/java/me/modmuss50/optifabric/compat/architectury/mixin/SpriteAtlasTextureNewMixin.java @@ -0,0 +1,37 @@ +package me.modmuss50.optifabric.compat.architectury.mixin; + +import java.util.Set; +import java.util.stream.Stream; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import net.minecraft.client.texture.SpriteAtlasTexture; +import net.minecraft.client.texture.SpriteAtlasTexture.Data; +import net.minecraft.resource.ResourceManager; +import net.minecraft.util.Identifier; +import net.minecraft.util.profiler.Profiler; + +import me.modmuss50.optifabric.compat.InterceptingMixin; +import me.modmuss50.optifabric.compat.PlacatingSurrogate; +import me.modmuss50.optifabric.compat.Shim; + +@Mixin(SpriteAtlasTexture.class) +@InterceptingMixin("dev/architectury/mixin/fabric/client/MixinTextureAtlas") +abstract class SpriteAtlasTextureNewMixin { + @PlacatingSurrogate + private void preStitch(ResourceManager resourceManager, Stream idStream, Profiler profiler, int mipmapLevel, CallbackInfoReturnable call, int mipmapLevels) { + } + + @Inject(method = "stitch", locals = LocalCapture.CAPTURE_FAILHARD, + at = @At(value = "INVOKE_STRING", target = "Lnet/minecraft/util/profiler/Profiler;swap(Ljava/lang/String;)V", args = "ldc=extracting_frames")) + private void preStitch(ResourceManager resourceManager, Stream idStream, Profiler profiler, int mipmapLevel, CallbackInfoReturnable call, int mipmapLevels, Set set) { + preStitch(resourceManager, idStream, profiler, mipmapLevels, call, set); + } + + @Shim + private native void preStitch(ResourceManager resourceManager, Stream idStream, Profiler profiler, int mipmapLevel, CallbackInfoReturnable call, Set set); +} \ No newline at end of file diff --git a/src/main/java/me/modmuss50/optifabric/mod/OptifabricSetup.java b/src/main/java/me/modmuss50/optifabric/mod/OptifabricSetup.java index b7496a33..467ae0d2 100644 --- a/src/main/java/me/modmuss50/optifabric/mod/OptifabricSetup.java +++ b/src/main/java/me/modmuss50/optifabric/mod/OptifabricSetup.java @@ -246,7 +246,10 @@ public boolean getAsBoolean() { Mixins.addConfiguration("optifabric.compat.images.mixins.json"); } - if (isPresent("architectury", ">=1.0.20")) { + if (isPresent("architectury", ">=2.0")) { + assert isPresent("minecraft", ">=1.17-beta.1"); + Mixins.addConfiguration("optifabric.compat.architectury-AB.new-mixins.json"); + } else if (isPresent("architectury", ">=1.0.20")) { Mixins.addConfiguration("optifabric.compat.architectury-B.mixins.json"); } else if (isPresent("architectury", ">=1.0.4")) { Mixins.addConfiguration("optifabric.compat.architectury-A.mixins.json"); diff --git a/src/main/java/me/modmuss50/optifabric/mod/OptifineSetup.java b/src/main/java/me/modmuss50/optifabric/mod/OptifineSetup.java index b6f4ca20..c47bfc00 100644 --- a/src/main/java/me/modmuss50/optifabric/mod/OptifineSetup.java +++ b/src/main/java/me/modmuss50/optifabric/mod/OptifineSetup.java @@ -41,7 +41,9 @@ import me.modmuss50.optifabric.mod.OptifineVersion.JarType; import me.modmuss50.optifabric.patcher.ClassCache; -import me.modmuss50.optifabric.patcher.LambdaRebuiler; +import me.modmuss50.optifabric.patcher.LambdaRebuilder; +import me.modmuss50.optifabric.patcher.StaticFuzzer; +import me.modmuss50.optifabric.util.ThrowingFunction; import me.modmuss50.optifabric.util.ZipUtils; public class OptifineSetup { @@ -72,6 +74,11 @@ public static Pair getRuntime() throws IOException { //Validate that the classCache found is for the same input jar if (Arrays.equals(classCache.getHash(), modHash)) { System.out.println("Found existing patched optifine jar, using that"); + + if (classCache.isConverted()) { + classCache.save(optifinePatches); + } + return Pair.of(remappedJar, classCache); } else { System.out.println("Class cache is from a different optifine jar, deleting and re-generating"); @@ -126,20 +133,16 @@ public static Pair getRuntime() throws IOException { }, jarOfTheFree); System.out.println("Finding lambdas to fix"); - LambdaRebuiler rebuilder = new LambdaRebuiler(jarOfTheFree, minecraftJar.toFile()); + LambdaRebuilder rebuilder = new LambdaRebuilder(jarOfTheFree, minecraftJar.toFile()); rebuilder.buildLambdaMap(); - - System.out.println("Remapping optifine with fixed lambda names"); - File lambdaFixJar = new File(versionDir, "Optifine-lambdafix.jar"); - Path[] libraries = getLibs(minecraftJar); - remapOptifine(jarOfTheFree, libraries, lambdaFixJar, rebuilder); + RetainingMappingsProvider lambdas = new RetainingMappingsProvider(); + Map> fuzzes = rebuilder.load("official", lambdas); String namespace = FabricLoader.getInstance().getMappingResolver().getCurrentRuntimeNamespace(); System.out.println("Remapping optifine from official to " + namespace); - remapOptifine(lambdaFixJar, libraries, remappedJar, createMappings("official", namespace)); + remapOptifine(jarOfTheFree, getLibs(minecraftJar), remappedJar, createMappings("official", namespace, lambdas)); //We are done, lets get rid of the stuff we no longer need - lambdaFixJar.delete(); jarOfTheFree.delete(); if (OptifineVersion.jarType == JarType.OPTIFINE_INSTALLER) { optifineModJar.delete(); @@ -155,7 +158,9 @@ public static Pair getRuntime() throws IOException { ZipUtils.extract(remappedJar, optifineClasses); } - return Pair.of(remappedJar, generateClassCache(remappedJar, optifinePatches, modHash, extract)); + try (StaticFuzzer transformer = !fuzzes.isEmpty() ? new StaticFuzzer(fuzzes) : null) { + return Pair.of(remappedJar, generateClassCache(remappedJar, fuzzes.isEmpty() ? IOUtils::toByteArray : transformer, optifinePatches, modHash, extract)); + } } private static void runInstaller(File installer, File output, File minecraftJar) throws IOException { @@ -177,9 +182,9 @@ private static void remapOptifine(File input, Path[] libraries, File output, IMa private static void remapOptifine(Path input, Path[] libraries, Path output, IMappingProvider mappings) throws IOException { Files.deleteIfExists(output); - TinyRemapper remapper = TinyRemapper.newRemapper().withMappings(mappings).renameInvalidLocals(FabricLoader.getInstance().isDevelopmentEnvironment()).rebuildSourceFilenames(true).build(); + TinyRemapper remapper = TinyRemapper.newRemapper().withMappings(mappings).skipLocalVariableMapping(true).renameInvalidLocals(FabricLoader.getInstance().isDevelopmentEnvironment()).rebuildSourceFilenames(true).build(); - try (OutputConsumerPath outputConsumer = new Builder(output).build()) { + try (OutputConsumerPath outputConsumer = new Builder(output).assumeArchive(true).build()) { outputConsumer.addNonClassFiles(input); remapper.readInputs(input); remapper.readClassPath(libraries); @@ -193,7 +198,7 @@ private static void remapOptifine(Path input, Path[] libraries, Path output, IMa } //Optifine currently has two fields that match the same name as Yarn mappings, we'll rename Optifine's to something else - private static IMappingProvider createMappings(String from, String to) { + private static IMappingProvider createMappings(String from, String to, IMappingProvider extra) { TinyTree normalMappings = FabricLauncherBase.getLauncher().getMappingConfiguration().getMappings(); Map nameToClass = normalMappings.getClasses().stream().collect(Collectors.toMap(clazz -> clazz.getName("intermediary"), Function.identity())); @@ -229,6 +234,8 @@ private static IMappingProvider createMappings(String from, String to) { TinyRemapperMappingsHelper.create(normalMappings, from, to).load(out); extraFields.forEach(out::acceptField); + + extra.load(out); }; } @@ -301,7 +308,7 @@ private static Path getLaunchMinecraftJar() { return contextJars.get(0); } - private static ClassCache generateClassCache(File from, File to, byte[] hash, boolean extractClasses) throws IOException { + private static ClassCache generateClassCache(File from, ThrowingFunction transformer, File to, byte[] hash, boolean extractClasses) throws IOException { File classesDir = new File(to.getParent(), "classes"); if (extractClasses) { if (classesDir.exists()) { @@ -317,7 +324,7 @@ private static ClassCache generateClassCache(File from, File to, byte[] hash, bo if ((name.startsWith("net/minecraft/") || name.startsWith("com/mojang/")) && name.endsWith(".class")) { try (InputStream in = jarFile.getInputStream(entry)) { - byte[] bytes = IOUtils.toByteArray(in); + byte[] bytes = transformer.apply(in); classCache.addClass(name.substring(0, name.length() - 6), bytes); if (extractClasses) { diff --git a/src/main/java/me/modmuss50/optifabric/mod/RetainingMappingsProvider.java b/src/main/java/me/modmuss50/optifabric/mod/RetainingMappingsProvider.java new file mode 100644 index 00000000..3a74f5dc --- /dev/null +++ b/src/main/java/me/modmuss50/optifabric/mod/RetainingMappingsProvider.java @@ -0,0 +1,83 @@ +package me.modmuss50.optifabric.mod; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.lang3.tuple.Triple; + +import net.fabricmc.tinyremapper.IMappingProvider; +import net.fabricmc.tinyremapper.IMappingProvider.MappingAcceptor; + +public class RetainingMappingsProvider implements MappingAcceptor, IMappingProvider { + private static class LocalMapping { + final Member method; + final int lvtIndex; + final int start; + final int asmIndex; + final String name; + + public LocalMapping(Member method, int lvtIndex, int start, int asmIndex, String name) { + this.method = method; + this.lvtIndex = lvtIndex; + this.start = start; + this.asmIndex = asmIndex; + this.name = name; + } + } + private final List> classes = new ArrayList<>(); + private final List> methods = new ArrayList<>(); + private final List> parameters = new ArrayList<>(); + private final List locals = new ArrayList<>(); + private final List> fields = new ArrayList<>(); + + @Override + public void acceptClass(String srcName, String dstName) { + classes.add(Pair.of(srcName, dstName)); + } + + @Override + public void acceptMethod(Member method, String name) { + methods.add(Pair.of(method, name)); + } + + @Override + public void acceptMethodArg(Member method, int lvtIndex, String name) { + parameters.add(Triple.of(method, lvtIndex, name)); + } + + @Override + public void acceptMethodVar(Member method, int lvtIndex, int start, int asmIndex, String name) { + locals.add(new LocalMapping(method, lvtIndex, start, asmIndex, name)); + } + + @Override + public void acceptField(Member field, String name) { + fields.add(Pair.of(field, name)); + } + + @Override + public void load(MappingAcceptor out) { + take(classes, out::acceptClass); + take(methods, out::acceptMethod); + take(fields, out::acceptField); + + for (Triple triple : parameters) { + out.acceptMethodArg(triple.getLeft(), triple.getMiddle(), triple.getRight()); + } + parameters.clear(); + + for (LocalMapping mapping : locals) { + out.acceptMethodVar(mapping.method, mapping.lvtIndex, mapping.start, mapping.asmIndex, mapping.name); + } + locals.clear(); + } + + private void take(List> things, BiConsumer acceptor) { + for (Pair pair : things) { + acceptor.accept(pair.getLeft(), pair.getRight()); + } + things.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/me/modmuss50/optifabric/patcher/ClassCache.java b/src/main/java/me/modmuss50/optifabric/patcher/ClassCache.java index 8348a6df..8f6ac7b3 100644 --- a/src/main/java/me/modmuss50/optifabric/patcher/ClassCache.java +++ b/src/main/java/me/modmuss50/optifabric/patcher/ClassCache.java @@ -36,6 +36,7 @@ public class ClassCache { private final byte[] hash; private final Map classes = new HashMap<>(); + private boolean converted; public ClassCache(byte[] hash) { this.hash = hash; @@ -79,7 +80,8 @@ public static ClassCache read(File input) throws IOException { char formatRevision = dis.readChar(); //Check the format of the file boolean isFormatA = formatRevision == 'A'; boolean isFormatB = formatRevision == 'B'; - if (!isFormatA && !isFormatB && formatRevision != 'C') return new ClassCache(null); + boolean isFormatC = formatRevision == 'C'; + if (!isFormatA && !isFormatB && !isFormatC && formatRevision != 'D') return new ClassCache(null); long expectedCRC = dis.readLong(); @@ -139,6 +141,16 @@ public FieldVisitor visitField(int access, String name, String descriptor, Strin } } + if (isFormatA || isFormatB || isFormatC) { + try (StaticFuzzer fuzzer = new StaticFuzzer()) { + for (Entry entry : classCache.classes.entrySet()) { + entry.setValue(fuzzer.apply(entry.getValue())); + } + } + + classCache.converted = true; + } + return classCache; } } @@ -149,7 +161,7 @@ public void save(File output) throws IOException { } try (DataOutputStream dos = new DataOutputStream(new GZIPOutputStream(new FileOutputStream(output)))) { - dos.writeChar('C'); //Format version + dos.writeChar('D'); //Format version dos.writeLong(calculateCRC()); //Expected CRC to get from fully reading //Write the hash @@ -173,4 +185,8 @@ public void save(File output) throws IOException { } } } + + public boolean isConverted() { + return converted; + } } diff --git a/src/main/java/me/modmuss50/optifabric/patcher/LambdaRebuiler.java b/src/main/java/me/modmuss50/optifabric/patcher/LambdaRebuilder.java similarity index 59% rename from src/main/java/me/modmuss50/optifabric/patcher/LambdaRebuiler.java rename to src/main/java/me/modmuss50/optifabric/patcher/LambdaRebuilder.java index 685fb066..ad39f7f3 100644 --- a/src/main/java/me/modmuss50/optifabric/patcher/LambdaRebuiler.java +++ b/src/main/java/me/modmuss50/optifabric/patcher/LambdaRebuilder.java @@ -10,37 +10,83 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.function.Function; import java.util.jar.JarEntry; import java.util.jar.JarFile; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collector; import java.util.stream.Collectors; import com.google.common.collect.Sets; import com.google.common.util.concurrent.Runnables; +import org.apache.commons.lang3.tuple.Pair; + import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; -import net.fabricmc.tinyremapper.IMappingProvider; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.MappingResolver; +import net.fabricmc.tinyremapper.IMappingProvider.MappingAcceptor; +import net.fabricmc.tinyremapper.IMappingProvider.Member; import me.modmuss50.optifabric.util.ASMUtils; -public class LambdaRebuiler implements IMappingProvider { +public class LambdaRebuilder { + private static final boolean ALLOW_VAGUE_EQUIVALENCE = !Boolean.getBoolean("optifabric.exactOnly"); private final File optifineFile; private final File minecraftClientFile; - private final Map fixes = new HashMap<>(); + protected final Map> fixes = new HashMap<>(); + + public static void main(String... args) throws IOException { + if (args == null || args.length != 2) { + System.out.println("Usage: "); + return; + } + + File vanilla = new File(args[0]); + if (!vanilla.exists() || !vanilla.isFile()) { + System.err.println("Invalid vanilla class: " + args[0]); + System.exit(1); + } + File optifine = new File(args[1]); + if (!optifine.exists() || !optifine.isFile()) { + System.err.println("Invalid OptiFine class: " + args[0]); + System.exit(1); + } + + ClassNode minecraft = ASMUtils.readClass(vanilla); + ClassNode patched = ASMUtils.readClass(optifine); + + LambdaRebuilder rebuilder = new LambdaRebuilder(); + int unsolved = rebuilder.findLambdas(minecraft.name, minecraft.methods, patched.methods); + + System.out.printf(unsolved == 0 ? "Fully matched up %d lambdas:%n" : "Partially matched %d/%d lambdas%n", rebuilder.fixes.size(), rebuilder.fixes.size() + unsolved); + for (Entry> entry : rebuilder.fixes.entrySet()) { + Member lambda = entry.getKey(); + Pair remap = entry.getValue(); + System.out.printf("\t%s#%s%s => %s%n", lambda.owner, lambda.name, lambda.desc, remap.getLeft(), remap.getRight()); + } + } - public LambdaRebuiler(File optifineFile, File minecraftClientFile) throws IOException { + protected LambdaRebuilder() { + minecraftClientFile = optifineFile = null; + } + + public LambdaRebuilder(File optifineFile, File minecraftClientFile) throws IOException { this.optifineFile = optifineFile; this.minecraftClientFile = minecraftClientFile; } @@ -55,24 +101,30 @@ public void buildLambdaMap() throws IOException { if (name.endsWith(".class") && !name.startsWith("net/") && !name.startsWith("optifine/") && !name.startsWith("javax/")) { ClassNode classNode = ASMUtils.readClass(optifineJar, entry); - ClassNode minecraftClass = ASMUtils.readClass(clientJar, clientJar.getJarEntry(name)); + ClassNode minecraftClass = ASMUtils.readClass(clientJar, Objects.requireNonNull(clientJar.getJarEntry(name), name.concat(" not present in vanilla"))); - if (!minecraftClass.name.equals(classNode.name)) { - throw new RuntimeException("Something went wrong"); - } - - findLambdas(minecraftClass.name, minecraftClass.methods, classNode.methods); + findLambdas(minecraftClass, classNode); } } } } - private boolean findLambdas(String className, List original, List patched) { + protected int findLambdas(ClassNode original, ClassNode patched) { + if (!original.name.equals(patched.name)) { + throw new IllegalArgumentException("Patched class (" + patched.name + ") is not the same as the original (" + original.name + ')'); + } + + return findLambdas(original.name, original.methods, patched.methods); + } + + private int findLambdas(String className, List original, List patched) { + final Collector> methodMapper = Collectors.toMap(method -> method.name.concat(method.desc), Function.identity()); + List commonMethods = new ArrayList<>(); List lostMethods = new ArrayList<>(); List gainedMethods = new ArrayList<>(); { - Map originalMethods = original.stream().collect(Collectors.toMap(method -> method.name.concat(method.desc), Function.identity())); - Map patchedMethods = patched.stream().collect(Collectors.toMap(method -> method.name.concat(method.desc), Function.identity())); + Map originalMethods = original.stream().collect(methodMapper); + Map patchedMethods = patched.stream().collect(methodMapper); for (String methodName : Sets.union(originalMethods.keySet(), patchedMethods.keySet())) { MethodNode originalMethod = originalMethods.get(methodName); @@ -98,14 +150,13 @@ private boolean findLambdas(String className, List original, List !method.equal && method.hasLambdas()) || lostMethods.isEmpty() || gainedMethods.isEmpty()) return true; - - List gainedLambdas = gainedMethods.stream().filter(method -> (method.access & Opcodes.ACC_SYNTHETIC) != 0 && method.name.startsWith("lambda$")).collect(Collectors.toList()); - if (gainedLambdas.isEmpty()) return true; //Nothing looks like a lambda + if (commonMethods.stream().noneMatch(method -> !method.equal && method.hasLambdas()) || lostMethods.isEmpty() || gainedMethods.isEmpty()) return 0; - Map possibleLambdas = gainedLambdas.stream().collect(Collectors.toMap(method -> method.name.concat(method.desc), Function.identity())); //The collection of lambdas we're looking to fix, any others are irrelevant from the point of view that they're probably fine - Map nameToLosses = lostMethods.stream().collect(Collectors.toMap(method -> method.name.concat(method.desc), Function.identity())); + //The collection of lambdas we're looking to fix, any others are irrelevant from the point of view that they're probably fine + Map possibleLambdas = gainedMethods.stream().filter(method -> (method.access & Opcodes.ACC_SYNTHETIC) != 0 && method.name.startsWith("lambda$")).collect(methodMapper); + if (possibleLambdas.isEmpty()) return 0; //Nothing looks like a lambda + Map nameToLosses = lostMethods.stream().collect(methodMapper); for (int i = 0; i < commonMethods.size(); i++) {//Indexed for loop as each added fix will add to commonMethods MethodComparison method = commonMethods.get(i); @@ -127,7 +178,14 @@ private boolean findLambdas(String className, List original, List { @@ -171,11 +229,11 @@ private boolean findLambdas(String className, List original, List commonMethods, List lostMethods, List gainedMethods, @@ -193,7 +251,9 @@ private void resolveCloseMethod(String className, List commonM private void pairUp(String className, List commonMethods, List lostMethods, List gainedMethods, List originalLambdas, List patchedLambdas, Map nameToLosses, Map possibleLambdas, Runnable onPair) { - for (Iterator itOriginal = originalLambdas.iterator(), itPatched = patchedLambdas.iterator(); itOriginal.hasNext() && itPatched.hasNext();) { + assert originalLambdas.size() == patchedLambdas.size(); //It would be silly to pair up lists which aren't the same length + + for (Iterator itOriginal = originalLambdas.iterator(), itPatched = patchedLambdas.iterator(); itOriginal.hasNext() && itPatched.hasNext();) { Lambda lost = itOriginal.next(); Lambda gained = itPatched.next(); @@ -206,7 +266,7 @@ private void pairUp(String className, List commonMethods, List if (lostMethod == null) { if (gainedMethod == null) { assert Objects.equals(lost.getFullName(), gained.getFullName()); - return; + continue; } else { throw new IllegalStateException("Couldn't find original method for lambda: " + lost.getFullName()); } @@ -223,20 +283,51 @@ private void pairUp(String className, List commonMethods, List } private boolean addFix(String className, List commonMethods, MethodNode from, MethodNode to) { - if (!from.desc.equals(to.desc)) { + boolean vague = !from.desc.equals(to.desc); //Are we trying to fudge a fix? + + if (vague && !ALLOW_VAGUE_EQUIVALENCE) { System.err.println("Description changed remapping lambda handle: " + className + '#' + from.name + from.desc + " => " + className + '#' + to.name + to.desc); return false; //Don't add the fix if it is wrong + } else if (vague) { + System.out.printf("Fuzzing %s#%s%s as %s%s%n", className, from.name, from.desc, to.name, to.desc); } - fixes.put(new Member(className, from.name, from.desc), to.name); + fixes.put(new Member(className, from.name, from.desc), Pair.of(to.name, to.desc)); from.name = to.name; //Apply the rename to the actual method node too - commonMethods.add(new MethodComparison(to, from)); + commonMethods.add(new MethodComparison(to, from, vague)); return true; } - @Override - public void load(MappingAcceptor out) { - fixes.forEach(out::acceptMethod); + public Map> load(String from, MappingAcceptor out) { + MappingResolver mapper = FabricLoader.getInstance().getMappingResolver(); + Map> fuzzes = ALLOW_VAGUE_EQUIVALENCE ? new HashMap<>() : Collections.emptyMap(); + + for (Entry> entry : fixes.entrySet()) { + Member lambda = entry.getKey(); + Pair remap = entry.getValue(); + + String lambdaOwner = lambda.owner.replace('/', '.'); + String vanilla = mapper.mapMethodName(from, lambdaOwner, remap.getLeft(), remap.getRight()); + out.acceptMethod(lambda, vanilla); + + if (!lambda.desc.equals(remap.getRight())) { + assert ALLOW_VAGUE_EQUIVALENCE; + fuzzes.computeIfAbsent(mapper.mapClassName(from, lambdaOwner).replace('.', '/'), k -> new HashMap<>()).put(map(mapper, from, vanilla, lambda.desc), map(mapper, from, vanilla, remap.getRight())); + } + } + + return fuzzes; + } + + private static String map(MappingResolver mapper, String from, String name, String desc) { + StringBuffer buf = new StringBuffer(name); + + Matcher matcher = Pattern.compile("L([^;/]+?);").matcher(desc); + while (matcher.find()) { + matcher.appendReplacement(buf, Matcher.quoteReplacement('L' + mapper.mapClassName(from, matcher.group(1).replace('/', '.')).replace('.', '/') + ';')); + } + + return matcher.appendTail(buf).toString(); } } diff --git a/src/main/java/me/modmuss50/optifabric/patcher/MethodComparison.java b/src/main/java/me/modmuss50/optifabric/patcher/MethodComparison.java index 3a607ded..5b54ba45 100644 --- a/src/main/java/me/modmuss50/optifabric/patcher/MethodComparison.java +++ b/src/main/java/me/modmuss50/optifabric/patcher/MethodComparison.java @@ -41,8 +41,12 @@ class MethodComparison { private final List patchedLambdas = new ArrayList<>(); public MethodComparison(MethodNode original, MethodNode patched) { + this(original, patched, false); + } + + MethodComparison(MethodNode original, MethodNode patched, boolean permissive) { assert Objects.equals(original.name, patched.name); - assert Objects.equals(original.desc, patched.desc); + assert Objects.equals(original.desc, patched.desc) || permissive; node = patched; effectivelyEqual = compare(original.instructions, patched.instructions); @@ -240,7 +244,7 @@ private boolean compare(InsnList listA, InsnList listB, AbstractInsnNode insnA, } } - private static boolean isJavaLambdaMetafactory(Handle bsm) { + static boolean isJavaLambdaMetafactory(Handle bsm) { return bsm.getTag() == Opcodes.H_INVOKESTATIC && "java/lang/invoke/LambdaMetafactory".equals(bsm.getOwner()) && ("metafactory".equals(bsm.getName()) @@ -306,4 +310,9 @@ public List getOriginalLambads() { public List getPatchedLambads() { return Collections.unmodifiableList(patchedLambdas); } + + @Override + public String toString() { + return node.name.concat(node.desc); + } } \ No newline at end of file diff --git a/src/main/java/me/modmuss50/optifabric/patcher/StaticFuzzer.java b/src/main/java/me/modmuss50/optifabric/patcher/StaticFuzzer.java new file mode 100644 index 00000000..8b1d176e --- /dev/null +++ b/src/main/java/me/modmuss50/optifabric/patcher/StaticFuzzer.java @@ -0,0 +1,199 @@ +package me.modmuss50.optifabric.patcher; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.Map.Entry; + +import org.apache.commons.lang3.tuple.Pair; + +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InvokeDynamicInsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; + +import net.fabricmc.loader.launch.common.FabricLauncherBase; + +import net.fabricmc.tinyremapper.IMappingProvider.Member; + +import me.modmuss50.optifabric.util.ASMUtils; +import me.modmuss50.optifabric.util.ThrowingFunction; + +public class StaticFuzzer extends LambdaRebuilder implements ThrowingFunction, Closeable { + private final Map> fuzzes; + private final JarFile vanilla; + + public StaticFuzzer() throws IOException { + this(null); + } + + public StaticFuzzer(Map> fuzzes) throws IOException { + this.fuzzes = fuzzes; + vanilla = new JarFile(FabricLauncherBase.minecraftJar.toFile()); + } + + private ClassNode getVanilla(String name) { + try { + JarEntry entry = vanilla.getJarEntry(name.concat(".class")); + return entry != null ? ASMUtils.readClass(vanilla, entry) : null; + } catch (IOException e) { + throw new RuntimeException("Error reading vanilla class " + name + " from " + vanilla.getName(), e); + } + } + + public byte[] apply(byte[] in) { + assert fuzzes == null; + ClassReader reader = new ClassReader(in); + + ClassNode vanilla = getVanilla(reader.getClassName()); + if (vanilla == null) { + System.err.println("Failed to find " + reader.getClassName()); + return in; + } + ClassNode optifine = new ClassNode(); + reader.accept(optifine, 0); + findLambdas(vanilla, optifine); + + Map toCheck = new HashMap<>(); + for (Entry> entry : fixes.entrySet()) { + Member lambda = entry.getKey(); + Pair remap = entry.getValue(); + assert lambda.owner.equals(reader.getClassName()); + toCheck.put(lambda.name.concat(lambda.desc), remap.getLeft().concat(remap.getRight())); + } + + if (toCheck.isEmpty()) { + assert fixes.isEmpty(); + return in; + } else { + fixes.clear(); + return apply(reader, vanilla, optifine, toCheck); + } + } + + @Override + @SuppressWarnings("deprecation") //Reduce, reuse, recycle + public byte[] apply(InputStream in) throws IOException { + assert fuzzes != null; + ClassReader reader = new ClassReader(in); + + Map toCheck = fuzzes.get(reader.getClassName()); + if (toCheck == null) return reader.b; + + ClassNode optifine = new ClassNode(); + reader.accept(optifine, 0); + ClassNode vanilla = getVanilla(reader.getClassName()); + return vanilla != null ? apply(reader, vanilla, optifine, toCheck) : reader.b; + } + + private byte[] apply(ClassReader reader, ClassNode vanilla, ClassNode optifine, Map toCheck) { + fix(toCheck, vanilla, optifine); + + ClassWriter writer = new ClassWriter(reader, 0); + optifine.accept(writer); + return writer.toByteArray(); + } + + private void fix(Map toCheck, ClassNode minecraft, ClassNode optifine) { + Object2IntMap memberToAccess = new Object2IntOpenHashMap<>(minecraft.methods.size()); + memberToAccess.defaultReturnValue(-1); + for (MethodNode method : minecraft.methods) { + String key = method.name.concat(method.desc); + memberToAccess.put(key, method.access); + } + + Set staticFlip = new HashSet<>(); + for (MethodNode method : optifine.methods) { + String key = method.name.concat(method.desc); + + String remap = toCheck.get(key); + if (remap == null) continue; + int access = memberToAccess.getInt(remap); + if (access == -1) throw new IllegalStateException("Unable to find vanilla method " + minecraft.name + '#' + remap); + + if (Modifier.isStatic(method.access) != Modifier.isStatic(access)) { + if (Modifier.isPrivate(method.access)) {//We'll just locally fix the difference + staticFlip.add(key); + } else { + if (Modifier.isStatic(method.access)) {//Become static, previously wasn't + Type[] args = Type.getArgumentTypes(method.desc); + + if (args.length >= 1 && optifine.name.equals(args[0].getInternalName())) {//Could we fix it quickly? + staticFlip.add(method.name.concat(method.desc)); + } else {//Not really + System.err.println("Method has become static: " + optifine.name + '#' + key); + } + } else {//No longer static, previously was + System.err.println("Method is no longer static: " + optifine.name + '#' + key); + } + } + } + } + + if (!staticFlip.isEmpty()) { + for (MethodNode method : optifine.methods) { + if (staticFlip.contains(method.name.concat(method.desc))) { + if (Modifier.isStatic(method.access ^= Modifier.STATIC)) {//Method made static, need to add this as a parameter + method.desc = "(L" + optifine.name + ';' + method.desc.substring(1); + } else {//Method no longer static, need to add a this somehow + Type[] args = Type.getArgumentTypes(method.desc); + + if (args.length >= 1 && optifine.name.equals(args[0].getInternalName())) {//Just remove the leading type argument + method.desc = Type.getMethodDescriptor(Type.getReturnType(method.desc), Arrays.copyOfRange(args, 1, args.length)); + } else {//More of a nuisance, all the LVT indices will need bumping up by one + throw new UnsupportedOperationException("Removing static from " + optifine.name + '#' + method.name + method.desc + " is a bit more effort"); + } + } + } + + for (AbstractInsnNode insn : method.instructions) { + switch (insn.getType()) { + case AbstractInsnNode.METHOD_INSN: { + MethodInsnNode minsn = (MethodInsnNode) insn; + + if (optifine.name.equals(minsn.owner) && staticFlip.contains(minsn.name.concat(minsn.desc))) { + minsn.setOpcode(minsn.getOpcode() == Opcodes.INVOKESTATIC ? Opcodes.INVOKEVIRTUAL : Opcodes.INVOKESTATIC); + } + break; + } + + case AbstractInsnNode.INVOKE_DYNAMIC_INSN: { + InvokeDynamicInsnNode dinsn = (InvokeDynamicInsnNode) insn; + + if (MethodComparison.isJavaLambdaMetafactory(dinsn.bsm)) { + Handle lambda = (Handle) dinsn.bsmArgs[1]; + if (optifine.name.equals(lambda.getOwner()) && staticFlip.contains(lambda.getName().concat(lambda.getDesc()))) { + dinsn.bsmArgs[1] = new Handle(lambda.getTag() == Opcodes.H_INVOKESTATIC ? Opcodes.H_INVOKEVIRTUAL : Opcodes.H_INVOKESTATIC, + lambda.getOwner(), lambda.getName(), lambda.getDesc(), lambda.isInterface()); + } + } + break; + } + } + } + } + } + } + + @Override + public void close() throws IOException { + vanilla.close(); + } +} \ No newline at end of file diff --git a/src/main/java/me/modmuss50/optifabric/util/ASMUtils.java b/src/main/java/me/modmuss50/optifabric/util/ASMUtils.java index fe4ab564..28432b4f 100644 --- a/src/main/java/me/modmuss50/optifabric/util/ASMUtils.java +++ b/src/main/java/me/modmuss50/optifabric/util/ASMUtils.java @@ -1,5 +1,7 @@ package me.modmuss50.optifabric.util; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Objects; @@ -14,6 +16,12 @@ public static ClassNode readClass(byte[] bytes) { return readClass(new ClassReader(Objects.requireNonNull(bytes, "Cannot read null class bytes"))); } + public static ClassNode readClass(File file) throws IOException { + try (InputStream in = new FileInputStream(Objects.requireNonNull(file, "Cannot read null file"))) { + return readClass(new ClassReader(in)); + } + } + public static ClassNode readClass(JarFile jar, JarEntry entry) throws IOException { try (InputStream in = jar.getInputStream(entry)) { return readClass(new ClassReader(Objects.requireNonNull(in, "Entry not present in jar"))); diff --git a/src/main/java/me/modmuss50/optifabric/util/ThrowingFunction.java b/src/main/java/me/modmuss50/optifabric/util/ThrowingFunction.java new file mode 100644 index 00000000..d114b391 --- /dev/null +++ b/src/main/java/me/modmuss50/optifabric/util/ThrowingFunction.java @@ -0,0 +1,21 @@ +package me.modmuss50.optifabric.util; + +import java.util.Objects; + +public interface ThrowingFunction { + R apply(T thing) throws E; + + default ThrowingFunction compose(ThrowingFunction before) { + Objects.requireNonNull(before); + return v -> apply(before.apply(v)); + } + + default ThrowingFunction andThen(ThrowingFunction after) { + Objects.requireNonNull(after); + return t -> after.apply(apply(t)); + } + + static ThrowingFunction identity() { + return t -> t; + } +} \ No newline at end of file diff --git a/src/main/resources/optifabric.compat.architectury-AB.new-mixins.json b/src/main/resources/optifabric.compat.architectury-AB.new-mixins.json new file mode 100644 index 00000000..49362205 --- /dev/null +++ b/src/main/resources/optifabric.compat.architectury-AB.new-mixins.json @@ -0,0 +1,9 @@ +{ + "parent": "optifabric.mixins.json", + "package": "me.modmuss50.optifabric.compat.architectury.mixin", + "plugin": "me.modmuss50.optifabric.compat.InterceptingMixinPlugin", + "mixins": [ + "GameRendererNewMixin", + "SpriteAtlasTextureNewMixin" + ] +} \ No newline at end of file