2018-09-06 11:39:01 +00:00
|
|
|
/*
|
|
|
|
* Minecraft Forge
|
2019-02-10 22:57:03 +00:00
|
|
|
* Copyright (c) 2016-2019.
|
2018-09-06 11:39:01 +00:00
|
|
|
*
|
|
|
|
* This library is free software; you can redistribute it and/or
|
|
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
|
|
* License as published by the Free Software Foundation version 2.1
|
|
|
|
* of the License.
|
|
|
|
*
|
|
|
|
* This library is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
|
|
* Lesser General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
|
|
* License along with this library; if not, write to the Free Software
|
|
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
|
|
*/
|
2018-09-21 09:15:03 +00:00
|
|
|
|
2018-09-06 11:39:01 +00:00
|
|
|
package net.minecraftforge.common.asm;
|
|
|
|
|
|
|
|
import java.nio.file.Path;
|
2019-02-07 21:36:30 +00:00
|
|
|
import java.util.List;
|
|
|
|
import java.util.stream.Collectors;
|
2018-09-06 11:39:01 +00:00
|
|
|
|
2019-01-02 02:35:23 +00:00
|
|
|
import net.minecraftforge.fml.loading.AdvancedLogMessageAdapter;
|
2018-09-06 11:39:01 +00:00
|
|
|
import org.apache.logging.log4j.LogManager;
|
|
|
|
import org.apache.logging.log4j.Logger;
|
2018-09-16 11:20:28 +00:00
|
|
|
import org.objectweb.asm.Label;
|
2018-09-06 11:39:01 +00:00
|
|
|
import org.objectweb.asm.Opcodes;
|
|
|
|
import org.objectweb.asm.Type;
|
|
|
|
import org.objectweb.asm.commons.InstructionAdapter;
|
|
|
|
import org.objectweb.asm.tree.ClassNode;
|
|
|
|
import org.objectweb.asm.tree.FieldNode;
|
|
|
|
import org.objectweb.asm.tree.MethodNode;
|
|
|
|
|
|
|
|
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Modifies specified enums to allow runtime extension by making the $VALUES field non-final and
|
|
|
|
* injecting constructor calls which are not valid in normal java code.
|
|
|
|
*/
|
|
|
|
public class RuntimeEnumExtender implements ILaunchPluginService {
|
|
|
|
|
|
|
|
private static final Logger LOGGER = LogManager.getLogger();
|
|
|
|
private final Type STRING = Type.getType(String.class);
|
2018-09-16 11:20:28 +00:00
|
|
|
private final Type ENUM = Type.getType(Enum.class);
|
2019-02-07 21:36:30 +00:00
|
|
|
private final Type MARKER_IFACE = Type.getType("Lnet/minecraftforge/common/IExtensibleEnum;");
|
2018-09-06 11:39:01 +00:00
|
|
|
private final Type ARRAY_UTILS = Type.getType("Lorg/apache/commons/lang3/ArrayUtils;"); //Don't directly reference this to prevent class loading.
|
|
|
|
private final String ADD_DESC = Type.getMethodDescriptor(Type.getType(Object[].class), Type.getType(Object[].class), Type.getType(Object.class));
|
2019-01-17 18:11:54 +00:00
|
|
|
private final Type UNSAFE_HACKS = Type.getType("Lnet/minecraftforge/fml/UnsafeHacks;"); //Again, not direct reference to prevent class loading.
|
2018-09-06 11:39:01 +00:00
|
|
|
private final String CLEAN_DESC = Type.getMethodDescriptor(Type.VOID_TYPE, Type.getType(Class.class));
|
2018-09-16 11:20:28 +00:00
|
|
|
private final String NAME_DESC = Type.getMethodDescriptor(STRING);
|
|
|
|
private final String EQUALS_DESC = Type.getMethodDescriptor(Type.BOOLEAN_TYPE, STRING);
|
2018-09-06 11:39:01 +00:00
|
|
|
|
|
|
|
@Override
|
|
|
|
public String name() {
|
|
|
|
return "runtime_enum_extender";
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override public void addResource(Path resource, String name) { }
|
|
|
|
|
|
|
|
@Override public <T> T getExtension() { return null; } // ?
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean handlesClass(Type classType, boolean isEmpty)
|
|
|
|
{
|
|
|
|
return !isEmpty; //Any way to determine if its a enum at this level?
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public ClassNode processClass(ClassNode classNode, Type classType)
|
|
|
|
{
|
|
|
|
if ((classNode.access & Opcodes.ACC_ENUM) == 0)
|
|
|
|
return classNode;
|
|
|
|
|
2018-09-10 03:16:51 +00:00
|
|
|
Type array = Type.getType("[" + classType.getDescriptor());
|
2018-09-06 11:39:01 +00:00
|
|
|
final int flags = Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL | Opcodes.ACC_SYNTHETIC;
|
|
|
|
|
|
|
|
FieldNode values = classNode.fields.stream().filter(f -> f.desc.contentEquals(array.getDescriptor()) && ((f.access & flags) == flags)).findFirst().orElse(null);
|
2019-02-07 21:36:30 +00:00
|
|
|
|
|
|
|
if (!classNode.interfaces.contains(MARKER_IFACE.getInternalName())) {
|
|
|
|
return classNode;
|
|
|
|
}
|
|
|
|
|
2018-09-06 11:39:01 +00:00
|
|
|
//Static methods named "create", with first argument as a string, and returning this type
|
2019-02-07 21:36:30 +00:00
|
|
|
List<MethodNode> candidates = classNode.methods.stream()
|
|
|
|
.filter(m -> ((m.access & Opcodes.ACC_STATIC) != 0) && m.name.equals("create") && Type.getReturnType(m.desc).equals(classType))
|
|
|
|
.collect(Collectors.toList());
|
|
|
|
|
|
|
|
candidates.forEach(mtd ->
|
2018-09-06 11:39:01 +00:00
|
|
|
{
|
|
|
|
Type[] args = Type.getArgumentTypes(mtd.desc);
|
2019-02-07 21:36:30 +00:00
|
|
|
if (args.length == 0 || !args[0].equals(STRING)) {
|
|
|
|
LOGGER.fatal(()->new AdvancedLogMessageAdapter(sb-> {
|
|
|
|
sb.append("Enum has create method without String as first parameter:\n");
|
|
|
|
sb.append(" Enum: " + classType.getDescriptor()).append("\n");
|
|
|
|
sb.append(" Target: ").append(mtd.name + mtd.desc).append("\n");
|
|
|
|
}));
|
|
|
|
throw new IllegalStateException("Enum has create method without String as first parameter: " + mtd.name + mtd.desc);
|
|
|
|
}
|
|
|
|
|
2018-09-06 11:39:01 +00:00
|
|
|
Type[] ctrArgs = new Type[args.length + 1];
|
|
|
|
ctrArgs[0] = STRING;
|
|
|
|
ctrArgs[1] = Type.INT_TYPE;
|
|
|
|
for (int x = 1; x < args.length; x++)
|
|
|
|
ctrArgs[1 + x] = args[x];
|
|
|
|
|
|
|
|
String desc = Type.getMethodDescriptor(Type.VOID_TYPE, ctrArgs);
|
|
|
|
|
|
|
|
MethodNode ctr = classNode.methods.stream().filter(m -> m.name.equals("<init>") && m.desc.equals(desc)).findFirst().orElse(null);
|
|
|
|
if (ctr == null)
|
|
|
|
{
|
2019-01-06 20:46:29 +00:00
|
|
|
LOGGER.fatal(()->new AdvancedLogMessageAdapter(sb-> {
|
2018-09-10 03:16:51 +00:00
|
|
|
sb.append("Enum has create method with no matching constructor:\n");
|
2019-02-07 21:36:30 +00:00
|
|
|
sb.append(" Enum: ").append(classType.getDescriptor()).append("\n");
|
|
|
|
sb.append(" Candidate: ").append(mtd.desc).append("\n");
|
2018-09-10 03:16:51 +00:00
|
|
|
sb.append(" Target: ").append(desc).append("\n");
|
2018-11-17 07:15:39 +00:00
|
|
|
classNode.methods.stream().filter(m -> m.name.equals("<init>")).forEach(m -> sb.append(" : ").append(m.desc).append("\n"));
|
2018-09-10 03:16:51 +00:00
|
|
|
}));
|
2018-09-06 11:39:01 +00:00
|
|
|
throw new IllegalStateException("Enum has create method with no matching constructor: " + desc);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (values == null)
|
|
|
|
{
|
2019-01-06 20:46:29 +00:00
|
|
|
LOGGER.fatal(()->new AdvancedLogMessageAdapter(sb-> {
|
2018-09-10 03:16:51 +00:00
|
|
|
sb.append("Enum has create method but we could not find $VALUES. Found:\n");
|
|
|
|
classNode.fields.stream().filter(f -> (f.access & Opcodes.ACC_STATIC) != 0).
|
|
|
|
forEach(m -> sb.append(" ").append(m.name).append(" ").append(m.desc).append("\n"));
|
|
|
|
}));
|
2018-09-06 11:39:01 +00:00
|
|
|
throw new IllegalStateException("Enum has create method but we could not find $VALUES");
|
|
|
|
}
|
|
|
|
|
|
|
|
values.access &= values.access & ~Opcodes.ACC_FINAL; //Strip the final so JITer doesn't inline things.
|
|
|
|
|
2019-02-07 22:01:40 +00:00
|
|
|
mtd.access |= Opcodes.ACC_SYNCHRONIZED;
|
2018-09-06 11:39:01 +00:00
|
|
|
mtd.instructions.clear();
|
|
|
|
InstructionAdapter ins = new InstructionAdapter(mtd);
|
2018-09-16 11:20:28 +00:00
|
|
|
|
|
|
|
int vars = 0;
|
|
|
|
for (Type arg : args)
|
|
|
|
vars += arg.getSize();
|
|
|
|
|
|
|
|
{
|
|
|
|
vars += 1; //int x
|
|
|
|
Label for_start = new Label();
|
|
|
|
Label for_condition = new Label();
|
|
|
|
Label for_inc = new Label();
|
|
|
|
|
|
|
|
ins.iconst(0);
|
|
|
|
ins.store(vars, Type.INT_TYPE);
|
|
|
|
ins.goTo(for_condition);
|
|
|
|
//if (!VALUES[x].name().equalsIgnoreCase(name)) goto for_inc
|
|
|
|
ins.mark(for_start);
|
|
|
|
ins.getstatic(classType.getInternalName(), values.name, values.desc);
|
|
|
|
ins.load(vars, Type.INT_TYPE);
|
|
|
|
ins.aload(array);
|
|
|
|
ins.invokevirtual(ENUM.getInternalName(), "name", NAME_DESC, false);
|
|
|
|
ins.load(0, STRING);
|
|
|
|
ins.invokevirtual(STRING.getInternalName(), "equalsIgnoreCase", EQUALS_DESC, false);
|
|
|
|
ins.ifeq(for_inc);
|
|
|
|
//return VALUES[x];
|
|
|
|
ins.getstatic(classType.getInternalName(), values.name, values.desc);
|
|
|
|
ins.load(vars, Type.INT_TYPE);
|
|
|
|
ins.aload(array);
|
|
|
|
ins.areturn(classType);
|
|
|
|
//x++
|
|
|
|
ins.mark(for_inc);
|
|
|
|
ins.iinc(vars, 1);
|
|
|
|
//if (x < VALUES.length) goto for_start
|
|
|
|
ins.mark(for_condition);
|
|
|
|
ins.load(vars, Type.INT_TYPE);
|
|
|
|
ins.getstatic(classType.getInternalName(), values.name, values.desc);
|
|
|
|
ins.arraylength();
|
|
|
|
ins.ificmplt(for_start);
|
|
|
|
}
|
|
|
|
|
|
|
|
{
|
|
|
|
vars += 1; //enum ret;
|
2019-01-30 20:51:14 +00:00
|
|
|
//ret = new ThisType(name, VALUES.length, args..)
|
2018-09-16 11:20:28 +00:00
|
|
|
ins.anew(classType);
|
|
|
|
ins.dup();
|
|
|
|
ins.load(0, STRING);
|
|
|
|
ins.getstatic(classType.getInternalName(), values.name, values.desc);
|
|
|
|
ins.arraylength();
|
|
|
|
int idx = 1;
|
|
|
|
for (int x = 1; x < args.length; x++)
|
|
|
|
{
|
|
|
|
ins.load(idx, args[x]);
|
|
|
|
idx += args[x].getSize();
|
|
|
|
}
|
|
|
|
ins.invokespecial(classType.getInternalName(), "<init>", desc, false);
|
|
|
|
ins.store(vars, classType);
|
|
|
|
// VALUES = ArrayUtils.add(VALUES, ret)
|
|
|
|
ins.getstatic(classType.getInternalName(), values.name, values.desc);
|
|
|
|
ins.load(vars, classType);
|
|
|
|
ins.invokestatic(ARRAY_UTILS.getInternalName(), "add", ADD_DESC, false);
|
|
|
|
ins.checkcast(array);
|
|
|
|
ins.putstatic(classType.getInternalName(), values.name, values.desc);
|
|
|
|
//EnumHelper.cleanEnumCache(ThisType.class)
|
|
|
|
ins.visitLdcInsn(classType);
|
2019-01-17 18:11:54 +00:00
|
|
|
ins.invokestatic(UNSAFE_HACKS.getInternalName(), "cleanEnumCache", CLEAN_DESC, false);
|
2018-09-16 11:20:28 +00:00
|
|
|
//return ret
|
|
|
|
ins.load(vars, classType);
|
|
|
|
ins.areturn(classType);
|
|
|
|
}
|
2018-09-06 11:39:01 +00:00
|
|
|
});
|
|
|
|
return classNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|