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 60ce33b..8de817e 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,25 @@ public static List> getInvocationInsns(MethodNode methodN }); } + public static int getMethodCallOrdinal(MethodNode method, MethodInsnNode insn) { + List insns = new ArrayList<>(); + for (AbstractInsnNode i : method.instructions) { + if (i instanceof MethodInsnNode m && InsnComparator.instructionsEqual(m, insn)) { + insns.add(m); + } + } + return insns.indexOf(insn); + } + + public static int getArgIndex(String desc, Type type) { + List args = Arrays.asList(Type.getArgumentTypes(desc)); + List found = args.stream().filter(type::equals).toList(); + if (found.size() != 1) { + return -1; + } + return args.indexOf(found.getFirst()); + } + @Nullable public static List findMethodCallParamInsns(MethodNode methodNode, MethodInsnNode insn) { MethodCallInterpreter interpreter = MethodCallAnalyzer.analyzeInterpretMethod(methodNode, new MethodCallInterpreter(insn)); diff --git a/definition/src/main/java/org/sinytra/adapter/patch/analysis/selector/AnnotationHandle.java b/definition/src/main/java/org/sinytra/adapter/patch/analysis/selector/AnnotationHandle.java index 9701102..44748ce 100644 --- a/definition/src/main/java/org/sinytra/adapter/patch/analysis/selector/AnnotationHandle.java +++ b/definition/src/main/java/org/sinytra/adapter/patch/analysis/selector/AnnotationHandle.java @@ -82,4 +82,11 @@ public void refresh(AnnotationNode annotationNode) { this.annotationNode = annotationNode; this.handleCache.values().forEach(v -> v.refresh(annotationNode)); } + + public void setOrAppend(String key, Object value) { + getValue(key).ifPresentOrElse( + v -> v.set(value), + () -> appendValue(key, value) + ); + } } 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 7156d42..edcdef0 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 @@ -4,12 +4,14 @@ import com.google.common.collect.Multimap; import org.jetbrains.annotations.Nullable; import org.objectweb.asm.AnnotationVisitor; +import org.objectweb.asm.Handle; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.*; -import org.sinytra.adapter.patch.analysis.locals.LocalVariableLookup; +import org.sinytra.adapter.patch.analysis.InsnComparator; import org.sinytra.adapter.patch.analysis.MethodCallAnalyzer; import org.sinytra.adapter.patch.analysis.MethodLabelComparator; +import org.sinytra.adapter.patch.analysis.locals.LocalVariableLookup; import org.sinytra.adapter.patch.analysis.selector.AnnotationHandle; import org.sinytra.adapter.patch.api.MethodContext; import org.sinytra.adapter.patch.api.MixinConstants; @@ -22,13 +24,11 @@ import org.sinytra.adapter.patch.transformer.pipeline.MethodTransformationPipeline; import org.sinytra.adapter.patch.util.AdapterUtil; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; +import java.util.*; +import java.util.stream.Stream; public class DynFixMethodComparison implements DynamicFixer { - private static final Set ACCEPTED_ANNOTATIONS = Set.of(MixinConstants.INJECT, MixinConstants.WRAP_OPERATION); + private static final Set ACCEPTED_ANNOTATIONS = Set.of(MixinConstants.INJECT, MixinConstants.WRAP_OPERATION, MixinConstants.MODIFY_ARG); public record Data(AbstractInsnNode cleanInjectionInsn) {} @@ -58,6 +58,10 @@ public FixResult apply(ClassNode classNode, MethodNode methodNode, MethodContext } List> hunkLabels = comparisonResult.patchedLabels(); + if (methodContext.methodAnnotation().matchesDesc(MixinConstants.MODIFY_ARG)) { + return handleModifyArgInjectionPoint(cleanInjectionInsn, hunkLabels, methodContext); + } + if (methodContext.methodAnnotation().matchesDesc(MixinConstants.WRAP_OPERATION)) { return handleWrapOperationToInstanceOf(cleanInjectionInsn, comparisonResult.cleanLabel(), hunkLabels, methodContext) .or(() -> handleWrapOpertationNewInjectionPoint(cleanInjectionInsn, comparisonResult.cleanLabel(), hunkLabels, methodContext)) @@ -90,6 +94,93 @@ public FixResult apply(ClassNode classNode, MethodNode methodNode, MethodContext return null; } + // Handle ModifyArg when targetting INDY values + private static FixResult handleModifyArgInjectionPoint(AbstractInsnNode cleanInjectionInsn, List> hunkLabels, MethodContext methodContext) { + if (!(cleanInjectionInsn instanceof MethodInsnNode minsn)) { + return null; + } + + Type desiredType = Type.getArgumentTypes(methodContext.getMixinMethod().desc)[0]; + + int index = MethodCallAnalyzer.getArgIndex(minsn.desc, desiredType) + 1; + if (index == 0) { + return null; + } + List cleanCallParamInsns = MethodCallAnalyzer.findMethodCallParamInsns(methodContext.findCleanInjectionTarget().methodNode(), minsn); + if (cleanCallParamInsns.size() <= index) { + return null; + } + + AbstractInsnNode targetArgInsn = cleanCallParamInsns.get(index); + if (!(targetArgInsn instanceof InvokeDynamicInsnNode cleanIndy)) { + return null; + } + + MethodContext.TargetPair cleanTarget = methodContext.findCleanInjectionTarget(); + MethodContext.TargetPair dirtyTarget = methodContext.findDirtyInjectionTarget(); + // Handle cases where the method has been split off + if (cleanIndy.bsmArgs.length > 2 && cleanIndy.bsmArgs[1] instanceof Handle handle && handle.getOwner().equals(dirtyTarget.classNode().name)) { + MethodNode cleanMethod = MethodCallAnalyzer.findMethodByUniqueName(cleanTarget.classNode(), handle.getName()).orElse(null); + if (cleanMethod == null) { + return null; + } + MethodNode dirtyMethod = MethodCallAnalyzer.findMethodByUniqueName(dirtyTarget.classNode(), handle.getName()).orElse(null); + if (dirtyMethod == null) { + return null; + } + if (DynFixSplitMethod.isDirtyDeprecatedMethod(cleanMethod, dirtyMethod)) { + List invocations = DynFixSplitMethod.collectMethodInvocations(dirtyTarget.classNode(), dirtyMethod); + if (invocations != null) { + MethodNode last = invocations.getLast(); + if (last.desc.equals(dirtyMethod.desc)) { + InvokeDynamicInsnNode clone = (InvokeDynamicInsnNode) cleanIndy.clone(Map.of()); + clone.bsmArgs = Stream.of(clone.bsmArgs).toArray(); + clone.bsmArgs[1] = new Handle(handle.getTag(), handle.getOwner(), last.name, handle.getDesc(), handle.isInterface()); + cleanIndy = clone; + } + } + } + } + + MethodNode dirtyMethod = methodContext.findDirtyInjectionTarget().methodNode(); + List matches = new ArrayList<>(); + for (List label : hunkLabels) { + for (AbstractInsnNode insn : label) { + if (insn instanceof MethodInsnNode m && Stream.of(Type.getArgumentTypes(m.desc)).filter(desiredType::equals).count() == 1) { + int argIndex = MethodCallAnalyzer.getArgIndex(m.desc, desiredType) + 1; + if (argIndex == 0) { + continue; + } + List list = MethodCallAnalyzer.findMethodCallParamInsns(dirtyMethod, m); + if (list.size() > argIndex && list.get(argIndex) instanceof InvokeDynamicInsnNode dirtyIndy && InsnComparator.instructionsEqual(cleanIndy, dirtyIndy)) { + matches.add(m); + } + } + } + } + + if (matches.size() == 1) { + MethodInsnNode m = matches.getFirst(); + String newInjectionPoint = Type.getObjectType(m.owner).getDescriptor() + m.name + m.desc; + + Patch.Result result = MethodTransformationPipeline.builder(new ModifyInjectionPoint("INVOKE", newInjectionPoint, true, false)) + .onSuccess(() -> (cls, mtd, mtx, ctx) -> { + int ordinal = MethodCallAnalyzer.getMethodCallOrdinal(dirtyMethod, m); + if (ordinal == -1) { + throw new IllegalStateException("Ordinal not found?"); + } + AnnotationHandle handle = mtx.injectionPointAnnotationOrThrow(); + handle.setOrAppend("ordinal", ordinal); + return Patch.Result.APPLY; + }) + .apply(methodContext); + + return FixResult.of(result, PatchAuditTrail.Match.FULL); + } + + return null; + } + private static Optional handleWrapOpertationNewInjectionPoint(AbstractInsnNode cleanInjectionInsn, List cleanLabel, List> hunkLabels, MethodContext methodContext) { if (!(cleanInjectionInsn instanceof MethodInsnNode minsn) || hunkLabels.size() != 1) { return Optional.empty(); diff --git a/definition/src/main/java/org/sinytra/adapter/patch/transformer/dynfix/DynFixSplitMethod.java b/definition/src/main/java/org/sinytra/adapter/patch/transformer/dynfix/DynFixSplitMethod.java index 13113f0..148e6ba 100644 --- a/definition/src/main/java/org/sinytra/adapter/patch/transformer/dynfix/DynFixSplitMethod.java +++ b/definition/src/main/java/org/sinytra/adapter/patch/transformer/dynfix/DynFixSplitMethod.java @@ -50,33 +50,46 @@ public FixResult apply(ClassNode classNode, MethodNode methodNode, MethodContext return null; } + + public static boolean isDirtyDeprecatedMethod(MethodNode clean, MethodNode dirty) { + return !AdapterUtil.hasAnnotation(clean.visibleAnnotations, DEPRECATED) && AdapterUtil.hasAnnotation(dirty.visibleAnnotations, DEPRECATED); + } - private static List locateCandidates(MethodContext methodContext) { - MethodNode cleanTargetMethod = methodContext.findCleanInjectionTarget().methodNode(); - ClassNode dirtyTargetClass = methodContext.findDirtyInjectionTarget().classNode(); - MethodNode dirtyTargetMethod = methodContext.findDirtyInjectionTarget().methodNode(); - - // Check that a Deprecated annotation was added to the dirty method - if (AdapterUtil.hasAnnotation(cleanTargetMethod.visibleAnnotations, DEPRECATED) || !AdapterUtil.hasAnnotation(dirtyTargetMethod.visibleAnnotations, DEPRECATED)) { - return tryFindPartialCandidates(cleanTargetMethod, dirtyTargetClass, dirtyTargetMethod, methodContext); - } - + @Nullable + public static List collectMethodInvocations(ClassNode cls, MethodNode mtd) { // Iterate over isns, leave out first and last elements // Collect method invocations // All labels must be finalized by a method invocation to pass List invocations = new ArrayList<>(); - for (int i = 1; i < dirtyTargetMethod.instructions.size() - 1; i++) { - AbstractInsnNode insn = dirtyTargetMethod.instructions.get(i); + for (int i = 1; i < mtd.instructions.size() - 1; i++) { + AbstractInsnNode insn = mtd.instructions.get(i); if (insn instanceof LabelNode) { AbstractInsnNode previous = insn.getPrevious(); - if (previous instanceof MethodInsnNode methodInsn && methodInsn.owner.equals(dirtyTargetClass.name)) { - MethodNode method = dirtyTargetClass.methods.stream().filter(m -> m.name.equals(methodInsn.name) && m.desc.equals(methodInsn.desc)).findFirst().orElseThrow(); + if (previous instanceof MethodInsnNode methodInsn && methodInsn.owner.equals(cls.name)) { + MethodNode method = cls.methods.stream().filter(m -> m.name.equals(methodInsn.name) && m.desc.equals(methodInsn.desc)).findFirst().orElseThrow(); invocations.add(method); } else if (previous == null || !OpcodeUtil.isReturnOpcode(previous.getOpcode())) { return null; } } } + return invocations; + } + + private static List locateCandidates(MethodContext methodContext) { + MethodNode cleanTargetMethod = methodContext.findCleanInjectionTarget().methodNode(); + ClassNode dirtyTargetClass = methodContext.findDirtyInjectionTarget().classNode(); + MethodNode dirtyTargetMethod = methodContext.findDirtyInjectionTarget().methodNode(); + + // Check that a Deprecated annotation was added to the dirty method + if (!isDirtyDeprecatedMethod(cleanTargetMethod, dirtyTargetMethod)) { + return tryFindPartialCandidates(cleanTargetMethod, dirtyTargetClass, dirtyTargetMethod, methodContext); + } + + List invocations = collectMethodInvocations(dirtyTargetClass, dirtyTargetMethod); + if (invocations == null) { + return null; + } List candidates = findInsnsCalls(invocations, methodContext); 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 08e74a3..2acd835 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 @@ -184,6 +184,12 @@ void testSplitMethodInjectionTarget() throws Exception { assertTargetMethod(), assertInjectionPoint() ); + assertSameCode( + "org/sinytra/adapter/test/mixin/GuiMixin", + "afterMainHud", + assertTargetMethod(), + assertInjectionPoint() + ); } @Test diff --git a/test/src/testClasses/java/org/sinytra/adapter/test/mixin/GuiMixin.java b/test/src/testClasses/java/org/sinytra/adapter/test/mixin/GuiMixin.java index 40949a6..42bdae8 100644 --- a/test/src/testClasses/java/org/sinytra/adapter/test/mixin/GuiMixin.java +++ b/test/src/testClasses/java/org/sinytra/adapter/test/mixin/GuiMixin.java @@ -5,6 +5,7 @@ import net.minecraft.client.DeltaTracker; import net.minecraft.client.gui.Gui; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.LayeredDraw; import net.minecraft.client.resources.MobEffectTextureManager; import net.minecraft.core.Holder; import net.minecraft.world.effect.MobEffectInstance; @@ -98,4 +99,35 @@ private int moveHealthDown(int original) { private int moveHealthDownExpected(int original) { return original; } + + // https://github.com/SkyblockerMod/Skyblocker/blob/8cfd59bbf6c71d1de6ed35ad36b2abd801eddb71/src/main/java/de/hysky/skyblocker/mixins/InGameHudMixin.java#L97 + @ModifyArg( + method = "(Lnet/minecraft/client/Minecraft;)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/LayeredDraw;add(Lnet/minecraft/client/gui/LayeredDraw$Layer;)Lnet/minecraft/client/gui/LayeredDraw;", + ordinal = 2 + ) + ) + private LayeredDraw.Layer afterMainHud(LayeredDraw.Layer mainHudLayer) { + return (context, tickCounter) -> { + mainHudLayer.render(context, tickCounter); + System.out.println("Hello"); + }; + } + + @ModifyArg( + method = "(Lnet/minecraft/client/Minecraft;)V", + at = @At( + value = "INVOKE", + target = "Lnet/neoforged/neoforge/client/gui/GuiLayerManager;add(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/gui/LayeredDraw$Layer;)Lnet/neoforged/neoforge/client/gui/GuiLayerManager;", + ordinal = 11 + ) + ) + private LayeredDraw.Layer afterMainHudExpected(LayeredDraw.Layer mainHudLayer) { + return (context, tickCounter) -> { + mainHudLayer.render(context, tickCounter); + System.out.println("Hello"); + }; + } }