Skip to content

Commit

Permalink
Client/Server Only Methods/Fields in Cfg
Browse files Browse the repository at this point in the history
  • Loading branch information
IntegerLimit committed Aug 5, 2024
1 parent a51316b commit aa16cee
Show file tree
Hide file tree
Showing 6 changed files with 651 additions and 1 deletion.
5 changes: 5 additions & 0 deletions src/main/groovy-tests/recipeMapTests.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import static gregtech.api.GTValues.*

// Find and Removing GT Recipe Helpers. Goes in Post Init.

// For running this on servers, when using recipe properties (in 2.8.10), you will need to add client-side methods
// via labs config.
// Example for Temperature Property:
// gregtech/api/recipes/recipeproperties/TemperatureProperty@drawInfo@(Lnet/minecraft/client/Minecraft;IIILjava/lang/Object;)V

// Building Test Recipes
mods.gregtech.sifter.recipeBuilder()
.inputs(metaitem('nomilabs:dustImpureOsmiridium8020'))
Expand Down
48 changes: 48 additions & 0 deletions src/main/java/com/nomiceu/nomilabs/config/LabsConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,54 @@ public static class Advanced {
@Config.RequiresMcRestart
public String[] ignoreBiomes = new String[0];

@Config.Comment({ "List of Fields to be client side only, acting as @SideOnly(Side.CLIENT).",
"Do not change unless you know what you are doing!",
"Helps with fixing GrS errors Server Side.",
"Format: <CLASS>@<FIELD>",
"Example: gregtech/api/recipes/recipeproperties/TemperatureProperty@KEY",
"Accepts Obfuscated Fields.",
"Many Client Side Only modifications, on the same class, may be inefficient.",
"[default: ]" })
@Config.LangKey("config.nomilabs.advanced.client_side_fields")
@Config.RequiresMcRestart
public String[] clientSideFields = new String[0];

@Config.Comment({ "List of Methods to be client side only, acting as @SideOnly(Side.CLIENT).",
"Do not change unless you know what you are doing!",
"Helps with fixing GrS errors Server Side.",
"Format: <CLASS>@<METHOD>@<DESC>",
"Example: gregtech/api/recipes/recipeproperties/TemperatureProperty@drawInfo@(Lnet/minecraft/client/Minecraft;IIILjava/lang/Object;)V",
"Accepts Obfuscated Methods.",
"Many Client Side Only modifications, on the same class, may be inefficient.",
"[default: ]" })
@Config.LangKey("config.nomilabs.advanced.client_side_methods")
@Config.RequiresMcRestart
public String[] clientSideMethods = new String[0];

@Config.Comment({ "List of Fields to be server side only, acting as @SideOnly(Side.SERVER).",
"Do not change unless you know what you are doing!",
"Helps with fixing GrS errors Client Side.",
"Format: <CLASS>@<FIELD>",
"Example: gregtech/api/recipes/recipeproperties/TemperatureProperty@KEY",
"Accepts Obfuscated Fields.",
"Many Server Side Only modifications, on the same class, may be inefficient.",
"[default: ]" })
@Config.LangKey("config.nomilabs.advanced.server_side_fields")
@Config.RequiresMcRestart
public String[] serverSideFields = new String[0];

@Config.Comment({ "List of Methods to be server side only, acting as @SideOnly(Side.SERVER).",
"Do not change unless you know what you are doing!",
"Helps with fixing GrS errors Client Side.",
"Format: <CLASS>@<METHOD>@<DESC>",
"Example: gregtech/api/recipes/recipeproperties/TemperatureProperty@drawInfo@(Lnet/minecraft/client/Minecraft;IIILjava/lang/Object;)V",
"Accepts Obfuscated Methods.",
"Many Server Side Only modifications, on the same class, may be inefficient.",
"[default: ]" })
@Config.LangKey("config.nomilabs.advanced.server_side_methods")
@Config.RequiresMcRestart
public String[] serverSideMethods = new String[0];

@Config.Comment({ "How to Modify the Language Tab in Minecraft Options.",
"LABS or NOMI adds buttons and text for lanugage pack download.",
"[default: LABS]" })
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/nomiceu/nomilabs/core/LabsCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class LabsCore implements IFMLLoadingPlugin, IEarlyMixinLoader {

@Override
public String[] getASMTransformerClass() {
return new String[0];
return new String[] { "com.nomiceu.nomilabs.core.LabsTransformer" };
}

@Override
Expand Down
134 changes: 134 additions & 0 deletions src/main/java/com/nomiceu/nomilabs/core/LabsTransformer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package com.nomiceu.nomilabs.core;

import java.util.*;

import net.minecraft.launchwrapper.IClassTransformer;
import net.minecraftforge.fml.common.FMLLog;
import net.minecraftforge.fml.relauncher.FMLLaunchHandler;
import net.minecraftforge.fml.relauncher.Side;

import org.apache.commons.lang3.tuple.Pair;
import org.apache.logging.log4j.Level;
import org.objectweb.asm.*;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.MethodNode;

import com.nomiceu.nomilabs.config.LabsConfig;

/**
* @apiNote Inspired by net.minecraftforge.fml.common.asm.transformers.SideTransformer
* <p>
* Allows for Client/Server Only Methods to be specified via Config.
*/
@SuppressWarnings({ "unused", "deprecation" })
public class LabsTransformer implements IClassTransformer, Opcodes {

// Map of Class -> Pair Of Obf Mapping Lists, Left is Fields, Right is Methods
private static Map<String, Pair<List<ObfMapping>, List<ObfMapping>>> toRemove;

/**
* Must setup here so FMLCommonHandler has time to load.
*/
private static void setupToRemove() {
if (toRemove != null) return;

// Can't use Object2ObjectOpenHashMap as in ASM stage
toRemove = new HashMap<>();
String[] methodsRemove;
String[] fieldsRemove;

if (FMLLaunchHandler.side() == Side.SERVER) {
// Remove CLIENT side methods & fields
fieldsRemove = LabsConfig.advanced.clientSideFields;
methodsRemove = LabsConfig.advanced.clientSideMethods;
} else {
// Remove SERVER side methods & fields
fieldsRemove = LabsConfig.advanced.serverSideFields;
methodsRemove = LabsConfig.advanced.serverSideMethods;
}

for (var field : fieldsRemove) {
var parts = field.split("@");
if (parts.length != 2)
throw new IllegalStateException("Invalid Field Remove: " + field +
"! Must have Two Parts, Seperated by '@', Got " + parts.length + "!");

toRemove.computeIfAbsent(parts[0], (k) -> Pair.of(new ArrayList<>(), new ArrayList<>()))
.getLeft().add(new ObfMapping(parts[0], parts[1]));
}

for (var method : methodsRemove) {
var parts = method.split("@");
if (parts.length != 3)
throw new IllegalStateException("Invalid Method Remove: " + method +
"! Must have Three Parts, Seperated by '@', Got " + parts.length + "!");

toRemove.computeIfAbsent(parts[0], (k) -> Pair.of(new ArrayList<>(), new ArrayList<>()))
.getRight().add(new ObfMapping(parts[0], parts[1], parts[2]));
}

FMLLog.log("LabsASM", Level.DEBUG, "Computed To Remove: %s", toRemove);
}

@Override
public byte[] transform(String name, String transformedName, byte[] basicClass) {
setupToRemove();

String internalName = transformedName.replace('.', '/');
if (!toRemove.containsKey(internalName)) return basicClass;

var pair = toRemove.get(internalName);
var fieldsRemove = pair.getLeft();
var methodsRemove = pair.getRight();

ClassNode classNode = new ClassNode();
ClassReader classReader = new ClassReader(basicClass);
classReader.accept(classNode, 0);

for (var toRemove : fieldsRemove) {
Iterator<FieldNode> fields = classNode.fields.iterator();
boolean found = false;
while (fields.hasNext()) {
FieldNode field = fields.next();
if (!toRemove.s_name.equals(field.name)) continue;

FMLLog.log("LabsASM", Level.WARN, "Removing Field: %s.%s", classNode.name, field.name);
fields.remove();
found = true;
break;
}

if (!found)
throw new IllegalStateException("Could not find Field " + classNode.name + "." + toRemove.s_name + "!");
}

var gatherer = new LambdaGatherer();
for (var toRemove : methodsRemove) {
Iterator<MethodNode> methods = classNode.methods.iterator();
boolean found = false;
while (methods.hasNext()) {
MethodNode method = methods.next();
if (!toRemove.matches(method)) continue;

FMLLog.log("LabsASM", Level.WARN, "Removing Method: %s.%s%s", classNode.name, method.name,
method.desc);
methods.remove();
gatherer.accept(method);
found = true;
break;
}

if (!found)
throw new IllegalStateException(
"Could not find Method " + classNode.name + "." + toRemove.s_name + toRemove.s_desc + "!");
}

LambdaGatherer.removeDynamicLambdaMethods(gatherer, classNode);

ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classNode.accept(writer);

return writer.toByteArray();
}
}
81 changes: 81 additions & 0 deletions src/main/java/com/nomiceu/nomilabs/core/LambdaGatherer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.nomiceu.nomilabs.core;

import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;

import net.minecraftforge.fml.common.FMLLog;

import org.apache.logging.log4j.Level;
import org.objectweb.asm.Handle;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;

/**
* @apiNote net.minecraftforge.fml.common.asm.transformers.SideTransformer.LambdaGatherer
*/
@SuppressWarnings("deprecation")
public class LambdaGatherer extends MethodVisitor {

private static final Handle META_FACTORY = new Handle(Opcodes.H_INVOKESTATIC, "java/lang/invoke/LambdaMetafactory",
"metafactory",
"(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;",
false);
private final List<Handle> dynamicLambdaHandles = new ArrayList<>();

public LambdaGatherer() {
super(Opcodes.ASM5);
}

public void accept(MethodNode method) {
ListIterator<AbstractInsnNode> insnNodeIterator = method.instructions.iterator();
while (insnNodeIterator.hasNext()) {
AbstractInsnNode insnNode = insnNodeIterator.next();
if (insnNode.getType() == AbstractInsnNode.INVOKE_DYNAMIC_INSN) {
insnNode.accept(this);
}
}
}

@Override
public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs) {
if (META_FACTORY.equals(bsm)) {
Handle dynamicLambdaHandle = (Handle) bsmArgs[1];
dynamicLambdaHandles.add(dynamicLambdaHandle);
}
}

public List<Handle> getDynamicLambdaHandles() {
return dynamicLambdaHandles;
}

/**
* Remove dynamic synthetic lambda methods that are inside of removed methods.
*/
public static void removeDynamicLambdaMethods(LambdaGatherer gatherer, ClassNode classNode) {
for (List<Handle> dynamicLambdaHandles = gatherer.getDynamicLambdaHandles(); !dynamicLambdaHandles
.isEmpty(); dynamicLambdaHandles = gatherer.getDynamicLambdaHandles()) {
gatherer = new LambdaGatherer();
var methods = classNode.methods.iterator();
while (methods.hasNext()) {
MethodNode method = methods.next();
if ((method.access & Opcodes.ACC_SYNTHETIC) == 0) continue;

for (Handle dynamicLambdaHandle : dynamicLambdaHandles) {
if (method.name.equals(dynamicLambdaHandle.getName()) &&
method.desc.equals(dynamicLambdaHandle.getDesc())) {

FMLLog.log("LabsASM", Level.WARN, "Removing Method: %s.%s%s", classNode.name, method.name,
method.desc);

methods.remove();
gatherer.accept(method);
}
}
}
}
}
}
Loading

0 comments on commit aa16cee

Please sign in to comment.