Use the MCPC JAR remapper when loading classes. Fixes #11

This will allow plugins to use MinecraftReflection.getMinecraftClass() 
in both CraftBukkit and MCPC.
This commit is contained in:
Kristian S. Stangeland 2013-11-16 03:04:00 +01:00
parent 8c8ca3746b
commit 4be582ef87
4 changed files with 283 additions and 18 deletions

View File

@ -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<String, Class<?>> cache;
private String packageName;
private final Map<String, Class<?>> 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,11 +65,9 @@ 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) {
@ -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;
}
}

View File

@ -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;
}

View File

@ -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.
* <p>
@ -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;
// 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 assumtion
// 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.

View File

@ -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;
}
}
}