Replace CapabilityProvider mixin with ASM transformer

Works around this Mixin bug: https://github.com/FabricMC/Mixin/issues/146

Since CapabilityProvider is the parent of many commonly targeted classes
like Level, ItemStack, etc., this breaks mods

Fixes #650
This commit is contained in:
embeddedt 2026-04-28 18:46:09 -04:00
parent 4e3ecf9b6d
commit c73cdc49a4
No known key found for this signature in database
GPG Key ID: A69433EC199B5613
4 changed files with 177 additions and 40 deletions

View File

@ -1,39 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.faster_capabilities;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.nbt.CompoundTag;
import net.minecraftforge.common.capabilities.CapabilityProvider;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import java.util.Objects;
@Mixin(value = CapabilityProvider.class, remap = false)
@SuppressWarnings("rawtypes")
public class CapabilityProviderMixin {
@Shadow
private boolean isLazy;
@Shadow
private boolean initialized;
@Shadow
private CompoundTag lazyData;
/**
* @author embeddedt
* @reason avoid initializing capabilities in the case where we can reasonably expect the lazy data
* to deserialize to the same concrete caps
*/
@WrapMethod(method = "areCapsCompatible(Lnet/minecraftforge/common/capabilities/CapabilityProvider;)Z")
private boolean mfix$skipLazyInit(CapabilityProvider other, Operation<Boolean> original) {
if (this.isLazy && !this.initialized) {
var otherAcc = ((CapabilityProviderMixin)(Object)other);
if (otherAcc.isLazy && !otherAcc.initialized && otherAcc.getClass() == this.getClass() && Objects.equals(this.lazyData, otherAcc.lazyData)) {
return true;
}
}
return original.call(other);
}
}

View File

@ -5,6 +5,7 @@ import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.embeddedt.modernfix.core.config.ModernFixEarlyConfig;
import org.embeddedt.modernfix.core.config.Option;
import org.embeddedt.modernfix.core.launchplugin.CoreLaunchPluginService;
import org.embeddedt.modernfix.platform.ModernFixPlatformHooks;
import org.embeddedt.modernfix.world.ThreadDumper;
import org.objectweb.asm.Opcodes;
@ -109,7 +110,7 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin {
@Override
public void onLoad(String mixinPackage) {
CoreLaunchPluginService.install();
}
@Override

View File

@ -0,0 +1,73 @@
package org.embeddedt.modernfix.core.launchplugin;
import cpw.mods.modlauncher.LaunchPluginHandler;
import cpw.mods.modlauncher.Launcher;
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
import org.embeddedt.modernfix.core.launchplugin.transformer.CapabilityProviderTransformer;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.Map;
public class CoreLaunchPluginService implements ILaunchPluginService {
private static final Logger LOGGER = LoggerFactory.getLogger("ModernFixLaunchPlugin");
public static void install() {
try {
Field launchPluginsField = Launcher.class.getDeclaredField("launchPlugins");
launchPluginsField.setAccessible(true);
LaunchPluginHandler launchPluginHandler = (LaunchPluginHandler) launchPluginsField.get(Launcher.INSTANCE);
Field pluginsField = LaunchPluginHandler.class.getDeclaredField("plugins");
pluginsField.setAccessible(true);
Map<String, ILaunchPluginService> plugins = (Map<String, ILaunchPluginService>)pluginsField.get(launchPluginHandler);
var service = new CoreLaunchPluginService();
try {
plugins.put(service.name(), service);
} catch (Exception e) {
var newMap = new LinkedHashMap<>(plugins);
newMap.put(service.name(), service);
pluginsField.set(launchPluginHandler, newMap);
}
} catch(Exception e) {
LOGGER.error("Error installing launch plugin service", e);
}
}
@Override
public String name() {
return "modernfix";
}
private static final EnumSet<Phase> GO = EnumSet.of(Phase.AFTER);
private static final EnumSet<Phase> NOGO = EnumSet.noneOf(Phase.class);
private static final Map<String, Transformer> TRANSFORMERS = Map.of(
"net.minecraftforge.common.capabilities.CapabilityProvider", new CapabilityProviderTransformer()
);
@Override
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty) {
return !isEmpty && TRANSFORMERS.containsKey(classType.getClassName()) ? GO : NOGO;
}
@Override
public int processClassWithFlags(Phase phase, ClassNode classNode, Type classType, String reason) {
if (classNode == null) {
return 0;
}
var transformer = TRANSFORMERS.get(classType.getClassName());
if (transformer == null) {
return 0;
}
return transformer.transform(classNode);
}
public interface Transformer {
int transform(ClassNode node);
}
}

View File

@ -0,0 +1,102 @@
package org.embeddedt.modernfix.core.launchplugin.transformer;
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
import org.embeddedt.modernfix.core.launchplugin.CoreLaunchPluginService;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.*;
/**
* Injects an early-return into {@code CapabilityProvider#areCapsCompatible} that skips lazy
* initialization when both providers are lazy, uninitialized, of the same class, and carry equal
* {@code lazyData}. In that case the capabilities are trivially compatible and the full init can
* be avoided.
* @author embeddedt
*/
public class CapabilityProviderTransformer implements CoreLaunchPluginService.Transformer {
private static final String OWNER = "net/minecraftforge/common/capabilities/CapabilityProvider";
private static final String COMPOUND_TAG = "net/minecraft/nbt/CompoundTag";
private static final String HELPER_NAME = "mfix$skipLazyInit";
private static final String HELPER_DESC = "(L" + OWNER + ";L" + OWNER + ";)Z";
@Override
public int transform(ClassNode node) {
String targetDesc = "(L" + OWNER + ";)Z";
for (MethodNode method : node.methods) {
if (method.name.equals("areCapsCompatible") && method.desc.equals(targetDesc)) {
injectCall(method);
node.methods.add(buildHelper());
break;
}
}
return ILaunchPluginService.ComputeFlags.COMPUTE_FRAMES;
}
private MethodNode buildHelper() {
MethodNode mn = new MethodNode(
Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC,
HELPER_NAME, HELPER_DESC, null, null);
InsnList il = mn.instructions;
LabelNode returnFalse = new LabelNode();
// if (!self.isLazy) goto returnFalse
il.add(new VarInsnNode(Opcodes.ALOAD, 0));
il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "isLazy", "Z"));
il.add(new JumpInsnNode(Opcodes.IFEQ, returnFalse));
// if (self.initialized) goto returnFalse
il.add(new VarInsnNode(Opcodes.ALOAD, 0));
il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "initialized", "Z"));
il.add(new JumpInsnNode(Opcodes.IFNE, returnFalse));
// if (!other.isLazy) goto returnFalse
il.add(new VarInsnNode(Opcodes.ALOAD, 1));
il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "isLazy", "Z"));
il.add(new JumpInsnNode(Opcodes.IFEQ, returnFalse));
// if (other.initialized) goto returnFalse
il.add(new VarInsnNode(Opcodes.ALOAD, 1));
il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "initialized", "Z"));
il.add(new JumpInsnNode(Opcodes.IFNE, returnFalse));
// if (other.getClass() != self.getClass()) goto returnFalse
il.add(new VarInsnNode(Opcodes.ALOAD, 1));
il.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false));
il.add(new VarInsnNode(Opcodes.ALOAD, 0));
il.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false));
il.add(new JumpInsnNode(Opcodes.IF_ACMPNE, returnFalse));
// if (!Objects.equals(self.lazyData, other.lazyData)) goto returnFalse
il.add(new VarInsnNode(Opcodes.ALOAD, 0));
il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "lazyData", "L" + COMPOUND_TAG + ";"));
il.add(new VarInsnNode(Opcodes.ALOAD, 1));
il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "lazyData", "L" + COMPOUND_TAG + ";"));
il.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/util/Objects", "equals", "(Ljava/lang/Object;Ljava/lang/Object;)Z", false));
il.add(new JumpInsnNode(Opcodes.IFEQ, returnFalse));
il.add(new InsnNode(Opcodes.ICONST_1));
il.add(new InsnNode(Opcodes.IRETURN));
il.add(returnFalse);
il.add(new InsnNode(Opcodes.ICONST_0));
il.add(new InsnNode(Opcodes.IRETURN));
mn.maxLocals = 2;
mn.maxStack = 2;
return mn;
}
private void injectCall(MethodNode method) {
InsnList il = new InsnList();
LabelNode skip = new LabelNode();
il.add(new VarInsnNode(Opcodes.ALOAD, 0));
il.add(new VarInsnNode(Opcodes.ALOAD, 1));
il.add(new MethodInsnNode(Opcodes.INVOKESTATIC, OWNER, HELPER_NAME, HELPER_DESC, false));
il.add(new JumpInsnNode(Opcodes.IFEQ, skip));
il.add(new InsnNode(Opcodes.ICONST_1));
il.add(new InsnNode(Opcodes.IRETURN));
il.add(skip);
method.instructions.insert(il);
}
}