diff --git a/arclight-api/src/main/java/io/izzel/arclight/api/ArclightVersion.java b/arclight-api/src/main/java/io/izzel/arclight/api/ArclightVersion.java index 7403b1c0..7e85b36a 100644 --- a/arclight-api/src/main/java/io/izzel/arclight/api/ArclightVersion.java +++ b/arclight-api/src/main/java/io/izzel/arclight/api/ArclightVersion.java @@ -4,21 +4,27 @@ import java.util.Objects; public class ArclightVersion { - public static final ArclightVersion v1_14 = new ArclightVersion("1.14.4", 1140); - public static final ArclightVersion v1_15 = new ArclightVersion("1.15.2", 1152); + public static final ArclightVersion v1_14 = new ArclightVersion("1.14.4", 1140, "v1_14_R1"); + public static final ArclightVersion v1_15 = new ArclightVersion("1.15.2", 1152, "v1_15_R1"); private final String name; private final int num; + private final String pkg; - public ArclightVersion(String name, int num) { + public ArclightVersion(String name, int num, String pkg) { this.name = name; this.num = num; + this.pkg = pkg; } public String getName() { return name; } + public String packageName() { + return pkg; + } + @Override public String toString() { return "ArclightVersion{" + @@ -43,6 +49,11 @@ public class ArclightVersion { private static ArclightVersion version; + public static ArclightVersion current() { + if (ArclightVersion.version == null) throw new IllegalStateException("Version is not set!"); + return version; + } + public static void setVersion(ArclightVersion version) { if (ArclightVersion.version != null) throw new IllegalStateException("Version is already set!"); if (version == null) throw new IllegalArgumentException("Version cannot be null!"); diff --git a/arclight-common/src/main/java/io/izzel/arclight/common/asm/ArclightImplementer.java b/arclight-common/src/main/java/io/izzel/arclight/common/asm/ArclightImplementer.java new file mode 100644 index 00000000..b4d2c2d7 --- /dev/null +++ b/arclight-common/src/main/java/io/izzel/arclight/common/asm/ArclightImplementer.java @@ -0,0 +1,81 @@ +package io.izzel.arclight.common.asm; + +import cpw.mods.modlauncher.serviceapi.ILaunchPluginService; +import io.izzel.arclight.common.mod.util.log.ArclightI18nLogger; +import org.apache.logging.log4j.Logger; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.launch.MixinLaunchPlugin; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +public class ArclightImplementer implements ILaunchPluginService { + + static final Logger LOGGER = ArclightI18nLogger.getLogger("Implementer"); + + private static final EnumSet OH_YES_SIR = EnumSet.of(Phase.AFTER); + private static final EnumSet NOT_TODAY = EnumSet.noneOf(Phase.class); + + private final Map implementers = new HashMap<>(); + private volatile Consumer auditAcceptor; + private ITransformerLoader transformerLoader; + + @Override + public String name() { + return "arclight_implementer"; + } + + @Override + public void initializeLaunch(ITransformerLoader transformerLoader, Path[] specialPaths) { + this.transformerLoader = transformerLoader; + this.implementers.put("inventory", new InventoryImplementer()); + } + + @Override + public EnumSet handlesClass(Type classType, boolean isEmpty, String reason) { + if (MixinLaunchPlugin.NAME.equals(reason)) { + return NOT_TODAY; + } + return isEmpty ? NOT_TODAY : OH_YES_SIR; + } + + @Override + public EnumSet handlesClass(Type classType, boolean isEmpty) { + throw new IllegalStateException("Outdated ModLauncher"); + } + + @Override + public void customAuditConsumer(String className, Consumer auditDataAcceptor) { + auditAcceptor = auditDataAcceptor; + } + + @Override + public boolean processClass(Phase phase, ClassNode classNode, Type classType, String reason) { + if (MixinLaunchPlugin.NAME.equals(reason)) { + return false; + } + List trails = new ArrayList<>(); + for (Map.Entry entry : implementers.entrySet()) { + String key = entry.getKey(); + Implementer implementer = entry.getValue(); + if (implementer.processClass(classNode, transformerLoader)) { + trails.add(key); + } + } + if (!trails.isEmpty()) { + this.auditAcceptor.accept(new String[]{String.join(",", trails)}); + } + return !trails.isEmpty(); + } + + @Override + public boolean processClass(Phase phase, ClassNode classNode, Type classType) { + throw new IllegalStateException("Outdated ModLauncher"); + } +} diff --git a/arclight-common/src/main/java/io/izzel/arclight/common/asm/Implementer.java b/arclight-common/src/main/java/io/izzel/arclight/common/asm/Implementer.java new file mode 100644 index 00000000..4cb91b6e --- /dev/null +++ b/arclight-common/src/main/java/io/izzel/arclight/common/asm/Implementer.java @@ -0,0 +1,9 @@ +package io.izzel.arclight.common.asm; + +import cpw.mods.modlauncher.serviceapi.ILaunchPluginService; +import org.objectweb.asm.tree.ClassNode; + +public interface Implementer { + + boolean processClass(ClassNode node, ILaunchPluginService.ITransformerLoader transformerLoader); +} diff --git a/arclight-common/src/main/java/io/izzel/arclight/common/asm/InventoryImplementer.java b/arclight-common/src/main/java/io/izzel/arclight/common/asm/InventoryImplementer.java new file mode 100644 index 00000000..9ba3bbec --- /dev/null +++ b/arclight-common/src/main/java/io/izzel/arclight/common/asm/InventoryImplementer.java @@ -0,0 +1,200 @@ +package io.izzel.arclight.common.asm; + +import com.google.common.io.ByteStreams; +import cpw.mods.modlauncher.serviceapi.ILaunchPluginService; +import io.izzel.arclight.api.ArclightVersion; +import io.izzel.arclight.i18n.LocalizedException; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.VarInsnNode; + +import java.io.InputStream; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +public class InventoryImplementer implements Implementer { + + private static final Marker MARKER = MarkerManager.getMarker("INVENTORY"); + private static final String INV_TYPE = "net/minecraft/inventory/IInventory"; + private static final String BRIDGE_TYPE = "io/izzel/arclight/common/bridge/inventory/IInventoryBridge"; + + private final Map map = new ConcurrentHashMap<>(); + + public InventoryImplementer() { + map.put(INV_TYPE, true); + map.put("java/lang/Object", false); + } + + @Override + public boolean processClass(ClassNode node, ILaunchPluginService.ITransformerLoader transformerLoader) { + try { + if (node.interfaces.contains(BRIDGE_TYPE)) { + return false; + } + if (isInventoryClass(node, transformerLoader)) { + tryImplement(node); + return true; + } else return false; + } catch (Throwable t) { + if (t instanceof LocalizedException) { + ArclightImplementer.LOGGER.error(MARKER, ((LocalizedException) t).node(), ((LocalizedException) t).args()); + } else { + ArclightImplementer.LOGGER.error(t); + } + return false; + } + } + + private boolean isInventoryClass(ClassNode node, ILaunchPluginService.ITransformerLoader transformerLoader) { + if (node.interfaces.contains(INV_TYPE)) { + map.put(node.name, true); + return true; + } else { + Boolean b = map.get(node.superName); + if (b != null) { + map.put(node.name, b); + return b; + } else { + try { + return isInventoryClass(findClass(node.superName, transformerLoader), transformerLoader); + } catch (Throwable t) { + throw LocalizedException.unchecked("implementer.error", t); + } + } + } + } + + private ClassNode findClass(String typeName, ILaunchPluginService.ITransformerLoader transformerLoader) throws Exception { + try { + byte[] bytes = transformerLoader.buildTransformedClassNodeFor(Type.getObjectType(typeName).getClassName()); + ClassNode node = new ClassNode(); + new ClassReader(bytes).accept(node, ClassReader.EXPAND_FRAMES); + return node; + } catch (ClassNotFoundException e) { + InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(typeName + ".class"); + if (stream == null) throw LocalizedException.checked("implementer.not-found", typeName); + byte[] array = ByteStreams.toByteArray(stream); + ClassNode node = new ClassNode(); + new ClassReader(array).accept(node, ClassReader.EXPAND_FRAMES); + return node; + } + } + + private void tryImplement(ClassNode node) { + Set methods = new HashSet<>(); + for (MethodNode method : node.methods) { + methods.add(method.name + method.desc); + } + if (methods.contains("getViewers()Ljava/util/List;")) { + ArclightImplementer.LOGGER.debug(MARKER, "Found implemented class {}", node.name); + node.interfaces.add(BRIDGE_TYPE); + } else { + List list = findPossibleList(node); + if (list.size() != 1) { + if (list.size() > 1) { + ArclightImplementer.LOGGER.warn(MARKER, "Found multiple possible fields in class {}: {}", node.name, list.stream().map(it -> it.name + it.desc).collect(Collectors.joining(", "))); + } else return; + } + ArclightImplementer.LOGGER.debug(MARKER, "Implementing inventory for class {}", node.name); + FieldNode stackList = list.get(0); + FieldNode transaction = new FieldNode(Opcodes.ACC_PRIVATE, "transaction", Type.getType(List.class).getDescriptor(), null, null); + FieldNode maxStack = new FieldNode(Opcodes.ACC_PRIVATE, "maxStack", "I", null, 64); + node.fields.add(transaction); + node.fields.add(maxStack); + node.interfaces.add(BRIDGE_TYPE); + { + MethodNode methodNode = new MethodNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, "getContents", Type.getMethodDescriptor(Type.getType(List.class)), null, null); + InsnList insnList = new InsnList(); + insnList.add(new VarInsnNode(Opcodes.ALOAD, 0)); + insnList.add(new FieldInsnNode(Opcodes.GETFIELD, node.name, stackList.name, stackList.desc)); + insnList.add(new InsnNode(Opcodes.ARETURN)); + methodNode.instructions = insnList; + node.methods.add(methodNode); + } + { + MethodNode methodNode = new MethodNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, "onOpen", Type.getMethodDescriptor(Type.VOID_TYPE, Type.getObjectType("org/bukkit/craftbukkit/" + ArclightVersion.current().packageName() + "/entity/CraftHumanEntity")), null, null); + InsnList insnList = new InsnList(); + insnList.add(new VarInsnNode(Opcodes.ALOAD, 0)); + insnList.add(new FieldInsnNode(Opcodes.GETFIELD, node.name, transaction.name, transaction.desc)); + insnList.add(new VarInsnNode(Opcodes.ALOAD, 1)); + insnList.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, Type.getInternalName(List.class), "add", "(Ljava/lang/Object;)Z", false)); + insnList.add(new InsnNode(Opcodes.POP)); + insnList.add(new InsnNode(Opcodes.RETURN)); + methodNode.instructions = insnList; + node.methods.add(methodNode); + } + { + MethodNode methodNode = new MethodNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, "onClose", Type.getMethodDescriptor(Type.VOID_TYPE, Type.getObjectType("org/bukkit/craftbukkit/" + ArclightVersion.current().packageName() + "/entity/CraftHumanEntity")), null, null); + InsnList insnList = new InsnList(); + insnList.add(new VarInsnNode(Opcodes.ALOAD, 0)); + insnList.add(new FieldInsnNode(Opcodes.GETFIELD, node.name, transaction.name, transaction.desc)); + insnList.add(new VarInsnNode(Opcodes.ALOAD, 1)); + insnList.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, Type.getInternalName(List.class), "remove", "(Ljava/lang/Object;)Z", false)); + insnList.add(new InsnNode(Opcodes.POP)); + insnList.add(new InsnNode(Opcodes.RETURN)); + methodNode.instructions = insnList; + node.methods.add(methodNode); + } + { + MethodNode methodNode = new MethodNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, "getViewers", Type.getMethodDescriptor(Type.getType(List.class)), null, null); + InsnList insnList = new InsnList(); + insnList.add(new VarInsnNode(Opcodes.ALOAD, 0)); + insnList.add(new FieldInsnNode(Opcodes.GETFIELD, node.name, transaction.name, transaction.desc)); + insnList.add(new InsnNode(Opcodes.ARETURN)); + methodNode.instructions = insnList; + node.methods.add(methodNode); + } + if (!methods.contains("func_70297_j_()I")) { + MethodNode methodNode = new MethodNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, "func_70297_j_", "()I", null, null); + InsnList insnList = new InsnList(); + insnList.add(new VarInsnNode(Opcodes.ALOAD, 0)); + insnList.add(new FieldInsnNode(Opcodes.GETFIELD, node.name, maxStack.name, maxStack.desc)); + insnList.add(new InsnNode(Opcodes.IRETURN)); + methodNode.instructions = insnList; + node.methods.add(methodNode); + } + { + MethodNode methodNode = new MethodNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, "setMaxStackSize", "(I)V", null, null); + InsnList insnList = new InsnList(); + insnList.add(new VarInsnNode(Opcodes.ALOAD, 0)); + insnList.add(new VarInsnNode(Opcodes.ILOAD, 1)); + insnList.add(new FieldInsnNode(Opcodes.PUTFIELD, node.name, maxStack.name, maxStack.desc)); + insnList.add(new InsnNode(Opcodes.RETURN)); + methodNode.instructions = insnList; + node.methods.add(methodNode); + } + } + } + + private List findPossibleList(ClassNode node) { + LinkedList list = new LinkedList<>(); + for (FieldNode fieldNode : node.fields) { + boolean nonNullList = fieldNode.desc.equals("Lnet/minecraft/util/NonNullList;"); + if (nonNullList || fieldNode.desc.equals("Ljava/util/List;")) { + if (fieldNode.signature != null && fieldNode.signature.contains("")) { + if (nonNullList) { + list.addFirst(fieldNode); + } else { + list.addLast(fieldNode); + } + } + } + } + return list; + } +} diff --git a/arclight-common/src/main/resources/META-INF/services/cpw.mods.modlauncher.serviceapi.ILaunchPluginService b/arclight-common/src/main/resources/META-INF/services/cpw.mods.modlauncher.serviceapi.ILaunchPluginService new file mode 100644 index 00000000..b2330511 --- /dev/null +++ b/arclight-common/src/main/resources/META-INF/services/cpw.mods.modlauncher.serviceapi.ILaunchPluginService @@ -0,0 +1 @@ +io.izzel.arclight.common.asm.ArclightImplementer \ No newline at end of file diff --git a/i18n-config/src/main/resources/META-INF/i18n/zh_cn.conf b/i18n-config/src/main/resources/META-INF/i18n/zh_cn.conf index 22e61fab..f5462c0d 100644 --- a/i18n-config/src/main/resources/META-INF/i18n/zh_cn.conf +++ b/i18n-config/src/main/resources/META-INF/i18n/zh_cn.conf @@ -30,6 +30,10 @@ downloader { access-denied = "没有对 {0} 操作的权限: {1}" } +implementer { + not-found = "找不到类 {}" + error = "发生错误 {}" +} i18n { current-not-available = "选择的语言 {0} 不可用" using-language = "正在使用 {0} 语言,{1} 作为备选语言"