From 3c55cdf32744a24f1164e206823450e6663c9eb5 Mon Sep 17 00:00:00 2001 From: Geolykt Date: Thu, 11 Aug 2022 17:49:30 +0200 Subject: [PATCH] Use the UnsafeValues class to transform the AutoDisenchanter at runtime One step closer towards #111. However no man should ever have use ASM, UnsafeValues AND sun.misc.Unsafe just to provide compatibility with a plugin. Oh whatever, I like writing transformers either way even if it takes ages to do. --- pom.xml | 34 +- resources/plugin.yml | 1 + .../enchantments_plus/Enchantments_plus.java | 10 + .../compatibility/hackloader/Hackloader.java | 462 ++++++++++++++++++ .../HackloaderInjectionException.java | 28 ++ .../hackloader/SlimefunCallbacks.java | 59 +++ .../hackloader/package-info.java | 8 + .../geolykt/starloader/deobf/DescString.java | 64 +++ 8 files changed, 665 insertions(+), 1 deletion(-) create mode 100644 src/main/java/de/geolykt/enchantments_plus/compatibility/hackloader/Hackloader.java create mode 100644 src/main/java/de/geolykt/enchantments_plus/compatibility/hackloader/HackloaderInjectionException.java create mode 100644 src/main/java/de/geolykt/enchantments_plus/compatibility/hackloader/SlimefunCallbacks.java create mode 100644 src/main/java/de/geolykt/enchantments_plus/compatibility/hackloader/package-info.java create mode 100644 src/main/java/de/geolykt/starloader/deobf/DescString.java diff --git a/pom.xml b/pom.xml index 6ef7ab9..1f10589 100644 --- a/pom.xml +++ b/pom.xml @@ -47,7 +47,7 @@ org.spigotmc spigot-api - 1.19-R0.1-SNAPSHOT + 1.19.2-R0.1-SNAPSHOT provided @@ -186,6 +186,20 @@ + + + com.github.Slimefun + Slimefun4 + RC-32 + provided + + + * + * + + + + uk.antiperson.stackmob @@ -238,6 +252,20 @@ + + + org.ow2.asm + asm + 9.3 + compile + + + org.ow2.asm + asm-tree + 9.3 + compile + + @@ -290,6 +318,10 @@ org.bstats de.geolykt.enchantments_plus.bstats + + org.objectweb.asm + de.geolykt.enchantments_plus.compatibility.hackloader.asm + diff --git a/resources/plugin.yml b/resources/plugin.yml index b3869f4..2d614fa 100644 --- a/resources/plugin.yml +++ b/resources/plugin.yml @@ -3,6 +3,7 @@ main: de.geolykt.enchantments_plus.Enchantments_plus api-version: 1.16 version: ${project.version} softdepend: [Towny, WorldGuard, StackMob, GriefPrevention, ClaimChunk, RoseStacker, LogBlock, CoreProtect, ClaimedCubes] +load-before: [Slimefun] commands: ench: description: Gives basic access; /ench help. diff --git a/src/main/java/de/geolykt/enchantments_plus/Enchantments_plus.java b/src/main/java/de/geolykt/enchantments_plus/Enchantments_plus.java index ee1d9d9..07eb665 100644 --- a/src/main/java/de/geolykt/enchantments_plus/Enchantments_plus.java +++ b/src/main/java/de/geolykt/enchantments_plus/Enchantments_plus.java @@ -36,6 +36,8 @@ import org.bukkit.plugin.java.JavaPlugin; import de.geolykt.enchantments_plus.arrows.EnchantedArrow; +import de.geolykt.enchantments_plus.compatibility.hackloader.Hackloader; +import de.geolykt.enchantments_plus.compatibility.hackloader.HackloaderInjectionException; import de.geolykt.enchantments_plus.compatibility.nativeperm.WGHook; import de.geolykt.enchantments_plus.enchantments.*; import de.geolykt.enchantments_plus.evt.AnvilMerge; @@ -210,4 +212,12 @@ private void setupAnvilMerger() { return; } } + + static { + try { + Hackloader.injectHackloader(); + } catch (HackloaderInjectionException e) { + e.printStackTrace(); + } + } } diff --git a/src/main/java/de/geolykt/enchantments_plus/compatibility/hackloader/Hackloader.java b/src/main/java/de/geolykt/enchantments_plus/compatibility/hackloader/Hackloader.java new file mode 100644 index 0000000..e9f95af --- /dev/null +++ b/src/main/java/de/geolykt/enchantments_plus/compatibility/hackloader/Hackloader.java @@ -0,0 +1,462 @@ +package de.geolykt.enchantments_plus.compatibility.hackloader; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.UnsafeValues; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.PluginDescriptionFile; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.AbstractInsnNode; +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.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LocalVariableNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.VarInsnNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.geolykt.starloader.deobf.DescString; + +import sun.misc.Unsafe; + +/** + * The Hackloader is a tool to modify the bytecode of other plugins at runtime. + * + * @since 4.1.0 + */ +public class Hackloader { + + public static void injectHackloader() throws HackloaderInjectionException { + ClassNode unsafeItf; + try { + unsafeItf = nodeFromClass(UnsafeValues.class); + } catch (IOException e) { + throw new HackloaderInjectionException("Unable to get the bytecode of the UnsafeValues interface", e); + } + + ClassNode hackedUnsafe = new ClassNode(); + hackedUnsafe.superName = "java/lang/Object"; + hackedUnsafe.access = Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER; + hackedUnsafe.version = Opcodes.V16; + hackedUnsafe.name = "de/geolykt/enchantments_plus/compatibility/hackloader/HackedUnsafeValues"; + hackedUnsafe.interfaces.add(unsafeItf.name); + + MethodNode constructor = new MethodNode(Opcodes.ACC_PUBLIC, "", "(L" + unsafeItf.name + ";)V", null, null); + hackedUnsafe.methods.add(constructor); + FieldNode pristineUnsafe = new FieldNode(Opcodes.ACC_PUBLIC, "pristineUnsafe", "L" + unsafeItf.name + ";", null, null); + hackedUnsafe.fields.add(pristineUnsafe); + + constructor.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // ALOAD this + constructor.instructions.add(new VarInsnNode(Opcodes.ALOAD, 1)); // ALOAD server + constructor.instructions.add(new FieldInsnNode(Opcodes.PUTFIELD, hackedUnsafe.name, pristineUnsafe.name, pristineUnsafe.desc)); + constructor.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); // ALOAD this + constructor.instructions.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V")); + constructor.instructions.add(new InsnNode(Opcodes.RETURN)); + + for (MethodNode unsafeInterfaceMethod : unsafeItf.methods) { + MethodNode implMethod = new MethodNode(Opcodes.ACC_PUBLIC, unsafeInterfaceMethod.name, unsafeInterfaceMethod.desc, unsafeInterfaceMethod.signature, unsafeInterfaceMethod.exceptions.toArray(new String[0])); + + implMethod.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); + implMethod.instructions.add(new FieldInsnNode(Opcodes.GETFIELD, hackedUnsafe.name, pristineUnsafe.name, pristineUnsafe.desc)); + + DescString descString = new DescString(unsafeInterfaceMethod.desc); + + int localIndex = 0; + while (descString.hasNext()) { + String type = descString.nextType(); + int opcode; + boolean wide = false; + switch (type.codePointAt(0)) { + case 'L': + case '[': + opcode = Opcodes.ALOAD; + break; + case 'I': + case 'Z': + case 'S': + case 'C': + case 'B': + opcode = Opcodes.ILOAD; + break; + case 'J': + opcode = Opcodes.LLOAD; + wide = true; + break; + case 'F': + opcode = Opcodes.FLOAD; + break; + case 'D': + opcode = Opcodes.DLOAD; + wide = true; + break; + default: + throw new HackloaderInjectionException("Unknown type: " + type.codePointAt(0)); + } + implMethod.instructions.add(new VarInsnNode(opcode, ++localIndex)); + if (wide) { + localIndex++; + } + } + + String returnType = unsafeInterfaceMethod.desc.substring(unsafeInterfaceMethod.desc.lastIndexOf(')') + 1); + + int returnOpcode; + switch (returnType.codePointAt(0)) { + case 'L': + case '[': + returnOpcode = Opcodes.ARETURN; + break; + case 'I': + case 'Z': + case 'S': + case 'C': + case 'B': + returnOpcode = Opcodes.IRETURN; + break; + case 'J': + returnOpcode = Opcodes.LRETURN; + break; + case 'F': + returnOpcode = Opcodes.FRETURN; + break; + case 'D': + returnOpcode = Opcodes.DRETURN; + break; + case 'V': + returnOpcode = Opcodes.RETURN; + break; + default: + throw new HackloaderInjectionException("Unknown return type: " + returnType); + } + + implMethod.maxLocals = localIndex; + implMethod.maxStack = localIndex; + + implMethod.instructions.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, unsafeItf.name, unsafeInterfaceMethod.name, unsafeInterfaceMethod.desc)); + + String pdfDesc = Type.getDescriptor(PluginDescriptionFile.class); + if (implMethod.name.equals("processClass") && implMethod.desc.equals("(" + pdfDesc + "Ljava/lang/String;[B)[B")) { + implMethod.instructions.add(new VarInsnNode(Opcodes.ALOAD, 2)); + implMethod.instructions.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Type.getInternalName(Hackloader.class), "transform", "([BLjava/lang/String;)[B")); + } + + implMethod.instructions.add(new InsnNode(returnOpcode)); + + hackedUnsafe.methods.add(implMethod); + } + + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES); + hackedUnsafe.accept(writer); + byte[] hackedBytes = writer.toByteArray(); + Class hackUnsafeClassInstance; + + try { + hackUnsafeClassInstance = MethodHandles.lookup().defineClass(hackedBytes); + } catch (Exception e) { + throw new HackloaderInjectionException("Could not define the HackedUnsafeValues class!", e); + } + + Object hackUnsafeInstance; + try { + hackUnsafeInstance = hackUnsafeClassInstance.getConstructor(UnsafeValues.class).newInstance(Bukkit.getUnsafe()); + } catch (Exception e) { + throw new HackloaderInjectionException("Could not obtain an instance of the HackedUnsafeValues class!", e); + } + + try { + // Now we use the big Unsafe to set the little unsafe + Field bukkitUnsafeInstanceField = Bukkit.getUnsafe().getClass().getDeclaredField("INSTANCE"); + Field jvmUnsafeField = Unsafe.class.getDeclaredField("theUnsafe"); + jvmUnsafeField.setAccessible(true); + Unsafe jvmUnsafe = (Unsafe) jvmUnsafeField.get(null); + jvmUnsafeField.setAccessible(false); + Object fieldBase = jvmUnsafe.staticFieldBase(bukkitUnsafeInstanceField); + long fieldOffset = jvmUnsafe.staticFieldOffset(bukkitUnsafeInstanceField); + jvmUnsafe.putObject(fieldBase, fieldOffset, hackUnsafeInstance); + } catch (Exception e) { + throw new HackloaderInjectionException("Couldn't set the HackedUnsafeValues as the UnsafeValues impl", e); + } + } + + // based on https://github.com/Geolykt/Slimefun4/commit/36992c82cba65bd3d52bdb706dadc99416ac4be0 + static byte[] transformAutoDisenchanter(byte[] source) { + ClassNode node = new ClassNode(); + ClassReader reader = new ClassReader(source); + reader.accept(node, 0); + + boolean foundDisenchatMethod = false; + for (MethodNode method : node.methods) { + + if (method.name.equals("disenchant")) { + if (foundDisenchatMethod) { + getLogger().error("[Hackloader] Unable to transform {} because multiple disenchant methods were found.", node.name); + return source; // Void all modifications + } + foundDisenchatMethod = true; + AbstractInsnNode insn = method.instructions.getFirst(); + MethodInsnNode isEmptyCall = null; + MethodInsnNode transferEnchantmentsCall = null; + MethodInsnNode sizeCall = null; + while (insn != null) { + if (insn.getOpcode() == Opcodes.INVOKEINTERFACE) { + MethodInsnNode methodInsn = (MethodInsnNode) insn; + if (methodInsn.owner.equals("java/util/Map") && methodInsn.desc.equals("()Z") && methodInsn.name.equals("isEmpty")) { + if (isEmptyCall != null) { + getLogger().error("[Hackloader] Unable to transform {} because multiple isEmpty method calls were found.", node.name); + return source; + } + isEmptyCall = methodInsn; + } else if (methodInsn.owner.equals("java/util/Map") && methodInsn.desc.equals("()I") && methodInsn.name.equals("size")) { + if (sizeCall != null) { + getLogger().error("[Hackloader] Unable to transform {} because multiple size method calls were found.", node.name); + return source; + } + sizeCall = methodInsn; + } + } else if (insn.getOpcode() == Opcodes.INVOKEVIRTUAL) { + MethodInsnNode methodInsn = (MethodInsnNode) insn; + if (methodInsn.owner.equals(node.name) && methodInsn.name.equals("transferEnchantments")) { + if (transferEnchantmentsCall != null) { + getLogger().error("[Hackloader] Unable to transform {} because multiple transferEnchantments method calls were found.", node.name); + return source; + } + transferEnchantmentsCall = methodInsn; + } + } + insn = insn.getNext(); + } + + if (isEmptyCall == null) { + getLogger().error("[Hackloader] Unable to transform {} because no isEmpty method calls were found.", node.name); + return source; + } + if (sizeCall == null) { + getLogger().error("[Hackloader] Unable to transform {} because no size method calls were found.", node.name); + return source; + } + if (transferEnchantmentsCall == null) { + getLogger().error("[Hackloader] Unable to transform {} because no transferEnchantments method calls were found.", node.name); + return source; + } + + AbstractInsnNode aloadEnchs = isEmptyCall.getPrevious(); + if (aloadEnchs.getOpcode() != Opcodes.ALOAD) { + getLogger().error("[Hackloader] ALOAD enchantments not ALOAD"); + return source; + } + int enchCountLocal = method.maxLocals++; + method.instructions.insertBefore(aloadEnchs, new InsnNode(Opcodes.ICONST_0)); + method.instructions.insertBefore(aloadEnchs, new VarInsnNode(Opcodes.ISTORE, enchCountLocal)); + + AbstractInsnNode jumpReturnNull = isEmptyCall.getNext(); + if (jumpReturnNull.getOpcode() != Opcodes.IFNE) { + getLogger().error("[Hackloader] IFNE not IFNE"); + return source; + } + + LabelNode doVanillaLabel = null; + insn = jumpReturnNull.getNext(); + while (insn != null) { + if (insn instanceof LabelNode ln) { + doVanillaLabel = ln; + break; + } + insn = insn.getNext(); + } + + if (doVanillaLabel == null) { + getLogger().error("[Hackloader] doVanillaLabel not found as method ended prematurely"); + return source; + } + + JumpInsnNode replacementJump = new JumpInsnNode(Opcodes.IFEQ, doVanillaLabel); + method.instructions.insert(jumpReturnNull, replacementJump); + method.instructions.remove(jumpReturnNull); + + VarInsnNode firstDisenchatedItemStore = null; + VarInsnNode firstEnchantedBookStore = null; + + for (insn = doVanillaLabel; insn != null; insn = insn.getNext()) { + if (insn instanceof MethodInsnNode methodInsn && methodInsn.owner.equals("org/bukkit/inventory/ItemStack") && methodInsn.name.equals("clone")) { + if (insn.getNext().getOpcode() != Opcodes.ASTORE) { + getLogger().error("[Hackloader] Cloned itemstack not stored."); + return source; + } + if (firstDisenchatedItemStore != null) { + getLogger().error("[Hackloader] Lone itemstack clone not alone."); + return source; + } + firstDisenchatedItemStore = (VarInsnNode) insn.getNext(); + } else if (insn instanceof FieldInsnNode fieldInsn && fieldInsn.owner.equals(Type.getInternalName(Material.class)) && fieldInsn.name.equals("ENCHANTED_BOOK")) { + if (insn.getPrevious().getOpcode() != Opcodes.DUP || insn.getPrevious().getPrevious().getOpcode() != Opcodes.NEW + || insn.getNext().getOpcode() != Opcodes.INVOKESPECIAL || insn.getNext().getNext().getOpcode() != Opcodes.ASTORE) { + continue; + } + if (firstEnchantedBookStore != null) { + getLogger().error("[Hackloader] Lone enchanted book not alone."); + return source; + } + firstEnchantedBookStore = (VarInsnNode) insn.getNext().getNext(); + } + } + + if (firstDisenchatedItemStore == null) { + getLogger().error("[Hackloader] Itemstack not cloned"); + return source; + } + if (firstEnchantedBookStore == null) { + getLogger().error("[Hackloader] No enchanted book itemstack"); + return source; + } + + // Rewrite/Deduplicate index of the disenchatedItem and enchatedBook + int oldDisenchantedItemLocal = firstDisenchatedItemStore.var; + int oldEnchantedBookItemLocal = firstEnchantedBookStore.var; + int disenchantedItemLocal = method.maxLocals++; + int enchantedBookItemLocal = method.maxLocals++; + boolean beginEnchantedBookRewrite = false; + boolean beginDisenchantedItemRewrite = false; + + for (insn = doVanillaLabel; insn != null; insn = insn.getNext()) { + if (insn.getOpcode() == Opcodes.ASTORE || insn.getOpcode() == Opcodes.ALOAD) { + if (insn == firstDisenchatedItemStore) { + beginDisenchantedItemRewrite = true; + } else if (insn == firstEnchantedBookStore) { + beginEnchantedBookRewrite = true; + } + if (((VarInsnNode) insn).var == oldDisenchantedItemLocal && beginDisenchantedItemRewrite) { + ((VarInsnNode) insn).var = disenchantedItemLocal; + } else if (((VarInsnNode) insn).var == oldEnchantedBookItemLocal && beginEnchantedBookRewrite) { + ((VarInsnNode) insn).var = enchantedBookItemLocal; + } + } + } + + LabelNode firstLabel = null; + LabelNode lastLabel = null; + + for (insn = method.instructions.getFirst(); insn != null; insn = insn.getNext()) { + if (insn instanceof LabelNode label) { + if (firstLabel == null) { + firstLabel = label; + } + lastLabel = label; + } + } + + if (firstLabel == null) { + getLogger().error("[Hackloader] Unable to obtain first label in method"); + return source; + } + + // Write deduplicated locals to debug + method.localVariables.add(new LocalVariableNode("dedup_disenchantedItem", Type.getDescriptor(ItemStack.class), null, firstLabel, lastLabel, disenchantedItemLocal)); + method.localVariables.add(new LocalVariableNode("dedup_enchantedBook", Type.getDescriptor(ItemStack.class), null, firstLabel, lastLabel, enchantedBookItemLocal)); + + // Write null to deduplicated locals in order to proof our provided ranges + method.instructions.insert(firstLabel, new VarInsnNode(Opcodes.ASTORE, enchantedBookItemLocal)); + method.instructions.insert(firstLabel, new InsnNode(Opcodes.ACONST_NULL)); + method.instructions.insert(firstLabel, new VarInsnNode(Opcodes.ASTORE, disenchantedItemLocal)); + method.instructions.insert(firstLabel, new InsnNode(Opcodes.ACONST_NULL)); + + LabelNode postTransferEnchs = new LabelNode(); + method.instructions.insert(transferEnchantmentsCall, postTransferEnchs); + + InsnList noVanilla = new InsnList(); + noVanilla.add(new VarInsnNode(Opcodes.ALOAD, 1)); // ALOAD menu + noVanilla.add(new VarInsnNode(Opcodes.ALOAD, 2)); // ALOAD item + noVanilla.add(new MethodInsnNode(Opcodes.INVOKESTATIC, SlimefunCallbacks.INTERNAL_NAME, "autoDisenchanter$noVanillaEnchantments", "(Lme/mrCookieSlime/Slimefun/api/inventory/BlockMenu;Lorg/bukkit/inventory/ItemStack;)[Ljava/lang/Object;")); + + // disenchatedItemResult + noVanilla.add(new InsnNode(Opcodes.DUP)); + noVanilla.add(new InsnNode(Opcodes.ICONST_0)); + noVanilla.add(new InsnNode(Opcodes.AALOAD)); + noVanilla.add(new TypeInsnNode(Opcodes.CHECKCAST, Type.getInternalName(ItemStack.class))); + noVanilla.add(new VarInsnNode(Opcodes.ASTORE, disenchantedItemLocal)); + noVanilla.add(new VarInsnNode(Opcodes.ALOAD, disenchantedItemLocal)); + noVanilla.add(new JumpInsnNode(Opcodes.IFNULL, ((JumpInsnNode) jumpReturnNull).label)); + + // enchatedBookResult + noVanilla.add(new InsnNode(Opcodes.DUP)); + noVanilla.add(new InsnNode(Opcodes.ICONST_1)); + noVanilla.add(new InsnNode(Opcodes.AALOAD)); + noVanilla.add(new TypeInsnNode(Opcodes.CHECKCAST, Type.getInternalName(ItemStack.class))); + noVanilla.add(new VarInsnNode(Opcodes.ASTORE, enchantedBookItemLocal)); + + // enchantmentCountResult + noVanilla.add(new InsnNode(Opcodes.ICONST_2)); + noVanilla.add(new InsnNode(Opcodes.AALOAD)); + noVanilla.add(new TypeInsnNode(Opcodes.CHECKCAST, "java/lang/Integer")); + noVanilla.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I")); + noVanilla.add(new VarInsnNode(Opcodes.ISTORE, enchCountLocal)); + noVanilla.add(new JumpInsnNode(Opcodes.GOTO, postTransferEnchs)); + + method.instructions.insert(replacementJump, noVanilla); + + method.instructions.insert(sizeCall, new InsnNode(Opcodes.IADD)); + method.instructions.insert(sizeCall, new VarInsnNode(Opcodes.ILOAD, enchCountLocal)); + + InsnList vanilla = new InsnList(); + vanilla.add(new VarInsnNode(Opcodes.ALOAD, 1)); // ALOAD menu + vanilla.add(new VarInsnNode(Opcodes.ALOAD, disenchantedItemLocal)); // ALOAD disenchantedItem + vanilla.add(new VarInsnNode(Opcodes.ALOAD, enchantedBookItemLocal)); // ALOAD enchantedBook + vanilla.add(new MethodInsnNode(Opcodes.INVOKESTATIC, SlimefunCallbacks.INTERNAL_NAME, "autoDisenchanter$vanillEnchs", "(Lme/mrCookieSlime/Slimefun/api/inventory/BlockMenu;Lorg/bukkit/inventory/ItemStack;Lorg/bukkit/inventory/ItemStack;)I")); + vanilla.add(new VarInsnNode(Opcodes.ISTORE, enchCountLocal)); // ISTORE enchCount + + method.instructions.insert(transferEnchantmentsCall, vanilla); + } + } + + ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES); + node.accept(writer); + try (FileOutputStream fos = new FileOutputStream(new File("AutoDisenchanter.class"))) { + fos.write(writer.toByteArray()); + } catch (IOException e) { + e.printStackTrace(); + } + return writer.toByteArray(); + } + + static byte[] transform(byte[] bytes, String clazzName) { + if (clazzName.equals("io/github/thebusybiscuit/slimefun4/implementation/items/electric/machines/enchanting/AutoDisenchanter.class")) { + getLogger().info("[Hackloader] Transforming {}.", clazzName); + return transformAutoDisenchanter(bytes); + } + return bytes; + } + + private static ClassNode nodeFromClass(Class cl) throws IOException { + ClassNode node = new ClassNode(); + String fileName = cl.getName().replace('.', '/') + ".class"; + ClassLoader classlaoder = cl.getClassLoader(); + InputStream in = classlaoder.getResourceAsStream(fileName); + if (in == null) { + throw new IOException("Unable to get \"" + fileName + "\" from classloader \"" + classlaoder.getName() + "\""); + } + ClassReader reader = new ClassReader(in); + reader.accept(node, 0); + in.close(); + return node; + } + + private static Logger getLogger() { + return LoggerFactory.getLogger("Enchantments_plus"); + } +} diff --git a/src/main/java/de/geolykt/enchantments_plus/compatibility/hackloader/HackloaderInjectionException.java b/src/main/java/de/geolykt/enchantments_plus/compatibility/hackloader/HackloaderInjectionException.java new file mode 100644 index 0000000..cc10810 --- /dev/null +++ b/src/main/java/de/geolykt/enchantments_plus/compatibility/hackloader/HackloaderInjectionException.java @@ -0,0 +1,28 @@ +package de.geolykt.enchantments_plus.compatibility.hackloader; + +/** + * The {@link HackloaderInjectionException} is an exception that is thrown in the {@link Hackloader#injectHackloader()} + * method and may arise due to numerous reasons - for example due to restricted reflections. + * + * Either way it means that Hackloader cannot be used. + * + * @author Geolykt + * @since 4.1.10 + */ +public class HackloaderInjectionException extends Exception { + + /** + * serialVersionUID. + * + * @since 4.1.0 + */ + private static final long serialVersionUID = -6103274629602795105L; + + HackloaderInjectionException(String message, Throwable cause) { + super(message, cause); + } + + HackloaderInjectionException(String message) { + super(message); + } +} diff --git a/src/main/java/de/geolykt/enchantments_plus/compatibility/hackloader/SlimefunCallbacks.java b/src/main/java/de/geolykt/enchantments_plus/compatibility/hackloader/SlimefunCallbacks.java new file mode 100644 index 0000000..d25aaf7 --- /dev/null +++ b/src/main/java/de/geolykt/enchantments_plus/compatibility/hackloader/SlimefunCallbacks.java @@ -0,0 +1,59 @@ +package de.geolykt.enchantments_plus.compatibility.hackloader; + +import java.util.LinkedHashMap; + +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import de.geolykt.enchantments_plus.CustomEnchantment; + +import me.mrCookieSlime.Slimefun.api.inventory.BlockMenu; + +/** + * Class that contains callbacks that are called from transformed bytecode. + * These callbacks are entirely focused around the slimefun integration. + */ +public class SlimefunCallbacks { + + public static final String INTERNAL_NAME = "de/geolykt/enchantments_plus/compatibility/hackloader/SlimefunCallbacks"; + + public static int autoDisenchanter$vanillEnchs(@NotNull BlockMenu menu, @NotNull ItemStack disenchantedItem, @NotNull ItemStack enchantedBook) { + World world = menu.getLocation().getWorld(); + + LinkedHashMap enchs = CustomEnchantment.getEnchants(disenchantedItem, world, null); + + if (enchs.isEmpty()) { + return 0; // Nothing to do + } + + enchs.forEach((ench, level) -> { + CustomEnchantment.setEnchantment(enchantedBook, ench, level, world); + CustomEnchantment.setEnchantment(disenchantedItem, ench, 0, world); + }); + + return enchs.size(); + } + + public static @Nullable Object @NotNull[] autoDisenchanter$noVanillaEnchantments(@NotNull BlockMenu menu, @NotNull ItemStack original) { + World world = menu.getLocation().getWorld(); + LinkedHashMap enchs = CustomEnchantment.getEnchants(original, world, null); + + if (enchs.isEmpty()) { + return new Object[]{null, null, 0}; + } + + ItemStack disenchantedItem = original.clone(); + disenchantedItem.setAmount(1); // One item at a time + ItemStack bookItem = new ItemStack(Material.ENCHANTED_BOOK); + + enchs.forEach((ench, level) -> { + CustomEnchantment.setEnchantment(bookItem, ench, level, world); + CustomEnchantment.setEnchantment(disenchantedItem, ench, 0, world); + }); + + return new Object[]{disenchantedItem, bookItem, enchs.size()}; + } +} diff --git a/src/main/java/de/geolykt/enchantments_plus/compatibility/hackloader/package-info.java b/src/main/java/de/geolykt/enchantments_plus/compatibility/hackloader/package-info.java new file mode 100644 index 0000000..bb4494a --- /dev/null +++ b/src/main/java/de/geolykt/enchantments_plus/compatibility/hackloader/package-info.java @@ -0,0 +1,8 @@ +/** + * There are instances where supporting foreign plugins as-is is impossible as they lack proper API. + * While there are plugins that accept PRs from outsiders, there are various plugins where it either + * is outright not impossible or where it takes a very long time until a PR gets accepted. + * To remedy this issue enchantmentsPlus uses the nuclear option of cross-plugin compatibility: + * The Hackloader. + */ +package de.geolykt.enchantments_plus.compatibility.hackloader; diff --git a/src/main/java/de/geolykt/starloader/deobf/DescString.java b/src/main/java/de/geolykt/starloader/deobf/DescString.java new file mode 100644 index 0000000..e8860d1 --- /dev/null +++ b/src/main/java/de/geolykt/starloader/deobf/DescString.java @@ -0,0 +1,64 @@ +package de.geolykt.starloader.deobf; + +/** + * Utility for dissecting a method descriptor string. + */ +public class DescString { + + private char[] asArray; + private final String desc; + private int startIndex = 0; + + public DescString(String desc) { + int begin = 1; // Always starts with a paranthesis + int end = desc.lastIndexOf(')'); + this.desc = desc.substring(begin, end); + } + + public boolean hasNext() { + return desc.length() != startIndex; + } + + public String nextType() { + char type = desc.charAt(startIndex); + if (type == 'L') { + // Object-type type + // the description ends with a semicolon here, which has to be kept + int endPos = desc.indexOf(';', startIndex) + 1; + String ret = desc.substring(startIndex, endPos); + startIndex = endPos; + return ret; + } else if (type == '[') { + // array-type type - things will go spicy + if (asArray == null) { + asArray = desc.toCharArray(); + } + int typePosition = -1; + for (int i = startIndex + 1; i < asArray.length; i++) { + if (asArray[i] != '[') { + typePosition = i; + break; + } + } + if (asArray[typePosition] == 'L') { + int endPos = desc.indexOf(';', startIndex) + 1; + String ret = desc.substring(startIndex, endPos); + startIndex = endPos; + return ret; + } else { + typePosition++; + String ret = desc.substring(startIndex, typePosition); + startIndex = typePosition; + return ret; + } + } else { + // Primitive-type type + startIndex++; // Increment index by one, since the size of the type is exactly one + return Character.toString(type); + } + } + + public void reset() { + startIndex = 0; + } +}