diff --git a/core/src/main/java/org/sinytra/adapter/env/ann/ConstantData.java b/core/src/main/java/org/sinytra/adapter/env/ann/ConstantData.java index 753f8505..3cc1fe64 100644 --- a/core/src/main/java/org/sinytra/adapter/env/ann/ConstantData.java +++ b/core/src/main/java/org/sinytra/adapter/env/ann/ConstantData.java @@ -13,7 +13,7 @@ public class ConstantData { private static final PropertyContainerTemplate TEMPLATE = PropertyContainerTemplate.builder() - .requireOne(Keys.DOUBLE_VALUE, Keys.CLASS_VALUE) + .requireOne(Keys.INT_VALUE, Keys.DOUBLE_VALUE, Keys.CLASS_VALUE) .build(); private final PropertyContainer properties; @@ -47,6 +47,7 @@ public static Optional parse(AnnotationHandle handle) { } public static class Keys { + public static final PropertyKey INT_VALUE = PropertyKey.create("intValue", Integer.class); public static final PropertyKey DOUBLE_VALUE = PropertyKey.create("doubleValue", Double.class); public static final PropertyKey CLASS_VALUE = PropertyKey.create("classValue", Type.class); } diff --git a/core/src/main/java/org/sinytra/adapter/patch/mixin/MixinTypes.java b/core/src/main/java/org/sinytra/adapter/patch/mixin/MixinTypes.java index 840f9fa6..5f688b2b 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/mixin/MixinTypes.java +++ b/core/src/main/java/org/sinytra/adapter/patch/mixin/MixinTypes.java @@ -4,6 +4,7 @@ import org.sinytra.adapter.env.util.MixinAnnotations; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.ModifyArg; +import org.spongepowered.asm.mixin.injection.ModifyConstant; import org.spongepowered.asm.mixin.injection.ModifyVariable; import org.spongepowered.asm.mixin.injection.Redirect; @@ -14,12 +15,13 @@ public class MixinTypes { private static final Map MIXIN_TYPES = new HashMap<>(); // TODO: - // MixinConstants.MODIFY_ARGS, MixinConstants.MODIFY_CONST, MixinConstants.WRAP_WITH_CONDITION, + // MixinConstants.MODIFY_ARGS, MixinConstants.WRAP_WITH_CONDITION, // MixinConstants.OVERWRITE (dont forget special modifyTarget handling) // MixinConstants.ACCESSOR public static final MixinType INJECT = new InjectMixin(); public static final MixinType MODIFY_VAR = new ModifyVariableMixin(); public static final MixinType MODIFY_ARG = new ModifyArgMixin(); + public static final MixinType MODIFY_CONST = new ModifyConstantMixin(); public static final MixinType MODIFY_RET = new ModifyReturnValueMixin(); public static final MixinType REDIRECT = new RedirectMixin(); public static final MixinType WRAP_OP = new WrapOperationMixin(); @@ -29,6 +31,7 @@ public class MixinTypes { registerMixinType(Inject.class, INJECT); registerMixinType(ModifyVariable.class, MODIFY_VAR); registerMixinType(ModifyArg.class, MODIFY_ARG); + registerMixinType(ModifyConstant.class, MODIFY_CONST); registerMixinType(MixinAnnotations.MODIFY_RETURN_VAL_INTERNAL_NAME, MODIFY_RET); registerMixinType(Redirect.class, REDIRECT); registerMixinType(MixinAnnotations.WRAP_OPERATION_INTERNAL_NAME, WRAP_OP); diff --git a/core/src/main/java/org/sinytra/adapter/patch/mixin/ModifyConstantMixin.java b/core/src/main/java/org/sinytra/adapter/patch/mixin/ModifyConstantMixin.java new file mode 100644 index 00000000..36ffd727 --- /dev/null +++ b/core/src/main/java/org/sinytra/adapter/patch/mixin/ModifyConstantMixin.java @@ -0,0 +1,55 @@ +package org.sinytra.adapter.patch.mixin; + +import org.sinytra.adapter.env.ctx.MixinContext; +import org.sinytra.adapter.env.param.MethodParameters; +import org.sinytra.adapter.patch.Recipe; +import org.sinytra.adapter.patch.TxResult; +import org.sinytra.adapter.patch.config.Configuration; +import org.sinytra.adapter.patch.config.ConfigurationTemplates; +import org.sinytra.adapter.patch.config.MutableConfiguration; +import org.sinytra.adapter.patch.config.PropertyContainerTemplate; +import org.sinytra.adapter.patch.config.key.MixinKeys; +import org.sinytra.adapter.patch.processor.Processors; +import org.sinytra.adapter.patch.resolver.Resolvers; + +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import static org.sinytra.adapter.env.param.MethodParameters.ParamGroup.CAPTURED_PARAMS; +import static org.sinytra.adapter.env.param.MethodParameters.ParamGroup.SINGLE_ANY; + +public class ModifyConstantMixin implements MixinType { + private static final PropertyContainerTemplate TEMPLATE = ConfigurationTemplates.MIXIN_BASE.extend() + .require(MixinKeys.TARGET_CONSTANT) + .pluralKeys(MixinKeys.TARGET_METHOD, MixinKeys.TARGET_CONSTANT) + .build(); + + @Override + public PropertyContainerTemplate getConfigurationTemplate() { + return TEMPLATE; + } + + @Override + public Set getFlags() { + return EnumSet.of(MixinFlag.RETURN_TYPE_SENSITIVE); + } + + @Override + public TxResult preProcess(MixinContext context, MutableConfiguration clean, Resolvers resolvers, Processors processors) { + clean.setParameters(MethodParameters.create(context.methodNode(), List.of(SINGLE_ANY, CAPTURED_PARAMS))); + return TxResult.SUCCESS; + } + + @Override + public TxResult postProcess(MixinContext context, Configuration clean, MutableConfiguration dirty, Recipe recipe) { + if (dirty.getTargetMethod() == null) { + return TxResult.FAIL; + } + + dirty.inheritProperyIfAbsent(MixinKeys.TARGET_CONSTANT); + dirty.inheritParameters(); + dirty.inheritReturnType(); + return TxResult.SUCCESS; + } +} diff --git a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/InjectionPointResolver.java b/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/InjectionPointResolver.java index d20851f4..df945aae 100644 --- a/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/InjectionPointResolver.java +++ b/core/src/main/java/org/sinytra/adapter/patch/resolver/injection/InjectionPointResolver.java @@ -6,6 +6,7 @@ import org.sinytra.adapter.env.ctx.MixinContext; import org.sinytra.adapter.patch.Recipe; import org.sinytra.adapter.patch.config.Configuration; +import org.sinytra.adapter.patch.config.key.MixinKeys; import org.sinytra.adapter.patch.resolver.CompoundResolver; import org.sinytra.adapter.env.ctx.TargetPair; import org.sinytra.adapter.util.MethodQualifier; @@ -24,7 +25,8 @@ public InjectionPointResolver() { @Override protected boolean canApply(Recipe recipe) { - return recipe.dirty().getAtData() == null; + return recipe.dirty().getAtData() == null + && !recipe.clean().hasProperty(MixinKeys.TARGET_CONSTANT); } @Nullable diff --git a/core/src/main/java/org/sinytra/adapter/transform/PipelineMethodTransformer.java b/core/src/main/java/org/sinytra/adapter/transform/PipelineMethodTransformer.java index 769dbae6..47926e8d 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/PipelineMethodTransformer.java +++ b/core/src/main/java/org/sinytra/adapter/transform/PipelineMethodTransformer.java @@ -46,11 +46,12 @@ public PipelineMethodTransformer(List methodPatches, boolean patche @Override public PatchResult apply(MixinContext context, Configuration config) { + boolean hasManualPatch = this.patchResolver.matches(config); TargetPair cleanTarget = context.methods().findOwnMethodPair(context.cleanLookup(), config.getTargetMethod()); - if (cleanTarget == null) return PatchResult.PASS; + if (cleanTarget == null && !hasManualPatch) return PatchResult.PASS; TargetPair dirtyTarget = context.methods().findOwnMethodPair(context.dirtyLookup(), config.getTargetMethod()); - if (!this.patchResolver.matches(config) && !failsDirtyInjectionCheck(context, config, dirtyTarget) && hasValidSlice(context, config, dirtyTarget)) + if (!hasManualPatch && !failsDirtyInjectionCheck(context, config, dirtyTarget) && hasValidSlice(context, config, dirtyTarget)) return PatchResult.PASS; LOGGER.debug(MIXINPATCH, "Considering method {}", context.getMixinId()); diff --git a/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchBuilder.java b/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchBuilder.java index 8be5d2e2..ae5142b3 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchBuilder.java +++ b/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchBuilder.java @@ -30,6 +30,10 @@ public interface MethodPatchBuilder { MethodPatchBuilder modifyTarget(String method); + MethodPatchBuilder modifyOrdinal(int ordinal); + + MethodPatchBuilder modifyInjectionPointOrdinal(int ordinal); + MethodPatchBuilder modifyInjectionPoint(String target); MethodPatchBuilder modifyInjectionPoint(String value, String target); diff --git a/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchBuilderImpl.java b/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchBuilderImpl.java index 1e5dd727..30843b67 100644 --- a/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchBuilderImpl.java +++ b/core/src/main/java/org/sinytra/adapter/transform/patch/MethodPatchBuilderImpl.java @@ -109,6 +109,21 @@ public MethodPatchBuilder modifyTarget(String method) { return this; } + @Override + public MethodPatchBuilder modifyOrdinal(int ordinal) { + this.config.setProperty(MixinKeys.ORDINAL, ordinal); + return this; + } + + @Override + public MethodPatchBuilder modifyInjectionPointOrdinal(int ordinal) { + this.configCompleter = this.configCompleter.andThen((clean, dirty) -> { + AtData at = dirty.getAtData() == null ? clean.getAtData() : dirty.getAtData(); + dirty.setAtData(at.withOrdinal(ordinal)); + }); + return this; + } + @Override public MethodPatchBuilder modifyInjectionPoint(String target) { // AtData requires value which must be copied at transform time diff --git a/test/mc-26.1.2/src/test/java/org/sinytra/adapter/patch/test/mc_26_1_2/mixin/DynamicMixinPatchTest.java b/test/mc-26.1.2/src/test/java/org/sinytra/adapter/patch/test/mc_26_1_2/mixin/DynamicMixinPatchTest.java index 8d4cf1aa..0fe4f17c 100644 --- a/test/mc-26.1.2/src/test/java/org/sinytra/adapter/patch/test/mc_26_1_2/mixin/DynamicMixinPatchTest.java +++ b/test/mc-26.1.2/src/test/java/org/sinytra/adapter/patch/test/mc_26_1_2/mixin/DynamicMixinPatchTest.java @@ -10,6 +10,7 @@ import org.sinytra.adapter.patch.DynamicPatches; import org.sinytra.adapter.patch.Patcher; import org.sinytra.adapter.patch.test_main.mixin.MinecraftMixinPatchTest; +import org.sinytra.adapter.transform.patch.MethodPatch; import org.sinytra.adapter.types.FieldTypeUsageTransformer; import org.sinytra.adapter.util.provider.ClassLookup; import org.slf4j.Logger; @@ -17,6 +18,8 @@ import java.util.List; +import static org.sinytra.adapter.env.util.MixinAnnotations.MODIFY_CONST; + public class DynamicMixinPatchTest extends MinecraftMixinPatchTest { private static final Logger LOGGER = LogUtils.getLogger(); @@ -45,7 +48,14 @@ public void copyEntries(String from, String to) { ); patcher = Patcher.builder(patchEnvironment) .classTransformer(new FieldTypeUsageTransformer()) - .methodTransformers(DynamicPatches.methodTransformers(List.of())) + .methodTransformers(DynamicPatches.methodTransformers(List.of( + MethodPatch.builder() + .targetClass("net/minecraft/world/inventory/AnvilMenu") + .targetMethod("createResult") + .targetMixinType(MODIFY_CONST) + .modifyTarget("createResultInternal") + .build() + ))) .build(); } @@ -64,6 +74,21 @@ void testExtractUsingCodePath() throws Exception { ); } + /** + * Reproduces Origins Classes' efficient-repairs mixin, whose integer + * {@code @ModifyConstant} target moves from {@code AnvilMenu#createResult} + * to NeoForge's {@code AnvilMenu#createResultInternal}. + */ + @Test + void testModifyConstantTarget() throws Exception { + assertSameCode( + "org/sinytra/adapter/test/mc_26_1_2/mixin/AnvilMenuMixin", + "halfRepairMaterialCost", + assertTargetMethod(), + assertTargetsConstant() + ); + } + @Override protected LoadResult load(String className, List allowedMethods) throws Exception { ClassNode patched = loadClass(className); diff --git a/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/AnvilMenuMixin.java b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/AnvilMenuMixin.java new file mode 100644 index 00000000..904abe65 --- /dev/null +++ b/test/mc-26.1.2/src/testClasses/java/org/sinytra/adapter/test/mc_26_1_2/mixin/AnvilMenuMixin.java @@ -0,0 +1,29 @@ +package org.sinytra.adapter.test.mc_26_1_2.mixin; + +import net.minecraft.world.inventory.AnvilMenu; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +/** + * Equivalent of the Origins Classes efficient-repairs mixin used to verify + * retargeting an integer {@link ModifyConstant} injector for NeoForge. + */ +@Mixin(AnvilMenu.class) +public class AnvilMenuMixin { + @ModifyConstant( + method = "createResult", + constant = @Constant(intValue = 4, ordinal = 0) + ) + private int halfRepairMaterialCost(int original) { + return original / 2; + } + + @ModifyConstant( + method = "createResultInternal", + constant = @Constant(intValue = 4) + ) + private int halfRepairMaterialCostExpected(int original) { + return original / 2; + } +} diff --git a/test/src/test/java/org/sinytra/adapter/patch/test/MethodPatchExtensionTest.java b/test/src/test/java/org/sinytra/adapter/patch/test/MethodPatchExtensionTest.java new file mode 100644 index 00000000..73c110f3 --- /dev/null +++ b/test/src/test/java/org/sinytra/adapter/patch/test/MethodPatchExtensionTest.java @@ -0,0 +1,50 @@ +package org.sinytra.adapter.patch.test; + +import org.junit.jupiter.api.Test; +import org.sinytra.adapter.env.ann.AtData; +import org.sinytra.adapter.patch.config.MutableConfiguration; +import org.sinytra.adapter.patch.config.key.MixinKeys; +import org.sinytra.adapter.patch.mixin.MixinTypes; +import org.sinytra.adapter.transform.patch.MethodPatch; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * Tests extension points used by explicit platform compatibility patches. + */ +class MethodPatchExtensionTest { + /** + * Verifies that standard {@link ModifyConstant} injectors are dispatched to + * the configuration model that resolves their nested {@code @Constant} target. + */ + @Test + void recognizesModifyConstantMixins() { + assertSame( + MixinTypes.MODIFY_CONST, + MixinTypes.getMixinType(ModifyConstant.class.getName().replace('.', '/')) + ); + } + + /** + * Verifies that an injector ordinal and its nested injection-point ordinal can + * be changed independently without either update discarding the other. + */ + @Test + void updatesMethodAndInjectionPointOrdinalsTogether() { + MethodPatch patch = MethodPatch.builder() + .modifyOrdinal(3) + .modifyInjectionPointOrdinal(1) + .build(); + MutableConfiguration clean = MutableConfiguration.create() + .setAtData(AtData.builder("INVOKE").target("Lexample/Target;call()V").build()); + MutableConfiguration dirty = MutableConfiguration.create(); + + patch.configCompleter().accept(clean, dirty); + + assertEquals(3, patch.configuration().getProperty(MixinKeys.ORDINAL).orElseThrow()); + assertEquals(1, dirty.getAtData().getOrdinal().orElseThrow()); + assertEquals("Lexample/Target;call()V", dirty.getAtData().getTargetOrThrow()); + } +}