From 4be582ef8793bc6aeae7ecafd13544dacd05717a Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Sat, 16 Nov 2013 03:04:00 +0100 Subject: [PATCH] Use the MCPC JAR remapper when loading classes. Fixes #11 This will allow plugins to use MinecraftReflection.getMinecraftClass() in both CraftBukkit and MCPC. --- .../protocol/utility/CachedPackage.java | 28 ++-- .../protocol/utility/ClassSource.java | 37 +++++ .../protocol/utility/MinecraftReflection.java | 89 ++++++++++- .../protocol/utility/RemappedClassSource.java | 147 ++++++++++++++++++ 4 files changed, 283 insertions(+), 18 deletions(-) create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/utility/ClassSource.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/utility/RemappedClassSource.java diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/CachedPackage.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/CachedPackage.java index a629ba1b..8f8e1b49 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/CachedPackage.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/CachedPackage.java @@ -19,6 +19,7 @@ package com.comphenix.protocol.utility; import java.util.Map; +import com.google.common.base.Strings; import com.google.common.collect.Maps; /** @@ -27,12 +28,19 @@ import com.google.common.collect.Maps; * @author Kristian */ class CachedPackage { - private Map> cache; - private String packageName; + private final Map> cache; + private final String packageName; + private final ClassSource source; - public CachedPackage(String packageName) { + /** + * Construct a new cached package. + * @param packageName - the name of the current package. + * @param source - the class source. + */ + public CachedPackage(String packageName, ClassSource source) { this.packageName = packageName; this.cache = Maps.newConcurrentMap(); + this.source = source; } /** @@ -57,13 +65,11 @@ class CachedPackage { // Concurrency is not a problem - we don't care if we look up a class twice if (result == null) { // Look up the class dynamically - result = CachedPackage.class.getClassLoader(). - loadClass(combine(packageName, className)); + result = source.loadClass(combine(packageName, className)); cache.put(className, result); - } - + } return result; - + } catch (ClassNotFoundException e) { throw new RuntimeException("Cannot find class " + className, e); } @@ -75,9 +81,11 @@ class CachedPackage { * @param className - the class name. * @return We full class path. */ - private String combine(String packageName, String className) { - if (packageName.length() == 0) + public static String combine(String packageName, String className) { + if (Strings.isNullOrEmpty(packageName)) return className; + if (Strings.isNullOrEmpty(className)) + return packageName; return packageName + "." + className; } } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/ClassSource.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/ClassSource.java new file mode 100644 index 00000000..a9582f72 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/ClassSource.java @@ -0,0 +1,37 @@ +package com.comphenix.protocol.utility; + +/** + * Represents an abstract class loader that can only retrieve classes by their canonical name. + * @author Kristian + */ +abstract class ClassSource { + /** + * Construct a class source from the current class loader. + * @return A package source. + */ + public static ClassSource fromClassLoader() { + return fromClassLoader(ClassSource.class.getClassLoader()); + } + + /** + * Construct a class source from the given class loader. + * @param loader - the class loader. + * @return The corresponding package source. + */ + public static ClassSource fromClassLoader(final ClassLoader loader) { + return new ClassSource() { + @Override + public Class loadClass(String canonicalName) throws ClassNotFoundException { + return loader.loadClass(canonicalName); + } + }; + } + + /** + * Retrieve a class by name. + * @param canonicalName - the full canonical name of the class. + * @return The corresponding class + * @throws ClassNotFoundException If the class could not be found. + */ + public abstract Class loadClass(String canonicalName) throws ClassNotFoundException; +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java index 71f80a1a..957e91bf 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -30,6 +30,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nonnull; @@ -42,6 +43,10 @@ import org.bukkit.Bukkit; import org.bukkit.Server; import org.bukkit.inventory.ItemStack; +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; import com.comphenix.protocol.injector.BukkitUnwrapper; import com.comphenix.protocol.injector.packet.PacketRegistry; import com.comphenix.protocol.reflect.FuzzyReflection; @@ -52,6 +57,8 @@ import com.comphenix.protocol.reflect.fuzzy.FuzzyClassContract; import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract; import com.comphenix.protocol.reflect.fuzzy.FuzzyMatchers; import com.comphenix.protocol.reflect.fuzzy.FuzzyMethodContract; +import com.comphenix.protocol.utility.RemappedClassSource.RemapperUnavaibleException; +import com.comphenix.protocol.utility.RemappedClassSource.RemapperUnavaibleException.Reason; import com.comphenix.protocol.wrappers.WrappedDataWatcher; import com.comphenix.protocol.wrappers.nbt.NbtFactory; import com.comphenix.protocol.wrappers.nbt.NbtType; @@ -63,6 +70,9 @@ import com.google.common.base.Joiner; * @author Kristian */ public class MinecraftReflection { + public static final ReportType REPORT_CANNOT_FIND_MCPC_REMAPPER = new ReportType("Cannot find MCPC remapper."); + public static final ReportType REPORT_CANNOT_LOAD_CPC_REMAPPER = new ReportType("Unable to load MCPC remapper."); + /** * Regular expression that matches a Minecraft object. *

@@ -76,11 +86,22 @@ public class MinecraftReflection { */ private static String DYNAMIC_PACKAGE_MATCHER = null; + /** + * The Entity package in Forge 1.5.2 + */ + private static final String FORGE_ENTITY_PACKAGE = "net.minecraft.entity"; + /** * The package name of all the classes that belongs to the native code in Minecraft. */ private static String MINECRAFT_PREFIX_PACKAGE = "net.minecraft.server"; + /** + * Represents a regular expression that will match the version string in a package: + * org.bukkit.craftbukkit.v1_6_R2 -> v1_6_R2 + */ + private static final Pattern PACKAGE_VERSION_MATCHER = Pattern.compile(".*\\.(v\\d+_\\d+_\\w*\\d+)"); + private static String MINECRAFT_FULL_PACKAGE = null; private static String CRAFTBUKKIT_PACKAGE = null; @@ -99,9 +120,15 @@ public class MinecraftReflection { private static Method craftBukkitMethod; private static boolean craftItemStackFailed; + // The NMS version + private static String packageVersion; + // net.minecraft.server private static Class itemStackArrayClass; + // The current class source + private static ClassSource classSource; + /** * Whether or not we're currently initializing the reflection handler. */ @@ -152,6 +179,12 @@ public class MinecraftReflection { Class craftClass = craftServer.getClass(); CRAFTBUKKIT_PACKAGE = getPackage(craftClass.getCanonicalName()); + // Parse the package version + Matcher packageMatcher = PACKAGE_VERSION_MATCHER.matcher(CRAFTBUKKIT_PACKAGE); + if (packageMatcher.matches()) { + packageVersion = packageMatcher.group(1); + } + // Libigot patch handleLibigot(); @@ -161,12 +194,18 @@ public class MinecraftReflection { MINECRAFT_FULL_PACKAGE = getPackage(getHandle.getReturnType().getCanonicalName()); - // Pretty important invariant + // Pretty important invariantt if (!MINECRAFT_FULL_PACKAGE.startsWith(MINECRAFT_PREFIX_PACKAGE)) { - // Assume they're the same instead - MINECRAFT_PREFIX_PACKAGE = MINECRAFT_FULL_PACKAGE; - - // The package is usualy flat, so go with that assumtion + // See if we got the Forge entity package + if (MINECRAFT_FULL_PACKAGE.equals(FORGE_ENTITY_PACKAGE)) { + // USe the standard NMS versioned package + MINECRAFT_FULL_PACKAGE = CachedPackage.combine(MINECRAFT_PREFIX_PACKAGE, packageVersion); + } else { + // Assume they're the same instead + MINECRAFT_PREFIX_PACKAGE = MINECRAFT_FULL_PACKAGE; + } + + // The package is usualy flat, so go with that assumption String matcher = (MINECRAFT_PREFIX_PACKAGE.length() > 0 ? Pattern.quote(MINECRAFT_PREFIX_PACKAGE + ".") : "") + "\\w+"; @@ -195,6 +234,15 @@ public class MinecraftReflection { } } + /** + * Retrieve the package version of the underlying CraftBukkit server. + * @return The package version, or NULL if not applicable (before 1.4.6). + */ + public static String getPackageVersion() { + getMinecraftPackage(); + return packageVersion; + } + /** * Update the dynamic package matcher. * @param regex - the Minecraft package regex. @@ -1260,7 +1308,7 @@ public class MinecraftReflection { @SuppressWarnings("rawtypes") public static Class getCraftBukkitClass(String className) { if (craftbukkitPackage == null) - craftbukkitPackage = new CachedPackage(getCraftBukkitPackage()); + craftbukkitPackage = new CachedPackage(getCraftBukkitPackage(), getClassSource()); return craftbukkitPackage.getPackageClass(className); } @@ -1272,7 +1320,7 @@ public class MinecraftReflection { */ public static Class getMinecraftClass(String className) { if (minecraftPackage == null) - minecraftPackage = new CachedPackage(getMinecraftPackage()); + minecraftPackage = new CachedPackage(getMinecraftPackage(), getClassSource()); return minecraftPackage.getPackageClass(className); } @@ -1284,11 +1332,36 @@ public class MinecraftReflection { */ private static Class setMinecraftClass(String className, Class clazz) { if (minecraftPackage == null) - minecraftPackage = new CachedPackage(getMinecraftPackage()); + minecraftPackage = new CachedPackage(getMinecraftPackage(), getClassSource()); minecraftPackage.setPackageClass(className, clazz); return clazz; } + /** + * Retrieve the current class source. + * @return The class source. + */ + private static ClassSource getClassSource() { + ErrorReporter reporter = ProtocolLibrary.getErrorReporter(); + + // Lazy pattern again + if (classSource == null) { + // Attempt to use MCPC + try { + return classSource = new RemappedClassSource().initialize(); + } catch (RemapperUnavaibleException e) { + if (e.getReason() != Reason.MCPC_NOT_PRESENT) + reporter.reportWarning(MinecraftReflection.class, Report.newBuilder(REPORT_CANNOT_FIND_MCPC_REMAPPER)); + } catch (Exception e) { + reporter.reportWarning(MinecraftReflection.class, Report.newBuilder(REPORT_CANNOT_LOAD_CPC_REMAPPER)); + } + + // Just use the default class loader + classSource = ClassSource.fromClassLoader(); + } + return classSource; + } + /** * Retrieve the first class that matches a specified Minecraft name. * @param className - the specific Minecraft class. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/RemappedClassSource.java b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/RemappedClassSource.java new file mode 100644 index 00000000..1672883b --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/RemappedClassSource.java @@ -0,0 +1,147 @@ +package com.comphenix.protocol.utility; + +// Thanks to Bergerkiller for his excellent hack. :D + +// Copyright (C) 2013 bergerkiller +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +// Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import java.lang.reflect.Method; +import org.bukkit.Bukkit; + +import com.comphenix.protocol.reflect.FieldUtils; +import com.comphenix.protocol.reflect.MethodUtils; +import com.comphenix.protocol.utility.RemappedClassSource.RemapperUnavaibleException.Reason; + +class RemappedClassSource extends ClassSource { + private Object classRemapper; + private Method mapType; + private ClassLoader loader; + + /** + * Construct a new remapped class source using the default class loader. + */ + public RemappedClassSource() { + this(RemappedClassSource.class.getClassLoader()); + } + + /** + * Construct a new renampped class source with the provided class loader. + * @param loader - the class loader. + */ + public RemappedClassSource(ClassLoader loader) { + this.loader = loader; + } + + /** + * Attempt to load the MCPC remapper. + * @return TRUE if we succeeded, FALSE otherwise. + * @throws RemapperUnavaibleException If the remapper is not present. + */ + public RemappedClassSource initialize() { + try { + if (Bukkit.getServer() == null || !Bukkit.getServer().getVersion().contains("MCPC-Plus")) { + throw new RemapperUnavaibleException(Reason.MCPC_NOT_PRESENT); + } + + // Obtain the Class remapper used by MCPC+ + this.classRemapper = FieldUtils.readField(getClass().getClassLoader(), "remapper", true); + + if (this.classRemapper == null) { + throw new RemapperUnavaibleException(Reason.REMAPPER_DISABLED); + } + + // Initialize some fields and methods used by the Jar Remapper + Class renamerClazz = classRemapper.getClass(); + + this.mapType = MethodUtils.getAccessibleMethod(renamerClazz, "map", + new Class[] { String.class }); + + return this; + + } catch (RemapperUnavaibleException e) { + throw e; + } catch (Exception e) { + // Damn it + throw new RuntimeException("Cannot access MCPC remapper.", e); + } + } + + @Override + public Class loadClass(String canonicalName) throws ClassNotFoundException { + final String remapped = getClassName(canonicalName); + + try { + return loader.loadClass(remapped); + } catch (ClassNotFoundException e) { + throw new ClassNotFoundException("Cannot find " + canonicalName + "(Remapped: " + remapped + ")"); + } + } + + /** + * Retrieve the obfuscated class name given an unobfuscated canonical class name. + * @param path - the canonical class name. + * @return The obfuscated class name. + */ + private String getClassName(String path) { + try { + String remapped = (String) mapType.invoke(classRemapper, path.replace('.', '/')); + return remapped.replace('/', '.'); + } catch (Exception e) { + throw new RuntimeException("Cannot remap class name.", e); + } + } + + public static class RemapperUnavaibleException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public enum Reason { + MCPC_NOT_PRESENT("The server is not running MCPC+"), + REMAPPER_DISABLED("Running an MCPC+ server but the remapper is unavailable. Please turn it on!"); + + private final String message; + + private Reason(String message) { + this.message = message; + } + + /** + * Retrieve a human-readable version of this reason. + * @return The human-readable verison. + */ + public String getMessage() { + return message; + } + } + + private final Reason reason; + + public RemapperUnavaibleException(Reason reason) { + super(reason.getMessage()); + this.reason = reason; + } + + /** + * Retrieve the reasont he remapper is unavailable. + * @return The reason. + */ + public Reason getReason() { + return reason; + } + } +}