From 6851448f1b95e8eaaf8f4a71829c15966863a097 Mon Sep 17 00:00:00 2001 From: imreallybadatnames Date: Sun, 6 Oct 2024 21:22:53 +1100 Subject: [PATCH] sopa --- .../spectrum/compat/qsl/QSLCompatASM.java | 129 ++++++++++++++++++ .../dafuqs/spectrum/mixin/compat/Plugin.java | 4 +- .../LivingEntityPreventStatusClearMixin.java | 4 +- .../present/SpectrumEventListenersMixin.java | 49 +++++++ src/main/resources/spectrum.mixins.json | 5 +- 5 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 src/main/java/de/dafuqs/spectrum/compat/qsl/QSLCompatASM.java rename src/main/java/de/dafuqs/spectrum/mixin/{ => compat/quilt_status_effect/absent}/LivingEntityPreventStatusClearMixin.java (95%) create mode 100644 src/main/java/de/dafuqs/spectrum/mixin/compat/quilt_status_effect/present/SpectrumEventListenersMixin.java diff --git a/src/main/java/de/dafuqs/spectrum/compat/qsl/QSLCompatASM.java b/src/main/java/de/dafuqs/spectrum/compat/qsl/QSLCompatASM.java new file mode 100644 index 0000000000..d1b8b0b5f1 --- /dev/null +++ b/src/main/java/de/dafuqs/spectrum/compat/qsl/QSLCompatASM.java @@ -0,0 +1,129 @@ +package de.dafuqs.spectrum.compat.qsl; + +import org.objectweb.asm.*; +import org.objectweb.asm.tree.*; +import org.spongepowered.asm.util.Bytecode; + +import java.lang.invoke.*; + +public class QSLCompatASM { + /* FAPI-specific class name constants */ + private static final String TOREMAP_TRISTATE_CLASS_NAME_INTERNAL = "net/fabricmc/fabric/api/util/TriState"; + private static final String TOREMAP_TRISTATE_CLASS_NAME = "L" + TOREMAP_TRISTATE_CLASS_NAME_INTERNAL + ";"; + + /* QSL-specific class name constants */ + private static final String REMAPPED_TRISTATE_CLASS_NAME_INTERNAL = "org/quiltmc/qsl/base/api/util/TriState"; + private static final String REMAPPED_TRISTATE_CLASS_NAME = "L" + REMAPPED_TRISTATE_CLASS_NAME_INTERNAL + ";"; + + private static final String STATUS_EFFECT_EVENTS_NAME_INTERNAL = "org/quiltmc/qsl/entity/effect/api/StatusEffectEvents"; + + private static final String REMOVAL_REASON_CLASS_NAME = "Lorg/quiltmc/qsl/entity/effect/api/StatusEffectRemovalReason;"; + private static final String QSL_EVENT_CLASS_NAME_INTERNAL = "org/quiltmc/qsl/base/api/event/Event"; + private static final String QSL_EVENT_CLASS_NAME = "L" + QSL_EVENT_CLASS_NAME_INTERNAL + ";"; + private static final String QSL_EVENT_REGISTER_METHOD_NAME = "register"; + private static final String QSL_EVENT_REGISTER_METHOD_DESC = Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(Object.class)); + + private static final String TARGET_IFACE_NAME = "ShouldRemove"; + private static final String TARGET_IFACE_CLASS_NAME_FULL = STATUS_EFFECT_EVENTS_NAME_INTERNAL + "$" + TARGET_IFACE_NAME; + private static final String TARGET_EVENT_FIELD_NAME = "SHOULD_REMOVE"; + private static final String TARGET_IFACE_METHOD_NAME = "shouldRemove"; + private static String TARGET_IFACE_METHOD_DESC = ""; // non-final; populated on transform due to mappings + + /* mixin-specific constants */ + private static final String TARGET_CLASS_NAME = "de/dafuqs/spectrum/registries/SpectrumEventListeners"; + private static final String TARGET_METHOD_NAME = "register"; + private static final String MIXIN_CLASS_NAME_DOTS = "de.dafuqs.spectrum.mixin.compat.quilt_status_effect.present.SpectrumEventListenersMixin"; + private static final String TARGET_ASM_METHOD_NAME = "_shouldRemove"; + private static final String TRANSFORMED_METHOD_NAME = "QSL_shouldRemove"; + + /* generic constants */ + private static final Handle LMF_HANDLE = new Handle(Opcodes.H_INVOKESTATIC, Type.getInternalName(LambdaMetafactory.class), + "metafactory", Bytecode.generateDescriptor(CallSite.class, MethodHandles.Lookup.class, String.class, + MethodType.class, MethodType.class, MethodHandle.class, MethodType.class), false); + + public static String transformMethodDesc(String originalDescriptor) { + var methodArgs = Type.getArgumentTypes(originalDescriptor); + // check if method args are correct just in case + assert methodArgs.length == 3; assert methodArgs[2].equals(Type.getType(Object.class)); + methodArgs[2] = Type.getType(REMOVAL_REASON_CLASS_NAME); // replace 3rd argument with removal reason type + var newDescriptor = Type.getMethodDescriptor(Type.getType(REMAPPED_TRISTATE_CLASS_NAME), methodArgs); + TARGET_IFACE_METHOD_DESC = newDescriptor; // set the global to the freshly-baked descriptor for later use + return newDescriptor; + } + + public static MethodVisitor transformerVisitor(MethodVisitor orig) { + return new MethodVisitor(Opcodes.ASM9, orig) { + @Override + public void visitFieldInsn(int opcode, String owner, String name, String descriptor) { + // in this case the owner is TOREMAP_TRISTATE_CLASS_NAME_INTERNAL and the (dubiously implied) descriptor is TOREMAP_TRISTATE_CLASS_NAME, though check the descriptor just in case + // replace with: owner == REMAPPED_TRISTATE_CLASS_NAME_INTERNAL descriptor == REMAPPED_TRISTATE_CLASS_NAME + if (opcode == Opcodes.GETSTATIC && owner.equals(TOREMAP_TRISTATE_CLASS_NAME_INTERNAL) && descriptor.equals(TOREMAP_TRISTATE_CLASS_NAME)) { + owner = REMAPPED_TRISTATE_CLASS_NAME_INTERNAL; + descriptor = REMAPPED_TRISTATE_CLASS_NAME; + } + super.visitFieldInsn(opcode, owner, name, descriptor); + } + }; + } + + public static void applyOnRemovedMethodTransform(ClassNode targetClass, MethodNode targetMethod) { + /* _shouldRemove: transform TriState type used within into quilt equivalent and turn 3rd argument [reason] into appropriate quilt type */ + /* write remapped version into QSL_shouldRemove */ + targetMethod.accept(new ClassVisitor(Opcodes.ASM9, targetClass) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + return transformerVisitor(super.visitMethod(access, TRANSFORMED_METHOD_NAME, transformMethodDesc(descriptor), signature, exceptions)); + } + }); + // remove original method as it is unused + targetClass.methods.remove(targetMethod); + } + + public static void applyRegisterTransformations(MethodNode registerMethod) { + // graciously stolen from BeforeFinalReturn.find + AbstractInsnNode returnOpcode = null; + int op = Type.getReturnType(registerMethod.desc).getOpcode(Opcodes.IRETURN); + for (AbstractInsnNode insn : registerMethod.instructions) + if (insn instanceof InsnNode && insn.getOpcode() == op) returnOpcode = insn; + assert returnOpcode != null; + + // create separate insn batch + var addedInsns = new InsnList(); + /* register: get StatusEffectEvents.SHOULD_REMOVE */ + var eventField = new FieldInsnNode(Opcodes.GETSTATIC, STATUS_EFFECT_EVENTS_NAME_INTERNAL, TARGET_EVENT_FIELD_NAME, QSL_EVENT_CLASS_NAME); + /* register: emit INDY to create instance of org.quiltmc.qsl.entity.effect.api.StatusEffectEvents$ShouldRemove via QSL_shouldRemove */ + var targetIfaceDesc = Type.getMethodType(TARGET_IFACE_METHOD_DESC); + var targetImplDesc = new Handle(Opcodes.H_INVOKESTATIC, TARGET_CLASS_NAME, TRANSFORMED_METHOD_NAME, TARGET_IFACE_METHOD_DESC, false); + var indy = new InvokeDynamicInsnNode(TARGET_IFACE_METHOD_NAME, // `interfaceMethodName` + Type.getMethodDescriptor(Type.getObjectType(TARGET_IFACE_CLASS_NAME_FULL)), // `factoryType` + LMF_HANDLE, // `metafactory` handle + targetIfaceDesc, // `interfaceMethodType` + targetImplDesc, // `implementation` + targetIfaceDesc // `dynamicMethodType` + ); + /* register: invoke [INVOKEVIRTUAL] org.quiltmc.qsl.entity.effect.api.StatusEffectEvents.SHOULD_REMOVE.register() */ + var invokeRegister = new MethodInsnNode(Opcodes.INVOKEVIRTUAL, QSL_EVENT_CLASS_NAME_INTERNAL, QSL_EVENT_REGISTER_METHOD_NAME, QSL_EVENT_REGISTER_METHOD_DESC); + // add instructions in batch + addedInsns.add(eventField); + addedInsns.add(indy); + addedInsns.add(invokeRegister); + // add ASM'd instructions into method body and check + registerMethod.instructions.insertBefore(returnOpcode, addedInsns); + registerMethod.check(Opcodes.ASM9); + } + + public static void transformSpectrumEventListeners(ClassNode targetClass) { + MethodNode targetMethod = null; + MethodNode registerMethod = null; + for (var method : targetClass.methods) { + if (method.name.equals(TARGET_ASM_METHOD_NAME)) targetMethod = method; + else if (method.name.equals(TARGET_METHOD_NAME)) registerMethod = method; + } + assert targetMethod != null; applyOnRemovedMethodTransform(targetClass, targetMethod); + assert registerMethod != null; applyRegisterTransformations(registerMethod); + } + + public static void postApply(ClassNode targetClass, String mixinClassName) { + if (mixinClassName.equals(MIXIN_CLASS_NAME_DOTS)) transformSpectrumEventListeners(targetClass); + } +} diff --git a/src/main/java/de/dafuqs/spectrum/mixin/compat/Plugin.java b/src/main/java/de/dafuqs/spectrum/mixin/compat/Plugin.java index fa00428e27..96893d52fe 100644 --- a/src/main/java/de/dafuqs/spectrum/mixin/compat/Plugin.java +++ b/src/main/java/de/dafuqs/spectrum/mixin/compat/Plugin.java @@ -1,9 +1,9 @@ package de.dafuqs.spectrum.mixin.compat; +import de.dafuqs.spectrum.compat.qsl.QSLCompatASM; import net.fabricmc.loader.api.*; import org.objectweb.asm.tree.*; import org.spongepowered.asm.mixin.extensibility.*; - import java.util.*; public final class Plugin implements IMixinConfigPlugin { @@ -59,6 +59,8 @@ public void preApply(String targetClassName, ClassNode targetClass, String mixin @Override public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + // TODO: genericize for more ASM transformers in the future. + QSLCompatASM.postApply(targetClass, mixinClassName); } } \ No newline at end of file diff --git a/src/main/java/de/dafuqs/spectrum/mixin/LivingEntityPreventStatusClearMixin.java b/src/main/java/de/dafuqs/spectrum/mixin/compat/quilt_status_effect/absent/LivingEntityPreventStatusClearMixin.java similarity index 95% rename from src/main/java/de/dafuqs/spectrum/mixin/LivingEntityPreventStatusClearMixin.java rename to src/main/java/de/dafuqs/spectrum/mixin/compat/quilt_status_effect/absent/LivingEntityPreventStatusClearMixin.java index e08fa5a854..147e705c6b 100644 --- a/src/main/java/de/dafuqs/spectrum/mixin/LivingEntityPreventStatusClearMixin.java +++ b/src/main/java/de/dafuqs/spectrum/mixin/compat/quilt_status_effect/absent/LivingEntityPreventStatusClearMixin.java @@ -1,4 +1,4 @@ -package de.dafuqs.spectrum.mixin; +package de.dafuqs.spectrum.mixin.compat.quilt_status_effect.absent; import com.llamalad7.mixinextras.injector.v2.*; import com.llamalad7.mixinextras.injector.wrapoperation.*; @@ -16,7 +16,7 @@ import java.util.*; -@Mixin(value = LivingEntity.class, priority = 1001) // Separate mixin to fix conflict with Quilt standard lib +@Mixin(value = LivingEntity.class) public abstract class LivingEntityPreventStatusClearMixin { @Shadow diff --git a/src/main/java/de/dafuqs/spectrum/mixin/compat/quilt_status_effect/present/SpectrumEventListenersMixin.java b/src/main/java/de/dafuqs/spectrum/mixin/compat/quilt_status_effect/present/SpectrumEventListenersMixin.java new file mode 100644 index 0000000000..714eb0a42e --- /dev/null +++ b/src/main/java/de/dafuqs/spectrum/mixin/compat/quilt_status_effect/present/SpectrumEventListenersMixin.java @@ -0,0 +1,49 @@ +package de.dafuqs.spectrum.mixin.compat.quilt_status_effect.present; + +import de.dafuqs.spectrum.api.status_effect.Incurable; +import de.dafuqs.spectrum.mixin.accessors.StatusEffectInstanceAccessor; +import de.dafuqs.spectrum.registries.SpectrumEventListeners; +import de.dafuqs.spectrum.registries.SpectrumStatusEffects; +import net.fabricmc.fabric.api.util.TriState; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.effect.StatusEffectInstance; +import net.minecraft.network.packet.s2c.play.EntityStatusEffectS2CPacket; +import net.minecraft.server.world.ServerWorld; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + + +@Mixin(value = SpectrumEventListeners.class, remap = false) +public class SpectrumEventListenersMixin { + // used by ASM; See QSLCompatASM. Do not touch without first consulting QSLCompatASM. + // MUST: have 3 arguments, arguments must be of (LivingEntity entity, StatusEffectInstance effect, Object reason), + // return FAPI TriState -- MUST NOT USE TriState anywhere EXCEPT as return value. + @Unique + private static TriState _shouldRemove(LivingEntity entity, StatusEffectInstance effect, Object reason) { + if (Incurable.isIncurable(effect) && !affectedByImmunity(entity, effect.getAmplifier())) { + if (effect.getDuration() > 1200) { + ((StatusEffectInstanceAccessor) effect).setDuration(effect.getDuration() - 1200); + if (!entity.getWorld().isClient()) { + ((ServerWorld) entity.getWorld()).getChunkManager().sendToNearbyPlayers(entity, new EntityStatusEffectS2CPacket(entity.getId(), effect)); + } + } + return TriState.FALSE; + } + return TriState.DEFAULT; + } + + @Unique + private static boolean affectedByImmunity(LivingEntity instance, int amplifier) { + var immunity = instance.getStatusEffect(SpectrumStatusEffects.IMMUNITY); + var cost = 1200 + 600 * amplifier; + + if (immunity != null && immunity.getDuration() >= cost) { + ((StatusEffectInstanceAccessor) immunity).setDuration(Math.max(5, immunity.getDuration() - cost)); + if (!instance.getWorld().isClient()) { + ((ServerWorld) instance.getWorld()).getChunkManager().sendToNearbyPlayers(instance, new EntityStatusEffectS2CPacket(instance.getId(), immunity)); + } + return true; + } + return false; + } +} diff --git a/src/main/resources/spectrum.mixins.json b/src/main/resources/spectrum.mixins.json index e78836d6b8..ccb7f3bbb0 100644 --- a/src/main/resources/spectrum.mixins.json +++ b/src/main/resources/spectrum.mixins.json @@ -47,7 +47,7 @@ "KilledByPlayerLootConditionMixin", "LightningEntityMixin", "LivingEntityMixin", - "LivingEntityPreventStatusClearMixin", + "compat.quilt_status_effect.absent.LivingEntityPreventStatusClearMixin", "LoomContainerPatternSlotMixin", "LoomScreenHandlerMixin", "MapStateMixin", @@ -103,7 +103,8 @@ "compat.connectormod.absent.ExplosionMixin", "compat.connectormod.absent.PlayerEntityMixin", "compat.connectormod.present.ExplosionMixin", - "compat.connectormod.present.PlayerEntityMixin" + "compat.connectormod.present.PlayerEntityMixin", + "compat.quilt_status_effect.present.SpectrumEventListenersMixin" ], "client": [ "accessors.DimensionEffectsAccessor",