Skip to content

Commit

Permalink
Merge pull request DaFuqs#509 from imreallybadatnames/1.20.1-main-qsl
Browse files Browse the repository at this point in the history
Hideous ASM for QSL compatibility
  • Loading branch information
DaFuqs authored Oct 6, 2024
2 parents fecb2f8 + 6851448 commit b367ea1
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 5 deletions.
129 changes: 129 additions & 0 deletions src/main/java/de/dafuqs/spectrum/compat/qsl/QSLCompatASM.java
Original file line number Diff line number Diff line change
@@ -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(<LAMBDA>) */
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);
}
}
4 changes: 3 additions & 1 deletion src/main/java/de/dafuqs/spectrum/mixin/compat/Plugin.java
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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.*;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
5 changes: 3 additions & 2 deletions src/main/resources/spectrum.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"KilledByPlayerLootConditionMixin",
"LightningEntityMixin",
"LivingEntityMixin",
"LivingEntityPreventStatusClearMixin",
"compat.quilt_status_effect.absent.LivingEntityPreventStatusClearMixin",
"LoomContainerPatternSlotMixin",
"LoomScreenHandlerMixin",
"MapStateMixin",
Expand Down Expand Up @@ -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",
Expand Down

0 comments on commit b367ea1

Please sign in to comment.