+ * Note that we don't check if the underlying field has changed after the value has been cached, + * so it's best to use this on final fields. + * @param inner - the accessor. + * @return A cached field accessor. + */ + public static FieldAccessor getCached(final FieldAccessor inner) { + return new FieldAccessor() { + private final Object EMPTY = new Object(); + private volatile Object value = EMPTY; + + @Override + public void set(Object instance, Object value) { + inner.set(instance, value); + update(value); + } + + @Override + public Object get(Object instance) { + Object cache = value; + + if (cache != EMPTY) + return cache; + return update(inner.get(instance)); + } + + /** + * Update the cached value. + * @param value - the value to cache. + * @return The cached value. + */ + private Object update(Object value) { + return this.value = value; + } + + @Override + public Field getField() { + return inner.getField(); + } + }; + } /** * Retrieve a field accessor where the write operation is synchronized on the current field value. 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 569695bb..da5996e4 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -78,6 +78,7 @@ import com.google.common.base.Joiner; 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."); + public static final ReportType REPORT_NON_CRAFTBUKKIT_LIBRARY_PACKAGE = new ReportType("Cannot find standard Minecraft library location. Assuming MCPC."); /** * Regular expression that matches a Minecraft object. @@ -101,7 +102,12 @@ public class MinecraftReflection { * The package name of all the classes that belongs to the native code in Minecraft. */ private static String MINECRAFT_PREFIX_PACKAGE = "net.minecraft.server"; - + + /** + * The package with all the library classes. + */ + private static String MINECRAFT_LIBRARY_PACKAGE = "net.minecraft.util"; + /** * Represents a regular expression that will match the version string in a package: * org.bukkit.craftbukkit.v1_6_R2 -> v1_6_R2 @@ -114,6 +120,7 @@ public class MinecraftReflection { // Package private for the purpose of unit testing static CachedPackage minecraftPackage; static CachedPackage craftbukkitPackage; + static CachedPackage libraryPackage; // org.bukkit.craftbukkit private static Constructor> craftNMSConstructor; @@ -199,6 +206,9 @@ public class MinecraftReflection { // Libigot patch handleLibigot(); + // Minecraft library package + handleLibraryPackage(); + // Next, do the same for CraftEntity.getHandle() in order to get the correct Minecraft package Class> craftEntity = getCraftEntityClass(); Method getHandle = craftEntity.getMethod("getHandle"); @@ -244,7 +254,30 @@ public class MinecraftReflection { throw new IllegalStateException("Could not find Bukkit. Is it running?"); } } + + /** + * Retrieve the Minecraft library package string. + * @return The library package. + */ + private static String getMinecraftLibraryPackage() { + getMinecraftPackage(); + return MINECRAFT_LIBRARY_PACKAGE; + } + private static void handleLibraryPackage() { + try { + MINECRAFT_LIBRARY_PACKAGE = "net.minecraft.util"; + // Try loading Google GSON + getClassSource().loadClass(CachedPackage.combine(MINECRAFT_LIBRARY_PACKAGE, "com.google.gson.Gson")); + + } catch (Exception e) { + // Assume it's MCPC + MINECRAFT_LIBRARY_PACKAGE = ""; + ProtocolLibrary.getErrorReporter().reportWarning(MinecraftReflection.class, + Report.newBuilder(REPORT_NON_CRAFTBUKKIT_LIBRARY_PACKAGE)); + } + } + /** * Retrieve the package version of the underlying CraftBukkit server. * @return The package version, or NULL if not applicable (before 1.4.6). @@ -1552,6 +1585,20 @@ public class MinecraftReflection { } } + /** + * Retrieve the google.gson.Gson class used by Minecraft. + * @return The GSON class. + */ + public static Class> getMinecraftGsonClass() { + try { + return getMinecraftLibraryClass("com.google.gson.Gson"); + } catch (RuntimeException e) { + Class> match = FuzzyReflection.fromClass(PacketType.Status.Server.OUT_SERVER_INFO.getPacketClass()). + getFieldByType(".*\\.google\\.gson\\.Gson").getType(); + return setMinecraftLibraryClass("com.google.gson.Gson", match); + } + } + /** * Determine if a given method retrieved by ASM is a constructor. * @param name - the name of the method. @@ -1766,6 +1813,31 @@ public class MinecraftReflection { return minecraftPackage.getPackageClass(className); } + /** + * Retrieve the class object of a specific Minecraft library class. + * @param className - the specific library Minecraft class. + * @return Class object. + * @throws RuntimeException If we are unable to find the given class. + */ + public static Class> getMinecraftLibraryClass(String className) { + if (libraryPackage == null) + libraryPackage = new CachedPackage(getMinecraftLibraryPackage(), getClassSource()); + return libraryPackage.getPackageClass(className); + } + + /** + * Set the class object for the specific library class. + * @param className - name of the Minecraft library class. + * @param clazz - the new class object. + * @return The provided clazz object. + */ + private static Class> setMinecraftLibraryClass(String className, Class> clazz) { + if (libraryPackage == null) + libraryPackage = new CachedPackage(getMinecraftLibraryPackage(), getClassSource()); + libraryPackage.setPackageClass(className, clazz); + return clazz; + } + /** * Set the class object for the specific Minecraft class. * @param className - name of the Minecraft class. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedServerPing.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedServerPing.java index a5283a6c..44cbfb13 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedServerPing.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedServerPing.java @@ -20,11 +20,13 @@ import net.minecraft.util.io.netty.buffer.Unpooled; import net.minecraft.util.io.netty.handler.codec.base64.Base64; import net.minecraft.util.io.netty.util.IllegalReferenceCountException; +import com.comphenix.protocol.PacketType; import com.comphenix.protocol.injector.BukkitUnwrapper; import com.comphenix.protocol.reflect.EquivalentConverter; import com.comphenix.protocol.reflect.accessors.Accessors; import com.comphenix.protocol.reflect.accessors.ConstructorAccessor; import com.comphenix.protocol.reflect.accessors.FieldAccessor; +import com.comphenix.protocol.reflect.accessors.MethodAccessor; import com.comphenix.protocol.utility.MinecraftReflection; import com.comphenix.protocol.utility.MinecraftVersion; import com.google.common.base.Charsets; @@ -59,6 +61,14 @@ public class WrappedServerPing extends AbstractWrapper { private static FieldAccessor PLAYERS_MAXIMUM = PLAYERS_INTS[0]; private static FieldAccessor PLAYERS_ONLINE = PLAYERS_INTS[1]; + // Server ping serialization + private static Class> GSON_CLASS = MinecraftReflection.getMinecraftGsonClass(); + private static MethodAccessor GSON_TO_JSON = Accessors.getMethodAccessor(GSON_CLASS, "toJson", Object.class); + private static MethodAccessor GSON_FROM_JSON = Accessors.getMethodAccessor(GSON_CLASS, "fromJson", String.class, Class.class); + private static FieldAccessor PING_GSON = Accessors.getCached(Accessors.getFieldAccessor( + PacketType.Status.Server.OUT_SERVER_INFO.getPacketClass(), GSON_CLASS, true + )); + // Server data fields private static Class> VERSION_CLASS = MinecraftReflection.getServerPingServerDataClass(); private static ConstructorAccessor VERSION_CONSTRUCTOR = Accessors.getConstructorAccessor(VERSION_CLASS, String.class, int.class); @@ -117,6 +127,15 @@ public class WrappedServerPing extends AbstractWrapper { return new WrappedServerPing(handle); } + /** + * Construct a wrapper server ping from an encoded JSON string. + * @param json - the JSON string. + * @return The wrapped server ping. + */ + public static WrappedServerPing fromJson(String json) { + return fromHandle(GSON_FROM_JSON.invoke(PING_GSON.get(null), json, SERVER_PING)); + } + /** * Retrieve the message of the day. * @return The messge of the day. @@ -329,6 +348,14 @@ public class WrappedServerPing extends AbstractWrapper { return copy; } + /** + * Retrieve the underlying JSON representation of this server ping. + * @return The JSON representation. + */ + public String toJson() { + return (String) GSON_TO_JSON.invoke(PING_GSON.get(null), handle); + } + /** * Represents a compressed favicon. * @author Kristian