diff --git a/definition/src/main/java/org/sinytra/adapter/patch/MethodContextImpl.java b/definition/src/main/java/org/sinytra/adapter/patch/MethodContextImpl.java index 8388c0e..df4a3c4 100644 --- a/definition/src/main/java/org/sinytra/adapter/patch/MethodContextImpl.java +++ b/definition/src/main/java/org/sinytra/adapter/patch/MethodContextImpl.java @@ -96,8 +96,8 @@ public LocalVariableLookup dirtyLocalsTable() { @Override public MethodQualifier getTargetMethodQualifier() { // Get method targets - List methodRefs = methodAnnotation().>getValue("method").orElseThrow().get(); - if (methodRefs.size() > 1) { + List methodRefs = methodAnnotation().>getValue("method").map(AnnotationValueHandle::get).orElseGet(Collections::emptyList); + if (methodRefs.size() != 1) { // We only support single method targets for now return null; } @@ -246,7 +246,7 @@ private InsnList computeSlicedInsns(ISliceContext context, AnnotationNode annota @Nullable private TargetPair findInjectionTarget(ClassLookup lookup) { - Pair> pair = findInjectionTargetCandidates(lookup); + Pair> pair = findInjectionTargetCandidates(lookup, false); if (pair == null) { return null; } @@ -263,7 +263,7 @@ private TargetPair findInjectionTarget(ClassLookup lookup) { } @Nullable - public Pair> findInjectionTargetCandidates(ClassLookup lookup) { + public Pair> findInjectionTargetCandidates(ClassLookup lookup, boolean ignoreDesc) { // Find target method qualifier MethodQualifier qualifier = getTargetMethodQualifier(); if (qualifier == null || qualifier.name() == null) { @@ -288,7 +288,7 @@ public Pair> findInjectionTargetCandidates(ClassLook // Find target method in class String desc = qualifier.desc(); List candidates = targetClass.methods.stream() - .filter(mtd -> mtd.name.equals(qualifier.name()) && (desc == null || mtd.desc.equals(desc))) + .filter(mtd -> mtd.name.equals(qualifier.name()) && (ignoreDesc || desc == null || mtd.desc.equals(desc))) .toList(); // If there's multiple candidates, try removing bouncer methods if (candidates.size() > 1 && desc == null) { diff --git a/definition/src/main/java/org/sinytra/adapter/patch/analysis/MethodCallAnalyzer.java b/definition/src/main/java/org/sinytra/adapter/patch/analysis/MethodCallAnalyzer.java index 3388501..02776da 100644 --- a/definition/src/main/java/org/sinytra/adapter/patch/analysis/MethodCallAnalyzer.java +++ b/definition/src/main/java/org/sinytra/adapter/patch/analysis/MethodCallAnalyzer.java @@ -131,6 +131,22 @@ public static List findMethodCallParamInsns(MethodNode methodN return interpreter.getTargetArgs(); } + @Nullable + public static List findFullMethodCallParamInsns(MethodNode methodNode, MethodInsnNode minsn) { + List insns = findMethodCallParamInsns(methodNode, minsn); + if (minsn != null) { + List fullInsns = new ArrayList<>(); + for (AbstractInsnNode i = insns.getFirst(); i != null ; i = i.getNext()) { + if (i == minsn) { + break; + } + fullInsns.add(i); + } + return fullInsns; + } + return null; + } + public static List analyzeMethod(MethodNode methodNode, NaryOperationHandler handler) { return analyzeMethod(methodNode, (insn, values) -> true, handler); } diff --git a/definition/src/main/java/org/sinytra/adapter/patch/analysis/MethodLabelComparator.java b/definition/src/main/java/org/sinytra/adapter/patch/analysis/MethodLabelComparator.java new file mode 100644 index 0000000..826d4d7 --- /dev/null +++ b/definition/src/main/java/org/sinytra/adapter/patch/analysis/MethodLabelComparator.java @@ -0,0 +1,105 @@ +package org.sinytra.adapter.patch.analysis; + +import com.mojang.datafixers.util.Pair; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.FrameNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.MethodNode; +import org.sinytra.adapter.patch.api.MethodContext; + +import java.util.*; +import java.util.stream.Stream; + +public class MethodLabelComparator { + public record ComparisonResult(List> patchedLabels, List cleanLabel) {} + + @Nullable + public static ComparisonResult findPatchedLabels(AbstractInsnNode cleanInjectionInsn, MethodContext methodContext) { + List> cleanLabels = getLabelsInMethod(methodContext.findCleanInjectionTarget().methodNode()); + List> cleanLabelsOriginal = List.copyOf(cleanLabels); + + List> cleanMatchedLabels = cleanLabels.stream() + .filter(insns -> insns.contains(cleanInjectionInsn)) + .toList(); + if (cleanMatchedLabels.size() != 1) { + return null; + } + List cleanLabel = cleanMatchedLabels.getFirst(); + + List> dirtyLabels = getLabelsInMethod(methodContext.findDirtyInjectionTarget().methodNode()); + List> dirtyLabelsOriginal = List.copyOf(dirtyLabels); + + Map, List> matchedLabels = new LinkedHashMap<>(); + for (List cleanInsns : cleanLabelsOriginal) { + List> candidates = new ArrayList<>(); + + for (List dirtyInsns : dirtyLabels) { + if (InstructionMatcher.test(cleanInsns, dirtyInsns, InsnComparator.IGNORE_VAR_INDEX | InsnComparator.IGNORE_LINE_NUMBERS)) { + candidates.add(dirtyInsns); + } + } + // TODO Try and come up with something better + // This prevents messing up the order of labels + // Without this countermeasure, it might happen that a label that was deleted will match a seemingly identical label somewhere else in the method, which is wrong + // We disable any duplicated until we can properly handle such cases + if (candidates.size() == 1) { + List dirtyInsns = candidates.getFirst(); + matchedLabels.put(cleanInsns, dirtyInsns); + cleanLabels.remove(cleanInsns); + dirtyLabels.remove(dirtyInsns); + } + } + + Pair, List> patchRange = findPatchHunkRange(cleanLabel, cleanLabelsOriginal, matchedLabels); + if (patchRange == null) { + return null; + } + + List> patchedLabels = dirtyLabelsOriginal.subList(dirtyLabelsOriginal.indexOf(patchRange.getFirst()) + 1, dirtyLabelsOriginal.indexOf(patchRange.getSecond())); + return new ComparisonResult(patchedLabels, cleanLabel); + } + + @Nullable + private static Pair, List> findPatchHunkRange(List cleanLabel, List> cleanLabels, Map, List> matchedLabels) { + // Find last matched dirty label BEFORE the injection point + List dirtyLabelBefore = Stream.iterate(cleanLabels.indexOf(cleanLabel), i -> i >= 0, i -> i - 1) + .map(i -> matchedLabels.get(cleanLabels.get(i))) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + if (dirtyLabelBefore == null) { + return null; + } + + // Find first matched dirty label AFTER the injection point + List dirtyLabelAfter = Stream.iterate(cleanLabels.indexOf(cleanLabel), i -> i < cleanLabels.size(), i -> i + 1) + .map(i -> matchedLabels.get(cleanLabels.get(i))) + .filter(Objects::nonNull) + .findFirst() + .orElse(null); + if (dirtyLabelAfter == null) { + return null; + } + + return Pair.of(dirtyLabelBefore, dirtyLabelAfter); + } + + private static List> getLabelsInMethod(MethodNode methodNode) { + List> list = new ArrayList<>(); + List workingList = null; + for (AbstractInsnNode insn : methodNode.instructions) { + if (insn instanceof FrameNode) { + continue; + } + if (insn instanceof LabelNode) { + if (workingList != null) { + list.add(workingList); + } + workingList = new ArrayList<>(); + } + workingList.add(insn); + } + return list; + } +} diff --git a/definition/src/main/java/org/sinytra/adapter/patch/api/MethodContext.java b/definition/src/main/java/org/sinytra/adapter/patch/api/MethodContext.java index 28fa786..b3c2c3d 100644 --- a/definition/src/main/java/org/sinytra/adapter/patch/api/MethodContext.java +++ b/definition/src/main/java/org/sinytra/adapter/patch/api/MethodContext.java @@ -55,7 +55,7 @@ public interface MethodContext { List computeInjectionTargetInsns(@Nullable TargetPair target); @Nullable - Pair> findInjectionTargetCandidates(ClassLookup lookup); + Pair> findInjectionTargetCandidates(ClassLookup lookup, boolean ignoreDesc); void updateDescription(List parameters); diff --git a/definition/src/main/java/org/sinytra/adapter/patch/api/MixinConstants.java b/definition/src/main/java/org/sinytra/adapter/patch/api/MixinConstants.java index 28d75e5..9209581 100644 --- a/definition/src/main/java/org/sinytra/adapter/patch/api/MixinConstants.java +++ b/definition/src/main/java/org/sinytra/adapter/patch/api/MixinConstants.java @@ -32,5 +32,6 @@ public class MixinConstants { public static final String UNIQUE = "Lorg/spongepowered/asm/mixin/Unique;"; public static final String SHADOW = "Lorg/spongepowered/asm/mixin/Shadow;"; public static final String COERCE = "Lorg/spongepowered/asm/mixin/injection/Coerce;"; + public static final String CONSTANT = "Lorg/spongepowered/asm/mixin/injection/Constant;"; public static final List LVT_COMPATIBILITY_LEVELS = List.of(FabricUtil.COMPATIBILITY_0_10_0, FabricUtil.COMPATIBILITY_0_9_2); } diff --git a/definition/src/main/java/org/sinytra/adapter/patch/api/Patch.java b/definition/src/main/java/org/sinytra/adapter/patch/api/Patch.java index b56fd19..e4e3a32 100644 --- a/definition/src/main/java/org/sinytra/adapter/patch/api/Patch.java +++ b/definition/src/main/java/org/sinytra/adapter/patch/api/Patch.java @@ -16,6 +16,7 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.function.Supplier; public interface Patch { static ClassPatchBuilder builder() { @@ -44,6 +45,10 @@ public Result or(Result other) { } return this; } + + public Result orElseGet(Supplier other) { + return this == PASS ? other.get() : this; + } } interface Builder> extends MethodTransformBuilder { diff --git a/definition/src/main/java/org/sinytra/adapter/patch/transformer/MirrorableExtractMixin.java b/definition/src/main/java/org/sinytra/adapter/patch/transformer/MirrorableExtractMixin.java new file mode 100644 index 0000000..19cad56 --- /dev/null +++ b/definition/src/main/java/org/sinytra/adapter/patch/transformer/MirrorableExtractMixin.java @@ -0,0 +1,79 @@ +package org.sinytra.adapter.patch.transformer; + +import com.google.common.collect.ImmutableList; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.GeneratorAdapter; +import org.objectweb.asm.commons.Method; +import org.objectweb.asm.tree.*; +import org.sinytra.adapter.patch.analysis.MethodCallAnalyzer; +import org.sinytra.adapter.patch.api.*; +import org.sinytra.adapter.patch.util.AdapterUtil; +import org.sinytra.adapter.patch.util.OpcodeUtil; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Stream; + +public record MirrorableExtractMixin(String destinationClass, MethodInsnNode destinationMethodInvocation) implements MethodTransform { + @Override + public Patch.Result apply(ClassNode classNode, MethodNode methodNode, MethodContext methodContext, PatchContext context) { + Type selfType = Type.getObjectType(methodContext.findDirtyInjectionTarget().classNode().name); + Type[] params = Type.getArgumentTypes(this.destinationMethodInvocation.desc); + int selfIndex = Stream.of(Stream.iterate(0, i -> i < params.length, i -> i + 1) + .filter(i -> params[i].equals(selfType)) + .toList()) + .filter(list -> list.size() == 1) + .map(List::getFirst) + .findFirst() + .orElse(-1); + if (selfIndex == -1) { + return Patch.Result.PASS; + } + + List callInsns = MethodCallAnalyzer.findMethodCallParamInsns(methodContext.findDirtyInjectionTarget().methodNode(), this.destinationMethodInvocation); + if (callInsns == null || callInsns.size() <= selfIndex) { + return Patch.Result.PASS; + } + AbstractInsnNode selfParamInsn = callInsns.get(selfIndex); + if (!(selfParamInsn instanceof VarInsnNode varInsn) || varInsn.getOpcode() != Opcodes.ALOAD || varInsn.var != 0) { + return Patch.Result.PASS; + } + // Cool, out instance is passed into the method. Now let's inject there and call the old mixin method + ClassNode generatedTarget = methodContext.patchContext().environment().classGenerator().getOrGenerateMixinClass(methodContext.getMixinClass(), this.destinationClass, null); + methodContext.patchContext().environment().refmapHolder().copyEntries(methodContext.getMixinClass().name, generatedTarget.name); + // Generate a method with the same injector annotation + MethodNode originalMixinMethod = methodContext.getMixinMethod(); + String name = originalMixinMethod.name + "$adapter$mirror$" + AdapterUtil.randomString(5); + List originalParams = List.of(Type.getArgumentTypes(originalMixinMethod.desc)); + List newParams = ImmutableList.builder().add(Type.getArgumentTypes(this.destinationMethodInvocation.desc)).add(AdapterUtil.CI_TYPE).build(); + // Make sure we have all required params + if (!new HashSet<>(newParams).containsAll(originalParams)) { + return Patch.Result.PASS; + } + + String desc = Type.getMethodDescriptor(Type.VOID_TYPE, newParams.toArray(Type[]::new)); + // Change target + BundledMethodTransform.builder().modifyTarget(this.destinationMethodInvocation.name + this.destinationMethodInvocation.desc).apply(methodContext); + MethodNode invokerMixinMethod = (MethodNode) generatedTarget.visitMethod(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC, name, desc, null, null); + invokerMixinMethod.visibleAnnotations = new ArrayList<>(originalMixinMethod.visibleAnnotations); + // Make original mixin a unique public method + originalMixinMethod.access = OpcodeUtil.setAccessVisibility(originalMixinMethod.access, Opcodes.ACC_PUBLIC); + originalMixinMethod.visibleAnnotations.remove(methodContext.methodAnnotation().unwrap()); + originalMixinMethod.visitAnnotation(MixinConstants.UNIQUE, true); + // Now call the original mixin + GeneratorAdapter gen = new GeneratorAdapter(invokerMixinMethod, invokerMixinMethod.access, invokerMixinMethod.name, invokerMixinMethod.desc); + gen.newLabel(); + gen.loadArg(selfIndex); + for (Type type : originalParams) { + gen.loadArg(newParams.indexOf(type)); + } + gen.invokeVirtual(selfType, new Method(originalMixinMethod.name, originalMixinMethod.desc)); + gen.newLabel(); + gen.returnValue(); + gen.newLabel(); + gen.endMethod(); + return Patch.Result.APPLY; + } +} diff --git a/definition/src/main/java/org/sinytra/adapter/patch/transformer/dynfix/DynFixMethodComparison.java b/definition/src/main/java/org/sinytra/adapter/patch/transformer/dynfix/DynFixMethodComparison.java index 410d8fb..fd8ce76 100644 --- a/definition/src/main/java/org/sinytra/adapter/patch/transformer/dynfix/DynFixMethodComparison.java +++ b/definition/src/main/java/org/sinytra/adapter/patch/transformer/dynfix/DynFixMethodComparison.java @@ -1,27 +1,29 @@ package org.sinytra.adapter.patch.transformer.dynfix; -import com.google.common.collect.ImmutableList; -import com.mojang.datafixers.util.Pair; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; -import org.objectweb.asm.commons.GeneratorAdapter; -import org.objectweb.asm.commons.Method; import org.objectweb.asm.tree.*; -import org.sinytra.adapter.patch.analysis.InsnComparator; -import org.sinytra.adapter.patch.analysis.InstructionMatcher; +import org.sinytra.adapter.patch.analysis.LocalVariableLookup; import org.sinytra.adapter.patch.analysis.MethodCallAnalyzer; +import org.sinytra.adapter.patch.analysis.MethodLabelComparator; import org.sinytra.adapter.patch.api.MethodContext; import org.sinytra.adapter.patch.api.MixinConstants; import org.sinytra.adapter.patch.api.Patch; +import org.sinytra.adapter.patch.selector.AnnotationHandle; import org.sinytra.adapter.patch.selector.AnnotationValueHandle; import org.sinytra.adapter.patch.transformer.BundledMethodTransform; +import org.sinytra.adapter.patch.transformer.MirrorableExtractMixin; import org.sinytra.adapter.patch.transformer.ModifyInjectionPoint; +import org.sinytra.adapter.patch.transformer.param.*; import org.sinytra.adapter.patch.util.AdapterUtil; -import org.sinytra.adapter.patch.util.OpcodeUtil; -import java.util.*; -import java.util.stream.Stream; +import java.util.HashSet; +import java.util.List; +import java.util.Set; public class DynFixMethodComparison implements DynamicFixer { private static final Set ACCEPTED_ANNOTATIONS = Set.of(MixinConstants.INJECT, MixinConstants.WRAP_OPERATION); @@ -46,54 +48,19 @@ public Data prepare(MethodContext methodContext) { @Override public Patch.Result apply(ClassNode classNode, MethodNode methodNode, MethodContext methodContext, Data data) { - List> cleanLabels = getLabelsInMethod(methodContext.findCleanInjectionTarget().methodNode()); - List> cleanLabelsOriginal = List.copyOf(cleanLabels); - AbstractInsnNode cleanInjectionInsn = data.cleanInjectionInsn(); - List> cleanMatchedLabels = cleanLabels.stream() - .filter(insns -> insns.contains(cleanInjectionInsn)) - .toList(); - if (cleanMatchedLabels.size() != 1) { - return Patch.Result.PASS; - } - List cleanLabel = cleanMatchedLabels.getFirst(); - - List> dirtyLabels = getLabelsInMethod(methodContext.findDirtyInjectionTarget().methodNode()); - List> dirtyLabelsOriginal = List.copyOf(dirtyLabels); - - Map, List> matchedLabels = new LinkedHashMap<>(); - for (List cleanInsns : cleanLabelsOriginal) { - List> candidates = new ArrayList<>(); - - for (List dirtyInsns : dirtyLabels) { - if (InstructionMatcher.test(cleanInsns, dirtyInsns, InsnComparator.IGNORE_VAR_INDEX | InsnComparator.IGNORE_LINE_NUMBERS)) { - candidates.add(dirtyInsns); - } - } - // TODO Try and come up with something better - // This prevents messing up the order of labels - // Without this countermeasure, it might happen that a label that was deleted will match a seemingly identical label somewhere else in the method, which is wrong - // We disable any duplicated until we can properly handle such cases - if (candidates.size() == 1) { - List dirtyInsns = candidates.getFirst(); - matchedLabels.put(cleanInsns, dirtyInsns); - cleanLabels.remove(cleanInsns); - dirtyLabels.remove(dirtyInsns); - } - } - - Pair, List> patchRange = findPatchHunkRange(cleanLabel, cleanLabelsOriginal, matchedLabels); - if (patchRange == null) { + MethodLabelComparator.ComparisonResult comparisonResult = MethodLabelComparator.findPatchedLabels(cleanInjectionInsn, methodContext); + if (comparisonResult == null) { return Patch.Result.PASS; } - - ClassNode cleanTargetClass = methodContext.findCleanInjectionTarget().classNode(); - List> hunkLabels = dirtyLabelsOriginal.subList(dirtyLabelsOriginal.indexOf(patchRange.getFirst()) + 1, dirtyLabelsOriginal.indexOf(patchRange.getSecond())); + List> hunkLabels = comparisonResult.patchedLabels(); if (methodContext.methodAnnotation().matchesDesc(MixinConstants.WRAP_OPERATION)) { - return handleTargetModification(hunkLabels, methodContext); + return handleWrapOperationToInstanceOf(cleanInjectionInsn, comparisonResult.cleanLabel(), hunkLabels, methodContext) + .orElseGet(() -> handleTargetModification(hunkLabels, methodContext)); } + ClassNode cleanTargetClass = methodContext.findCleanInjectionTarget().classNode(); for (List insns : hunkLabels) { for (AbstractInsnNode insn : insns) { if (insn instanceof MethodInsnNode minsn && minsn.getOpcode() == Opcodes.INVOKESTATIC && !minsn.owner.equals(cleanTargetClass.name)) { @@ -118,6 +85,100 @@ public Patch.Result apply(ClassNode classNode, MethodNode methodNode, MethodCont return Patch.Result.PASS; } + private static Patch.Result handleWrapOperationToInstanceOf(AbstractInsnNode cleanInjectionInsn, List cleanLabel, List> hunkLabels, MethodContext methodContext) { + if (!(cleanInjectionInsn instanceof MethodInsnNode minsn) || hunkLabels.size() != 1 || !(cleanLabel.getLast() instanceof JumpInsnNode) || cleanLabel.stream().anyMatch(i -> i instanceof TypeInsnNode)) { + return Patch.Result.PASS; + } + List dirtyLabel = hunkLabels.getFirst(); + if (!(dirtyLabel.getLast() instanceof JumpInsnNode)) { + return Patch.Result.PASS; + } + List instanceOfCalls = dirtyLabel.stream().filter(i -> i instanceof TypeInsnNode).map(i -> (TypeInsnNode) i).toList(); + if (instanceOfCalls.size() != 1) { + return Patch.Result.PASS; + } + TypeInsnNode instanceOfCall = instanceOfCalls.getFirst(); + MethodNode methodNode = methodContext.getMixinMethod(); + LocalVariableLookup mixinLocals = new LocalVariableLookup(methodNode); + + Type[] argsTypes = Type.getArgumentTypes(methodNode.desc); + Set paramVars = new HashSet<>(); + for (int i = 0; i < argsTypes.length; i++) { + if (argsTypes[i].equals(AdapterUtil.OPERATION_TYPE)) { + break; + } + LocalVariableNode lvn = mixinLocals.getByParameterOrdinal(i); + paramVars.add(lvn.index); + } + + ParamTransformationUtil.extractWrapOperation(methodContext, methodNode, List.of(argsTypes), op -> { + for (int i = 1; i < paramVars.size(); i++) { + op.removeParameter(i); + } + }); + + List originalOpCall = ParamTransformationUtil.findWrapOperationOriginalCallArgs(methodNode, methodContext); + Multimap usedVars = HashMultimap.create(); + for (AbstractInsnNode insn : methodNode.instructions) { + if (insn instanceof VarInsnNode varInsn && !originalOpCall.contains(insn) && paramVars.contains(varInsn.var)) { + usedVars.put(varInsn.var, varInsn); + } + } + + List originalCallArgs = MethodCallAnalyzer.findMethodCallParamInsns(methodContext.findCleanInjectionTarget().methodNode(), minsn); + LocalVariableNode instanceLocal = mixinLocals.getByParameterOrdinal(0); + for (int paramVar : usedVars.keySet()) { + if (paramVar == instanceLocal.index) { + int cleanOrdinal = methodContext.cleanLocalsTable().getTypedOrdinal(methodContext.cleanLocalsTable().getByIndex(((VarInsnNode) originalCallArgs.getFirst()).var)).orElse(-1); + if (cleanOrdinal == -1) { + return Patch.Result.PASS; + } + LocalVariableNode dirtyLocal = methodContext.dirtyLocalsTable().getByTypedOrdinal(Type.getType(instanceLocal.desc), cleanOrdinal).orElse(null); + if (dirtyLocal == null) { + return Patch.Result.PASS; + } + TransformParameters.builder() + .transform(new InjectParameterTransform(argsTypes.length, Type.getType(dirtyLocal.desc), false)) + .build() + .apply(methodContext); + int newOrdinal = Type.getArgumentTypes(methodNode.desc).length - 1; + AnnotationVisitor visitor = methodNode.visitParameterAnnotation(newOrdinal, MixinConstants.LOCAL, false); + visitor.visit("ordinal", cleanOrdinal); + visitor.visitEnd(); + int newIndex = new LocalVariableLookup(methodNode).getByParameterOrdinal(newOrdinal).index; + usedVars.get(paramVar).forEach(varInsn -> varInsn.var = newIndex); + } else { + String loadedType = instanceOfCall.getPrevious() instanceof MethodInsnNode m ? Type.getReturnType(m.desc).getDescriptor() : null; + LocalVariableNode node = mixinLocals.getByIndex(paramVar); + if (loadedType == null || !loadedType.equals(node.desc)) { + return Patch.Result.PASS; + } + usedVars.get(paramVar).forEach(varInsn -> varInsn.var = instanceLocal.index); + } + } + + Patch.Result result = TransformParameters.builder() + .transform(new ReplaceParametersTransformer(0, Type.getObjectType("java/lang/Object"), false)) + .chain(b -> { + for (int i = 1; i < paramVars.size(); i++) { + b.transform(new RemoveParameterTransformer(i, false)); + } + }) + .build() + .apply(methodContext); + if (result == Patch.Result.PASS) { + return Patch.Result.PASS; + } + + AnnotationHandle ann = methodContext.methodAnnotation(); + ann.removeValues("at"); + AnnotationVisitor visitor = ann.unwrap().visitAnnotation("constant", MixinConstants.CONSTANT); + visitor.visit("classValue", Type.getObjectType(instanceOfCall.desc)); + visitor.visitEnd(); + + return Patch.Result.APPLY; + } + private static Patch.Result handleTargetModification(List> hunkLabels, MethodContext methodContext) { ClassNode dirtyTarget = methodContext.findDirtyInjectionTarget().classNode(); for (List insns : hunkLabels) { @@ -159,66 +220,7 @@ private static Patch.Result attemptExtractMixin(MethodInsnNode minsn, MethodCont return res; } // Extraction failed? Let's try something else - return createInjectionPoint(targetClass, minsn, methodContext); - } - - private static Patch.Result createInjectionPoint(ClassNode newTargetClass, MethodInsnNode minsn, MethodContext methodContext) { - Type selfType = Type.getObjectType(methodContext.findDirtyInjectionTarget().classNode().name); - Type[] params = Type.getArgumentTypes(minsn.desc); - int selfIndex = Stream.of(Stream.iterate(0, i -> i < params.length, i -> i + 1) - .filter(i -> params[i].equals(selfType)) - .toList()) - .filter(list -> list.size() == 1) - .map(List::getFirst) - .findFirst() - .orElse(-1); - if (selfIndex == -1) { - return Patch.Result.PASS; - } - - List callInsns = MethodCallAnalyzer.findMethodCallParamInsns(methodContext.findDirtyInjectionTarget().methodNode(), minsn); - if (callInsns == null || callInsns.size() <= selfIndex) { - return Patch.Result.PASS; - } - AbstractInsnNode selfParamInsn = callInsns.get(selfIndex); - if (selfParamInsn instanceof VarInsnNode varInsn && varInsn.getOpcode() == Opcodes.ALOAD && varInsn.var == 0) { - // Cool, out instance is passed into the method. Now let's inject there and call the old mixin method - ClassNode generatedTarget = methodContext.patchContext().environment().classGenerator().getOrGenerateMixinClass(methodContext.getMixinClass(), newTargetClass.name, null); - methodContext.patchContext().environment().refmapHolder().copyEntries(methodContext.getMixinClass().name, generatedTarget.name); - // Generate a method with the same injector annotation - MethodNode originalMixinMethod = methodContext.getMixinMethod(); - String name = originalMixinMethod.name + "$adapter$mirror$" + AdapterUtil.randomString(5); - List originalParams = List.of(Type.getArgumentTypes(originalMixinMethod.desc)); - List newParams = ImmutableList.builder().add(Type.getArgumentTypes(minsn.desc)).add(AdapterUtil.CI_TYPE).build(); - // Make sure we have all required params - if (!new HashSet<>(newParams).containsAll(originalParams)) { - return Patch.Result.PASS; - } - - String desc = Type.getMethodDescriptor(Type.VOID_TYPE, newParams.toArray(Type[]::new)); - // Change target - BundledMethodTransform.builder().modifyTarget(minsn.name + minsn.desc).apply(methodContext); - MethodNode invokerMixinMethod = (MethodNode) generatedTarget.visitMethod(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC, name, desc, null, null); - invokerMixinMethod.visibleAnnotations = new ArrayList<>(originalMixinMethod.visibleAnnotations); - // Make original mixin a unique public method - originalMixinMethod.access = OpcodeUtil.setAccessVisibility(originalMixinMethod.access, Opcodes.ACC_PUBLIC); - originalMixinMethod.visibleAnnotations.remove(methodContext.methodAnnotation().unwrap()); - originalMixinMethod.visitAnnotation(MixinConstants.UNIQUE, true); - // Now call the original mixin - GeneratorAdapter gen = new GeneratorAdapter(invokerMixinMethod, invokerMixinMethod.access, invokerMixinMethod.name, invokerMixinMethod.desc); - gen.newLabel(); - gen.loadArg(selfIndex); - for (Type type : originalParams) { - gen.loadArg(newParams.indexOf(type)); - } - gen.invokeVirtual(selfType, new Method(originalMixinMethod.name, originalMixinMethod.desc)); - gen.newLabel(); - gen.returnValue(); - gen.newLabel(); - gen.endMethod(); - return Patch.Result.APPLY; - } - return Patch.Result.PASS; + return new MirrorableExtractMixin(targetClass.name, minsn).apply(methodContext); } // TODO This should be an automatic upgrade tbh @@ -247,47 +249,4 @@ private static void adjustInjectorOrdinalForNewMethod(MethodInsnNode minsn, Meth } } } - - @Nullable - private static Pair, List> findPatchHunkRange(List cleanLabel, List> cleanLabels, Map, List> matchedLabels) { - // Find last matched dirty label BEFORE the injection point - List dirtyLabelBefore = Stream.iterate(cleanLabels.indexOf(cleanLabel), i -> i > 0, i -> i - 1) - .map(i -> matchedLabels.get(cleanLabels.get(i))) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - if (dirtyLabelBefore == null) { - return null; - } - - // Find first matched dirty label AFTER the injection point - List dirtyLabelAfter = Stream.iterate(cleanLabels.indexOf(cleanLabel), i -> i < cleanLabels.size(), i -> i + 1) - .map(i -> matchedLabels.get(cleanLabels.get(i))) - .filter(Objects::nonNull) - .findFirst() - .orElse(null); - if (dirtyLabelAfter == null) { - return null; - } - - return Pair.of(dirtyLabelBefore, dirtyLabelAfter); - } - - private static List> getLabelsInMethod(MethodNode methodNode) { - List> list = new ArrayList<>(); - List workingList = null; - for (AbstractInsnNode insn : methodNode.instructions) { - if (insn instanceof FrameNode) { - continue; - } - if (insn instanceof LabelNode) { - if (workingList != null) { - list.add(workingList); - } - workingList = new ArrayList<>(); - } - workingList.add(insn); - } - return list; - } } diff --git a/definition/src/main/java/org/sinytra/adapter/patch/transformer/dynfix/DynFixResolveAmbigousTarget.java b/definition/src/main/java/org/sinytra/adapter/patch/transformer/dynfix/DynFixResolveAmbigousTarget.java index 58256bf..d06f09c 100644 --- a/definition/src/main/java/org/sinytra/adapter/patch/transformer/dynfix/DynFixResolveAmbigousTarget.java +++ b/definition/src/main/java/org/sinytra/adapter/patch/transformer/dynfix/DynFixResolveAmbigousTarget.java @@ -3,12 +3,11 @@ import com.mojang.datafixers.util.Pair; import com.mojang.logging.LogUtils; import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; import org.sinytra.adapter.patch.api.MethodContext; import org.sinytra.adapter.patch.api.Patch; -import org.sinytra.adapter.patch.transformer.ModifyInjectionTarget; +import org.sinytra.adapter.patch.transformer.BundledMethodTransform; import org.slf4j.Logger; import java.util.List; @@ -22,13 +21,21 @@ public class DynFixResolveAmbigousTarget implements DynamicFixer { private static final Logger LOGGER = LogUtils.getLogger(); - public record Data(Pair> candidates) {} + public record Data(Pair> candidates) { + } @Nullable @Override public Data prepare(MethodContext methodContext) { - Pair> candidates = methodContext.findInjectionTargetCandidates(methodContext.patchContext().environment().dirtyClassLookup()); - if (candidates != null && candidates.getSecond().size() > 1) { + Pair> candidates = methodContext.findInjectionTargetCandidates(methodContext.patchContext().environment().dirtyClassLookup(), true); + if (candidates != null && !candidates.getSecond().isEmpty()) { + // Only apply single candidate change when the target desc has changed + if (candidates.getSecond().size() == 1) { + MethodContext.TargetPair cleanTarget = methodContext.findCleanInjectionTarget(); + if (cleanTarget == null || candidates.getSecond().getFirst().desc.equals(cleanTarget.methodNode().desc)) { + return null; + } + } return new Data(candidates); } return null; @@ -36,12 +43,12 @@ public Data prepare(MethodContext methodContext) { @Override public Patch.Result apply(ClassNode classNode, MethodNode methodNode, MethodContext methodContext, Data data) { - for (MethodNode target : data.candidates().getSecond()) { - List insns = methodContext.findInjectionTargetInsns(new MethodContext.TargetPair(data.candidates().getFirst(), target)); - if (!insns.isEmpty()) { + List candidates = data.candidates().getSecond(); + for (MethodNode target : candidates) { + if (candidates.size() == 1 || !methodContext.findInjectionTargetInsns(new MethodContext.TargetPair(data.candidates().getFirst(), target)).isEmpty()) { String newTarget = target.name + target.desc; LOGGER.debug(MIXINPATCH, "Resolving ambigous method selector of {}.{} to {}", classNode.name, methodNode.name, newTarget); - return new ModifyInjectionTarget(List.of(newTarget)).apply(methodContext); + return BundledMethodTransform.builder().modifyTarget(newTarget).apply(methodContext); } } return Patch.Result.PASS; diff --git a/definition/src/main/java/org/sinytra/adapter/patch/transformer/dynfix/DynamicInjectionPointPatch.java b/definition/src/main/java/org/sinytra/adapter/patch/transformer/dynfix/DynamicInjectionPointPatch.java index d1df25e..d5d8a8c 100644 --- a/definition/src/main/java/org/sinytra/adapter/patch/transformer/dynfix/DynamicInjectionPointPatch.java +++ b/definition/src/main/java/org/sinytra/adapter/patch/transformer/dynfix/DynamicInjectionPointPatch.java @@ -16,10 +16,12 @@ @SuppressWarnings({"rawtypes", "unchecked"}) public class DynamicInjectionPointPatch implements MethodTransform { private static final Logger LOGGER = LogUtils.getLogger(); + private static final List> PREPATCH = List.of( + new DynFixResolveAmbigousTarget() + ); private static final List> FIXES = List.of( new DynFixSliceBoundary(), new DynFixAtVariableAssignStore(), - new DynFixResolveAmbigousTarget(), new DynFixSplitMethod(), // Have this one always come last new DynFixMethodComparison(), @@ -32,15 +34,23 @@ public Patch.Result apply(ClassNode classNode, MethodNode methodNode, MethodCont // TODO Only show in tests LOGGER.debug(MIXINPATCH, "Considering method {}.{}", classNode.name, methodNode.name); + Patch.Result result = Patch.Result.PASS; + for (DynamicFixer fix : PREPATCH) { + Object data = fix.prepare(methodContext); + if (data != null) { + result = result.or(fix.apply(classNode, methodNode, methodContext, data)); + } + } for (DynamicFixer fix : FIXES) { Object data = fix.prepare(methodContext); if (data != null) { - Patch.Result result = fix.apply(classNode, methodNode, methodContext, data); - if (result != Patch.Result.PASS) { - return result; + Patch.Result patchResult = fix.apply(classNode, methodNode, methodContext, data); + if (patchResult != Patch.Result.PASS) { + return patchResult.or(result); } } } + return result; } return Patch.Result.PASS; } diff --git a/definition/src/main/java/org/sinytra/adapter/patch/transformer/param/ParamTransformationUtil.java b/definition/src/main/java/org/sinytra/adapter/patch/transformer/param/ParamTransformationUtil.java index 0b23363..54465b3 100644 --- a/definition/src/main/java/org/sinytra/adapter/patch/transformer/param/ParamTransformationUtil.java +++ b/definition/src/main/java/org/sinytra/adapter/patch/transformer/param/ParamTransformationUtil.java @@ -5,6 +5,7 @@ import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.*; +import org.sinytra.adapter.patch.analysis.MethodCallAnalyzer; import org.sinytra.adapter.patch.api.MethodContext; import org.sinytra.adapter.patch.api.MixinConstants; import org.sinytra.adapter.patch.selector.AnnotationHandle; @@ -49,6 +50,17 @@ public static List findWrapOperationOriginalCall(MethodNode me return List.of(); } + public static List findWrapOperationOriginalCallArgs(MethodNode methodNode, MethodContext methodContext) { + if (methodContext.methodAnnotation().matchesDesc(MixinConstants.WRAP_OPERATION)) { + for (AbstractInsnNode insn : methodNode.instructions) { + if (insn instanceof MethodInsnNode minsn && WO_ORIGINAL_CALL.matches(minsn)) { + return MethodCallAnalyzer.findFullMethodCallParamInsns(methodNode, minsn); + } + } + } + return List.of(); + } + @SuppressWarnings("DuplicatedCode") // The duplication is small public static void extractWrapOperation(final MethodContext methodContext, final MethodNode methodNode, final List params, final Consumer modification) { AnnotationHandle annotation = methodContext.methodAnnotation(); diff --git a/definition/src/main/java/org/sinytra/adapter/patch/transformer/param/RemoveParameterTransformer.java b/definition/src/main/java/org/sinytra/adapter/patch/transformer/param/RemoveParameterTransformer.java index 5e1d7b1..52a33ea 100644 --- a/definition/src/main/java/org/sinytra/adapter/patch/transformer/param/RemoveParameterTransformer.java +++ b/definition/src/main/java/org/sinytra/adapter/patch/transformer/param/RemoveParameterTransformer.java @@ -2,6 +2,7 @@ import com.mojang.serialization.Codec; import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.LocalVariableNode; import org.objectweb.asm.tree.MethodNode; @@ -15,18 +16,24 @@ import static org.sinytra.adapter.patch.transformer.param.ParamTransformationUtil.extractWrapOperation; -public record RemoveParameterTransformer(int index) implements ParameterTransformer { +public record RemoveParameterTransformer(int index, boolean upgradeWrapOperation) implements ParameterTransformer { public static final Codec CODEC = Codec.intRange(0, 255) .fieldOf("index").xmap(RemoveParameterTransformer::new, RemoveParameterTransformer::index) .codec(); + public RemoveParameterTransformer(int index) { + this(index, true); + } + @Override public Patch.Result apply(ClassNode classNode, MethodNode methodNode, MethodContext methodContext, PatchContext context, List parameters, int offset) { final int target = this.index() + offset; final int lvtIndex = ParamTransformationUtil.calculateLVTIndex(parameters, !methodContext.isStatic(), target); // Remove the use of the param in a wrapop first to avoid the new LVT messing with the outcome of that - extractWrapOperation(methodContext, methodNode, parameters, op -> op.removeParameter(target)); + if (this.upgradeWrapOperation) { + extractWrapOperation(methodContext, methodNode, parameters, op -> op.removeParameter(target)); + } LVTSnapshot.with(methodNode, () -> { LocalVariableNode lvn = methodNode.localVariables.stream() @@ -40,6 +47,8 @@ public Patch.Result apply(ClassNode classNode, MethodNode methodNode, MethodCont }); methodNode.parameters.remove(target); + methodNode.visibleParameterAnnotations = AdapterUtil.removeArrayElement(methodNode.visibleParameterAnnotations, this.index, List[]::new); + methodNode.invisibleParameterAnnotations = AdapterUtil.removeArrayElement(methodNode.invisibleParameterAnnotations, this.index, List[]::new); parameters.remove(target); return Patch.Result.COMPUTE_FRAMES; diff --git a/definition/src/main/java/org/sinytra/adapter/patch/transformer/param/ReplaceParametersTransformer.java b/definition/src/main/java/org/sinytra/adapter/patch/transformer/param/ReplaceParametersTransformer.java index 3c9b03a..980cbc6 100644 --- a/definition/src/main/java/org/sinytra/adapter/patch/transformer/param/ReplaceParametersTransformer.java +++ b/definition/src/main/java/org/sinytra/adapter/patch/transformer/param/ReplaceParametersTransformer.java @@ -20,7 +20,7 @@ import static org.sinytra.adapter.patch.PatchInstance.MIXINPATCH; import static org.sinytra.adapter.patch.transformer.param.ParamTransformationUtil.findWrapOperationOriginalCall; -public record ReplaceParametersTransformer(int index, Type type) implements ParameterTransformer { +public record ReplaceParametersTransformer(int index, Type type, boolean upgradeUsage) implements ParameterTransformer { static final Codec CODEC = RecordCodecBuilder.create(in -> in.group( Codec.intRange(0, 255).fieldOf("index").forGetter(ReplaceParametersTransformer::index), AdapterUtil.TYPE_CODEC.fieldOf("type").forGetter(ReplaceParametersTransformer::type) @@ -28,6 +28,10 @@ public record ReplaceParametersTransformer(int index, Type type) implements Para private static final Logger LOGGER = LogUtils.getLogger(); + public ReplaceParametersTransformer(int index, Type type) { + this(index, type, true); + } + @Override public Patch.Result apply(ClassNode classNode, MethodNode methodNode, MethodContext methodContext, PatchContext context, List parameters, int offset) { final int paramIndex = this.index + offset; @@ -47,7 +51,7 @@ public Patch.Result apply(ClassNode classNode, MethodNode methodNode, MethodCont List ignoreInsns = findWrapOperationOriginalCall(methodNode, methodContext); BytecodeFixerUpper bfu = context.environment().bytecodeFixerUpper(); - if (this.type.getSort() == Type.OBJECT && originalType.getSort() == Type.OBJECT) { + if (this.upgradeUsage && this.type.getSort() == Type.OBJECT && originalType.getSort() == Type.OBJECT) { // Replace variable usages with the new type for (AbstractInsnNode insn : methodNode.instructions) { if (ignoreInsns.contains(insn)) { diff --git a/definition/src/main/java/org/sinytra/adapter/patch/util/AdapterUtil.java b/definition/src/main/java/org/sinytra/adapter/patch/util/AdapterUtil.java index 085599b..7f035f8 100644 --- a/definition/src/main/java/org/sinytra/adapter/patch/util/AdapterUtil.java +++ b/definition/src/main/java/org/sinytra/adapter/patch/util/AdapterUtil.java @@ -23,10 +23,7 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.util.*; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.function.UnaryOperator; +import java.util.function.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -37,6 +34,7 @@ public final class AdapterUtil { private static final Pattern FIELD_REF_PATTERN = Pattern.compile("^(?L.+?;)?(?[^:]+)?:(?.+)?$"); public static final Type CI_TYPE = Type.getObjectType("org/spongepowered/asm/mixin/injection/callback/CallbackInfo"); public static final Type CIR_TYPE = Type.getObjectType("org/spongepowered/asm/mixin/injection/callback/CallbackInfoReturnable"); + public static final Type OPERATION_TYPE = Type.getObjectType(MixinConstants.OPERATION_INTERNAL_NAME); private static final Logger LOGGER = LogUtils.getLogger(); public static int getLVTOffsetForType(Type type) { @@ -242,6 +240,15 @@ public static AbstractInsnNode iterateInsns(AbstractInsnNode insn, UnaryOperator return null; } + public static T[] removeArrayElement(T[] arr, int index, IntFunction arrayGen) { + if (arr == null) { + return null; + } + List list = new ArrayList<>(Arrays.asList(arr)); + list.remove(index); + return list.toArray(arrayGen); + } + public record CapturedLocals(MethodContext.TargetPair target, boolean isStatic, int paramLocalStart, int paramLocalEnd, int lvtOffset, List expected, LocalVariableLookup lvt) { } diff --git a/test/src/test/java/org/sinytra/adapter/patch/test/mixin/DynamicMixinPatchTest.java b/test/src/test/java/org/sinytra/adapter/patch/test/mixin/DynamicMixinPatchTest.java index f7da52a..c862770 100644 --- a/test/src/test/java/org/sinytra/adapter/patch/test/mixin/DynamicMixinPatchTest.java +++ b/test/src/test/java/org/sinytra/adapter/patch/test/mixin/DynamicMixinPatchTest.java @@ -174,6 +174,16 @@ void testCompareModifiedMethod3() throws Exception { ); } + @Test + void testModifiedToInstanceOfCall() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mixin/StemBlockMixin", + "isOnFarmland", + assertTargetMethod(), + assertTargetsConstant() + ); + } + @Override protected LoadResult load(String className, List allowedMethods) throws Exception { final ClassNode patched = loadClass(className); diff --git a/test/src/test/java/org/sinytra/adapter/patch/test/mixin/MinecraftMixinPatchTest.java b/test/src/test/java/org/sinytra/adapter/patch/test/mixin/MinecraftMixinPatchTest.java index e4a7a62..13cdfa8 100644 --- a/test/src/test/java/org/sinytra/adapter/patch/test/mixin/MinecraftMixinPatchTest.java +++ b/test/src/test/java/org/sinytra/adapter/patch/test/mixin/MinecraftMixinPatchTest.java @@ -31,6 +31,7 @@ import java.util.zip.ZipFile; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; public abstract class MinecraftMixinPatchTest { private static final Logger LOGGER = LogUtils.getLogger(); @@ -221,4 +222,18 @@ protected AssertCallback assertSliceRange() { .isEqualTo(sliceExtractor.apply("to", expectedMethodAnn)); }; } + + protected AssertCallback assertTargetsConstant() { + return (patched, expected, env) -> { + AnnotationHandle patchedMethodAnn = new AnnotationHandle(patched.visibleAnnotations.getFirst()); + AnnotationHandle expectedMethodAnn = new AnnotationHandle(expected.visibleAnnotations.getFirst()); + + assertTrue(patchedMethodAnn.getNested("at").isEmpty()); + assertTrue(patchedMethodAnn.getNested("constant").isPresent()); + + Assertions.assertThat(patchedMethodAnn.getNested("constant").get().unwrap().values) + .as("Values") + .isEqualTo(expectedMethodAnn.getNested("constant").get().unwrap().values); + }; + } } diff --git a/test/src/testClasses/java/org/sinytra/adapter/test/mixin/StemBlockMixin.java b/test/src/testClasses/java/org/sinytra/adapter/test/mixin/StemBlockMixin.java new file mode 100644 index 0000000..0e84914 --- /dev/null +++ b/test/src/testClasses/java/org/sinytra/adapter/test/mixin/StemBlockMixin.java @@ -0,0 +1,35 @@ +package org.sinytra.adapter.test.mixin; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.FarmBlock; +import net.minecraft.world.level.block.StemBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Constant; + +@Mixin(StemBlock.class) +public class StemBlockMixin { + @WrapOperation( + method = "randomTick(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;Lnet/minecraft/util/RandomSource;)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/level/block/state/BlockState;is(Lnet/minecraft/world/level/block/Block;)Z" + ) + ) + private static boolean isOnFarmland(BlockState instance, Block block, Operation original) { + return Blocks.FARMLAND.equals(block) || instance.isAir() || original.call(instance, block); + } + + @WrapOperation( + method = "randomTick(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/server/level/ServerLevel;Lnet/minecraft/core/BlockPos;Lnet/minecraft/util/RandomSource;)V", + constant = @Constant(classValue = FarmBlock.class) + ) + private static boolean isOnFarmlandExpected(Object instance, Operation original, @Local(ordinal = 1) BlockState adapter_injected_3) { + return Blocks.FARMLAND.equals(instance) || adapter_injected_3.isAir() || original.call(instance); + } +}