From bdc739317bcb20458c5e2f8b72c611c50a0c330e Mon Sep 17 00:00:00 2001 From: "Kristian S. Stangeland" Date: Mon, 9 Dec 2013 23:09:08 +0100 Subject: [PATCH] Added a wrapper for ServerPing fields. --- ProtocolLib/.classpath | 1 + .../protocol/events/PacketContainer.java | 18 +- .../protocol/reflect/accessors/Accessors.java | 51 ++- .../accessors/ConstructorAccessor.java | 18 + .../accessors/DefaultConstrutorAccessor.java | 54 +++ .../reflect/fuzzy/FuzzyFieldContract.java | 9 + .../reflect/fuzzy/FuzzyMethodContract.java | 14 + .../protocol/utility/MinecraftReflection.java | 90 ++++- .../protocol/wrappers/BukkitConverters.java | 80 +++- .../wrappers/WrappedChatComponent.java | 6 +- .../protocol/wrappers/WrappedServerPing.java | 347 ++++++++++++++++++ .../utility/MinecraftReflectionTest.java | 22 +- .../wrappers/WrappedServerPingTest.java | 41 +++ 13 files changed, 739 insertions(+), 12 deletions(-) create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/reflect/accessors/ConstructorAccessor.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/reflect/accessors/DefaultConstrutorAccessor.java create mode 100644 ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedServerPing.java create mode 100644 ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/WrappedServerPingTest.java diff --git a/ProtocolLib/.classpath b/ProtocolLib/.classpath index e842aee4..b3408dbb 100644 --- a/ProtocolLib/.classpath +++ b/ProtocolLib/.classpath @@ -12,6 +12,7 @@ + diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java index 9fcdea35..75b7266b 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/events/PacketContainer.java @@ -69,6 +69,7 @@ import com.comphenix.protocol.wrappers.WrappedAttribute; import com.comphenix.protocol.wrappers.WrappedChatComponent; import com.comphenix.protocol.wrappers.WrappedDataWatcher; import com.comphenix.protocol.wrappers.WrappedGameProfile; +import com.comphenix.protocol.wrappers.WrappedServerPing; import com.comphenix.protocol.wrappers.WrappedWatchableObject; import com.comphenix.protocol.wrappers.nbt.NbtBase; import com.google.common.base.Function; @@ -496,12 +497,25 @@ public class PacketContainer implements Serializable { *

* This modifier will automatically marshall between WrappedChatComponent and the * internal Minecraft GameProfile. - * @return A modifier for GameProfile fields. + * @return A modifier for ChatComponent fields. */ public StructureModifier getChatComponents() { // Convert to and from the Bukkit wrapper return structureModifier.withType( - MinecraftReflection.getIChatBaseComponent(), BukkitConverters.getWrappedChatComponentConverter()); + MinecraftReflection.getIChatBaseComponentClass(), BukkitConverters.getWrappedChatComponentConverter()); + } + + /** + * Retrieve a read/write structure for the ServerPing fields in the following packet:
+ *

    + *
  • {@link PacketType.Status.Server#OUT_SERVER_INFO} + *
+ * @return A modifier for ServerPing fields. + */ + public StructureModifier getServerPings() { + // Convert to and from the wrapper + return structureModifier.withType( + MinecraftReflection.getServerPingClass(), BukkitConverters.getWrappedServerPingConverter()); } /** diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/accessors/Accessors.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/accessors/Accessors.java index 937f060c..bf6dc470 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/accessors/Accessors.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/accessors/Accessors.java @@ -1,10 +1,13 @@ package com.comphenix.protocol.reflect.accessors; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.List; import com.comphenix.protocol.reflect.ExactReflection; import com.comphenix.protocol.reflect.FuzzyReflection; +import com.google.common.base.Joiner; public final class Accessors { /** @@ -46,7 +49,7 @@ public final class Accessors { * @param instanceClass - the type of the instance to retrieve. * @param fieldClass - type of the field to retrieve. * @param forceAccess - whether or not to look for private and protected fields. - * @return The value of that field. + * @return The field accessor. * @throws IllegalArgumentException If the field cannot be found. */ public static FieldAccessor getFieldAccessor(Class instanceClass, Class fieldClass, boolean forceAccess) { @@ -55,6 +58,23 @@ public final class Accessors { return Accessors.getFieldAccessor(field); } + /** + * Retrieve an accessor (in declared order) for every field of the givne type. + * @param instanceClass - the type of the instance to retrieve. + * @param fieldClass - type of the field(s) to retrieve. + * @param forceAccess - whether or not to look for private and protected fields. + * @return The accessors. + */ + public static FieldAccessor[] getFieldAccessorArray(Class instanceClass, Class fieldClass, boolean forceAccess) { + List fields = FuzzyReflection.fromClass(instanceClass, forceAccess).getFieldListByType(fieldClass); + FieldAccessor[] accessors = new FieldAccessor[fields.size()]; + + for (int i = 0; i < accessors.length; i++) { + accessors[i] = getFieldAccessor(fields.get(i)); + } + return accessors; + } + /** * Retrieve an accessor for the first field of the given type. * @param instanceClass - the type of the instance to retrieve. @@ -98,7 +118,7 @@ public final class Accessors { return accessor; return new SynchronizedFieldAccessor(accessor); } - + /** * Retrieve a method accessor for a method with the given name and signature. * @param instanceClass - the parent class. @@ -119,6 +139,33 @@ public final class Accessors { return new DefaultMethodAccessor(method); } + /** + * Retrieve a constructor accessor for a constructor with the given signature. + * @param instanceClass - the parent class. + * @param parameters - the parameters. + * @return The constructor accessor. + */ + public static ConstructorAccessor getConstructorAccessor(Class instanceClass, Class... parameters) { + try { + return getConstructorAccessor(instanceClass.getDeclaredConstructor(parameters)); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException(String.format( + "Unable to find constructor %s(%s).", instanceClass, Joiner.on(",").join(parameters)) + ); + } catch (SecurityException e) { + throw new IllegalStateException("Cannot access constructors.", e); + } + } + + /** + * Retrieve a constructor accessor for a particular constructor, avoding checked exceptions. + * @param constructor - the constructor to access. + * @return The method accessor. + */ + public static ConstructorAccessor getConstructorAccessor(final Constructor constructor) { + return new DefaultConstrutorAccessor(constructor); + } + // Seal this class private Accessors() { } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/accessors/ConstructorAccessor.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/accessors/ConstructorAccessor.java new file mode 100644 index 00000000..15c5b3b1 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/accessors/ConstructorAccessor.java @@ -0,0 +1,18 @@ +package com.comphenix.protocol.reflect.accessors; + +import java.lang.reflect.Constructor; + +public interface ConstructorAccessor { + /** + * Invoke the underlying constructor. + * @param args - the arguments to pass to the method. + * @return The return value, or NULL for void methods. + */ + public Object invoke(Object... args); + + /** + * Retrieve the underlying constructor. + * @return The method. + */ + public Constructor getConstructor(); +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/accessors/DefaultConstrutorAccessor.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/accessors/DefaultConstrutorAccessor.java new file mode 100644 index 00000000..e27bca80 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/accessors/DefaultConstrutorAccessor.java @@ -0,0 +1,54 @@ +package com.comphenix.protocol.reflect.accessors; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +final class DefaultConstrutorAccessor implements ConstructorAccessor { + private final Constructor constructor; + + public DefaultConstrutorAccessor(Constructor method) { + this.constructor = method; + } + + @Override + public Object invoke(Object... args) { + try { + return constructor.newInstance(args); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Cannot use reflection.", e); + } catch (IllegalArgumentException e) { + throw e; + } catch (InvocationTargetException e) { + throw new RuntimeException("An internal error occured.", e.getCause()); + } catch (InstantiationException e) { + throw new RuntimeException("Cannot instantiate object.", e); + } + } + + @Override + public Constructor getConstructor() { + return constructor; + } + + @Override + public int hashCode() { + return constructor != null ? constructor.hashCode() : 0; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + + if (obj instanceof DefaultConstrutorAccessor) { + DefaultConstrutorAccessor other = (DefaultConstrutorAccessor) obj; + return other.constructor == constructor; + } + return true; + } + + @Override + public String toString() { + return "DefaultConstrutorAccessor [constructor=" + constructor + "]"; + } +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyFieldContract.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyFieldContract.java index 1b0c7b5a..680c1169 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyFieldContract.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyFieldContract.java @@ -114,6 +114,15 @@ public class FuzzyFieldContract extends AbstractFuzzyMember { } } + /** + * Match a field by its type. + * @param matcher - the type to match. + * @return The field contract. + */ + public static FuzzyFieldContract matchType(AbstractFuzzyMatcher> matcher) { + return newBuilder().typeMatches(matcher).build(); + } + /** * Return a new fuzzy field contract builder. * @return New fuzzy field contract builder. diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyMethodContract.java b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyMethodContract.java index 54b50f7a..79f0eed4 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyMethodContract.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/reflect/fuzzy/FuzzyMethodContract.java @@ -207,6 +207,20 @@ public class FuzzyMethodContract extends AbstractFuzzyMember { return this; } + /** + * Add a new required parameters by type and order for any matching method. + * @param type - the types of every parameters in order. + * @return This builder, for chaining. + */ + public Builder parameterExactArray(Class... types) { + parameterCount(types.length); + + for (int i = 0; i < types.length; i++) { + parameterExactType(types[i], i); + } + return this; + } + /** * Add a new required parameter whose type must be a superclass of the given type. *

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 f8715b50..c8953280 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/utility/MinecraftReflection.java @@ -650,7 +650,7 @@ public class MinecraftReflection { * Retrieve the IChatBaseComponent class. * @return The IChatBaseComponent. */ - public static Class getIChatBaseComponent() { + public static Class getIChatBaseComponentClass() { try { return getMinecraftClass("IChatBaseComponent"); } catch (RuntimeException e) { @@ -666,7 +666,7 @@ public class MinecraftReflection { * @return The serializer class. * @throws IllegalStateException If the class could not be found or deduced. */ - public static Class getChatSerializer() { + public static Class getChatSerializerClass() { try { return getMinecraftClass("ChatSerializer"); } catch (RuntimeException e) { @@ -692,6 +692,92 @@ public class MinecraftReflection { throw new IllegalStateException("Cannot find ChatSerializer class."); } + /** + * Retrieve the ServerPing class in Minecraft 1.7.2. + * @return The ServerPing class. + */ + public static Class getServerPingClass() { + if (!isUsingNetty()) + throw new IllegalStateException("ServerPing is only supported in 1.7.2."); + + try { + return getMinecraftClass("ServerPing"); + } catch (RuntimeException e) { + Class statusServerInfo = PacketType.Status.Server.OUT_SERVER_INFO.getPacketClass(); + + // Find a server ping object + AbstractFuzzyMatcher> serverPingContract = FuzzyClassContract.newBuilder(). + field(FuzzyFieldContract.newBuilder().typeExact(String.class).build()). + field(FuzzyFieldContract.newBuilder().typeDerivedOf(getIChatBaseComponentClass()).build()). + build(). + and(getMinecraftObjectMatcher()); + + return setMinecraftClass("ServerPing", + FuzzyReflection.fromClass(statusServerInfo, true). + getField(FuzzyFieldContract.matchType(serverPingContract)).getType()); + } + } + + /** + * Retrieve the ServerPingServerData class in Minecraft 1.7.2. + * @return The ServerPingServerData class. + */ + public static Class getServerPingServerDataClass() { + if (!isUsingNetty()) + throw new IllegalStateException("ServerPingServerData is only supported in 1.7.2."); + + try { + return getMinecraftClass("ServerPingServerData"); + } catch (RuntimeException e) { + Class serverPing = getServerPingClass(); + + // Find a server ping object + AbstractFuzzyMatcher> serverDataContract = FuzzyClassContract.newBuilder(). + constructor(FuzzyMethodContract.newBuilder().parameterExactArray(String.class, int.class)). + build(). + and(getMinecraftObjectMatcher()); + + return setMinecraftClass("ServerPingServerData", getTypeFromField(serverPing, serverDataContract)); + } + } + + /** + * Retrieve the ServerPingPlayerSample class in Minecraft 1.7.2. + * @return The ServerPingPlayerSample class. + */ + public static Class getServerPingPlayerSampleClass() { + if (!isUsingNetty()) + throw new IllegalStateException("ServerPingPlayerSample is only supported in 1.7.2."); + + try { + return getMinecraftClass("ServerPingPlayerSample"); + } catch (RuntimeException e) { + Class serverPing = getServerPingClass(); + + // Find a server ping object + AbstractFuzzyMatcher> serverPlayerContract = FuzzyClassContract.newBuilder(). + constructor(FuzzyMethodContract.newBuilder().parameterExactArray(int.class, int.class)). + field(FuzzyFieldContract.newBuilder().typeExact(GameProfile[].class)). + build(). + and(getMinecraftObjectMatcher()); + + return setMinecraftClass("ServerPingPlayerSample", getTypeFromField(serverPing, serverPlayerContract)); + } + } + + /** + * Retrieve the type of the field whose type matches. + * @param clazz - the declaring type. + * @param fieldTypeMatcher - the field type matcher. + * @return The type of the field. + */ + private static Class getTypeFromField(Class clazz, AbstractFuzzyMatcher> fieldTypeMatcher) { + final FuzzyFieldContract fieldMatcher = FuzzyFieldContract.matchType(fieldTypeMatcher); + + return FuzzyReflection.fromClass(clazz, true). + getField(fieldMatcher).getType(); + } + /** * Determine if this Minecraft version is using Netty. *

diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java index d26fd7c5..0def87d1 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/BukkitConverters.java @@ -18,6 +18,7 @@ package com.comphenix.protocol.wrappers; import java.lang.ref.WeakReference; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.ArrayList; @@ -32,6 +33,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.potion.PotionEffect; import org.bukkit.potion.PotionEffectType; +import com.comphenix.protocol.PacketType; import com.comphenix.protocol.ProtocolLibrary; import com.comphenix.protocol.ProtocolManager; import com.comphenix.protocol.injector.PacketConstructor; @@ -48,6 +50,7 @@ import com.comphenix.protocol.wrappers.nbt.NbtFactory; import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; /** * Contains several useful equivalent converters for normal Bukkit types. @@ -234,6 +237,58 @@ public class BukkitConverters { }; } + /** + * Retrieve an equivalent converter for an array of generic items. + *

+ * The array is wrapped in a list. + * @param genericItemType - the generic item type. + * @param itemConverter - an equivalent converter for the generic type. + * @return An equivalent converter. + */ + public static EquivalentConverter> getArrayConverter( + final Class genericItemType, final EquivalentConverter itemConverter) { + // Convert to and from the wrapper + return new IgnoreNullConverter>() { + @Override + protected List getSpecificValue(Object generic) { + if (generic instanceof Object[]) { + ImmutableList.Builder builder = ImmutableList.builder(); + + // Copy everything to a new list + for (Object item : (Object[]) generic) { + T result = itemConverter.getSpecific(item); + builder.add(result); + } + return builder.build(); + } + + // Not valid + return null; + } + + @Override + protected Object getGenericValue(Class genericType, Iterable specific) { + List list = Lists.newArrayList(specific); + Object[] output = (Object[]) Array.newInstance(genericType, list.size()); + + // Convert each object + for (int i = 0; i < output.length; i++) { + Object converted = itemConverter.getGeneric(genericItemType, list.get(i)); + output[i] = converted; + } + return output; + } + + @SuppressWarnings("unchecked") + @Override + public Class> getSpecificType() { + // Damn you Java + Class dummy = Iterable.class; + return (Class>) dummy; + } + }; + } + /** * Retrieve a converter for wrapped attribute snapshots. * @return Wrapped attribute snapshot converter. @@ -506,6 +561,29 @@ public class BukkitConverters { }; } + /** + * Retrieve the converter for the ServerPing packet in {@link PacketType.Status.Server#OUT_SERVER_INFO}. + * @return Server ping converter. + */ + public static EquivalentConverter getWrappedServerPingConverter() { + return new IgnoreNullConverter() { + @Override + protected Object getGenericValue(Class genericType, WrappedServerPing specific) { + return specific.getHandle(); + } + + @Override + protected WrappedServerPing getSpecificValue(Object generic) { + return WrappedServerPing.fromHandle(generic); + } + + @Override + public Class getSpecificType() { + return WrappedServerPing.class; + } + }; + } + /** * Retrieve the converter used to convert between a PotionEffect and the equivalent NMS Mobeffect. * @return The potion effect converter. @@ -664,7 +742,7 @@ public class BukkitConverters { // Types added in 1.7.2 if (MinecraftReflection.isUsingNetty()) { builder.put(MinecraftReflection.getGameProfileClass(), (EquivalentConverter) getWrappedGameProfileConverter()); - builder.put(MinecraftReflection.getIChatBaseComponent(), (EquivalentConverter) getWrappedChatComponentConverter()); + builder.put(MinecraftReflection.getIChatBaseComponentClass(), (EquivalentConverter) getWrappedChatComponentConverter()); } genericConverters = builder.build(); } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedChatComponent.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedChatComponent.java index fcdc7300..2bfd732c 100644 --- a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedChatComponent.java +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedChatComponent.java @@ -12,8 +12,8 @@ import com.comphenix.protocol.utility.MinecraftReflection; * @author Kristian */ public class WrappedChatComponent extends AbstractWrapper { - private static final Class SERIALIZER = MinecraftReflection.getChatSerializer(); - private static final Class COMPONENT = MinecraftReflection.getIChatBaseComponent(); + private static final Class SERIALIZER = MinecraftReflection.getChatSerializerClass(); + private static final Class COMPONENT = MinecraftReflection.getIChatBaseComponentClass(); private static MethodAccessor SERIALIZE_COMPONENT = null; private static MethodAccessor DESERIALIZE_COMPONENT = null; private static MethodAccessor CONSTRUCT_COMPONENT = null; @@ -35,7 +35,7 @@ public class WrappedChatComponent extends AbstractWrapper { private transient String cache; private WrappedChatComponent(Object handle, String cache) { - super(MinecraftReflection.getIChatBaseComponent()); + super(MinecraftReflection.getIChatBaseComponentClass()); setHandle(handle); this.cache = cache; } diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedServerPing.java b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedServerPing.java new file mode 100644 index 00000000..7927c2cc --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedServerPing.java @@ -0,0 +1,347 @@ +package com.comphenix.protocol.wrappers; + +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.List; + +import javax.imageio.ImageIO; + +import org.bukkit.entity.Player; + +import net.minecraft.util.com.mojang.authlib.GameProfile; +import net.minecraft.util.io.netty.buffer.ByteBuf; +import net.minecraft.util.io.netty.buffer.Unpooled; +import net.minecraft.util.io.netty.handler.codec.base64.Base64; + +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.utility.MinecraftReflection; +import com.comphenix.protocol.utility.MinecraftVersion; +import com.google.common.base.Charsets; +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.io.ByteStreams; + +/** + * Represents a server ping packet data. + * @author Kristian + */ +public class WrappedServerPing extends AbstractWrapper { + // Server ping fields + private static Class SERVER_PING = MinecraftReflection.getServerPingClass(); + private static ConstructorAccessor SERVER_PING_CONSTRUCTOR = Accessors.getConstructorAccessor(SERVER_PING); + private static FieldAccessor DESCRIPTION = Accessors.getFieldAccessor(SERVER_PING, MinecraftReflection.getIChatBaseComponentClass(), true); + private static FieldAccessor PLAYERS = Accessors.getFieldAccessor(SERVER_PING, MinecraftReflection.getServerPingPlayerSampleClass(), true); + private static FieldAccessor VERSION = Accessors.getFieldAccessor(SERVER_PING, MinecraftReflection.getServerPingServerDataClass(), true); + private static FieldAccessor FAVICON = Accessors.getFieldAccessor(SERVER_PING, String.class, true); + + // For converting to the underlying array + private static EquivalentConverter> PROFILE_CONVERT = + BukkitConverters.getArrayConverter(GameProfile[].class, BukkitConverters.getWrappedGameProfileConverter()); + + // Server ping player sample fields + private static Class PLAYERS_CLASS = MinecraftReflection.getServerPingPlayerSampleClass(); + private static ConstructorAccessor PLAYERS_CONSTRUCTOR = Accessors.getConstructorAccessor(PLAYERS_CLASS, int.class, int.class); + private static FieldAccessor[] PLAYERS_INTS = Accessors.getFieldAccessorArray(PLAYERS_CLASS, int.class, true); + private static FieldAccessor PLAYERS_PROFILES = Accessors.getFieldAccessor(PLAYERS_CLASS, GameProfile[].class, true); + private static FieldAccessor PLAYERS_MAXIMUM = PLAYERS_INTS[0]; + private static FieldAccessor PLAYERS_ONLINE = PLAYERS_INTS[1]; + + // Server data fields + private static Class VERSION_CLASS = MinecraftReflection.getServerPingServerDataClass(); + private static ConstructorAccessor VERSION_CONSTRUCTOR = Accessors.getConstructorAccessor(VERSION_CLASS, String.class, int.class); + private static FieldAccessor VERSION_NAME = Accessors.getFieldAccessor(VERSION_CLASS, String.class, true); + private static FieldAccessor VERSION_PROTOCOL = Accessors.getFieldAccessor(VERSION_CLASS, int.class, true); + + // Get profile from player + private static FieldAccessor ENTITY_HUMAN_PROFILE = Accessors.getFieldAccessor( + MinecraftReflection.getEntityPlayerClass().getSuperclass(), GameProfile.class, true); + + // Inner class + private Object players; + private Object version; + + /** + * Construct a new server ping initialized with empty values. + */ + public WrappedServerPing() { + super(MinecraftReflection.getServerPingClass()); + setHandle(SERVER_PING_CONSTRUCTOR.invoke()); + this.players = PLAYERS_CONSTRUCTOR.invoke(0, 0); + this.version = VERSION_CONSTRUCTOR.invoke(MinecraftVersion.WORLD_UPDATE.toString(), 4); + PLAYERS.set(handle, players); + VERSION.set(handle, version); + } + + private WrappedServerPing(Object handle) { + super(MinecraftReflection.getServerPingClass()); + setHandle(handle); + this.players = PLAYERS.get(handle); + this.version = VERSION.get(handle); + } + + /** + * Construct a wrapped server ping from a native NMS object. + * @param handle - the native object. + * @return The wrapped server ping object. + */ + public static WrappedServerPing fromHandle(Object handle) { + return new WrappedServerPing(handle); + } + + /** + * Retrieve the message of the day. + * @return The messge of the day. + */ + public WrappedChatComponent getMotD() { + return WrappedChatComponent.fromHandle(DESCRIPTION.get(handle)); + } + + /** + * Set the message of the day. + * @param description - message of the day. + */ + public void setMotD(WrappedChatComponent description) { + DESCRIPTION.set(handle, description.getHandle()); + } + + /** + * Set the message of the day. + *

+ * Warning: Only the first line will be transmitted. + * @param description - the message. + */ + public void setMotD(String message) { + setMotD(WrappedChatComponent.fromChatMessage(message)[0]); + } + + /** + * Retrieve the compressed PNG file that is being displayed as a favicon. + * @return The favicon. + */ + public CompressedImage getFavicon() { + return CompressedImage.fromEncodedText((String) FAVICON.get(handle)); + } + + /** + * Set the compressed PNG file that is being displayed. + * @param image - the new compressed image. + */ + public void setFavicon(CompressedImage image) { + FAVICON.set(handle, image.toEncodedText()); + } + + /** + * Retrieve the displayed number of online players. + * @return The displayed number. + */ + public int getPlayersOnline() { + return (Integer) PLAYERS_ONLINE.get(players); + } + + /** + * Set the displayed number of online players. + * @param online - online players. + */ + public void setPlayersOnline(int online) { + PLAYERS_ONLINE.set(players, online); + } + + /** + * Retrieve the displayed maximum number of players. + * @return The maximum number. + */ + public int getPlayersMaximum() { + return (Integer) PLAYERS_MAXIMUM.get(players); + } + + /** + * Set the displayed maximum number of players. + * @param maximum - maximum player count. + */ + public void setPlayersMaximum(int maximum) { + PLAYERS_MAXIMUM.set(players, maximum); + } + + /** + * Retrieve a copy of all the logged in players. + * @return Logged in players. + */ + public ImmutableList getPlayers() { + return ImmutableList.copyOf(PROFILE_CONVERT.getSpecific(PLAYERS_PROFILES.get(players))); + } + + /** + * Set the displayed list of logged in players. + * @param profile - every logged in player. + */ + public void setPlayers(Iterable profile) { + PLAYERS_PROFILES.set(handle, PROFILE_CONVERT.getGeneric(GameProfile[].class, profile)); + } + + /** + * Set the displayed lst of logged in players. + * @param players - the players to display. + */ + public void setBukkitPlayers(Iterable players) { + List profiles = Lists.newArrayList(); + + for (Player player : players) { + GameProfile profile = (GameProfile) ENTITY_HUMAN_PROFILE.get(BukkitUnwrapper.getInstance().unwrapItem(player)); + profiles.add(new WrappedGameProfile(profile.getId(), profile.getName())); + } + setPlayers(profiles); + } + + /** + * Retrieve the version name of the current server. + * @return The version name. + */ + public String getVersionName() { + return (String) VERSION_NAME.get(version); + } + + /** + * Set the version name of the current server. + * @param name - the new version name. + */ + public void setVersionName(String name) { + VERSION_NAME.set(version, name); + } + + /** + * Retrieve the protocol number. + * @return The protocol. + */ + public int getVersionProtocol() { + return (Integer) VERSION_PROTOCOL.get(version); + } + + /** + * Set the version protocol + * @param protocol - the protocol number. + */ + public void setVersionProtocol(int protocol) { + VERSION_PROTOCOL.set(version, protocol); + } + + /** + * Represents a compressed favicon. + * @author Kristian + */ + public static class CompressedImage { + private final String mime; + private final byte[] data; + + /** + * Construct a new compressed image. + * @param mime - the mime type. + * @param data - the raw compressed image data. + */ + public CompressedImage(String mime, byte[] data) { + this.mime = Preconditions.checkNotNull(mime, "mime cannot be NULL"); + this.data = Preconditions.checkNotNull(data, "data cannot be NULL"); + } + + /** + * Retrieve a compressed image from an input stream. + * @param input - the PNG as an input stream. + * @return The compressed image. + * @throws IOException If we cannot read the input stream. + */ + public static CompressedImage fromPng(InputStream input) throws IOException { + return new CompressedImage("image/png", ByteStreams.toByteArray(input)); + } + + /** + * Retrieve a compressed image from a byte array of a PNG file. + * @param data - the file as a byte array. + * @return The compressed image. + */ + public static CompressedImage fromPng(byte[] data) { + return new CompressedImage("image/png", data); + } + + /** + * Retrieve a compressed image from an image. + * @param image - the image. + * @throws IOException If we were unable to compress the image. + */ + public static CompressedImage fromPng(RenderedImage image) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + ImageIO.write(image, "png", output); + return new CompressedImage("image/png", output.toByteArray()); + } + + /** + * Retrieve a compressed image from an encoded text. + * @param text - the encoded text. + * @return The corresponding compressed image. + */ + public static CompressedImage fromEncodedText(String text) { + String mime = null; + byte[] data = null; + + for (String segment : Splitter.on(";").split(text)) { + if (segment.startsWith("data:")) { + mime = segment.substring(5); + } else if (segment.startsWith("base64,")) { + byte[] encoded = segment.substring(7).getBytes(Charsets.UTF_8); + ByteBuf decoded = Base64.decode(Unpooled.wrappedBuffer(encoded)); + + // Read into a byte array + data = new byte[decoded.readableBytes()]; + decoded.readBytes(data); + } else { + // We will ignore these segments + } + } + return new CompressedImage(mime, data); + } + + /** + * Retrieve the MIME type of the image. + *

+ * This is image/png in vanilla Minecraft. + * @return The MIME type. + */ + public String getMime() { + return mime; + } + + /** + * Retrieve a copy of the underlying data array. + * @return The underlying compressed image. + */ + public byte[] getDataCopy() { + return data.clone(); + } + + /** + * Uncompress and return the stored image. + * @return The image. + * @throws IOException If the image data could not be decoded. + */ + public BufferedImage getImage() throws IOException { + return ImageIO.read(new ByteArrayInputStream(data)); + } + + /** + * Convert the compressed image to encoded text. + * @return The encoded text. + */ + public String toEncodedText() { + return "data:" + mime + ";base64," + Base64.encode(Unpooled.wrappedBuffer(data)).toString(Charsets.UTF_8); + } + } +} diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/utility/MinecraftReflectionTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/utility/MinecraftReflectionTest.java index 5192e6bb..a82c8d3a 100644 --- a/ProtocolLib/src/test/java/com/comphenix/protocol/utility/MinecraftReflectionTest.java +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/utility/MinecraftReflectionTest.java @@ -5,6 +5,9 @@ import static org.junit.Assert.*; import net.minecraft.server.v1_7_R1.ChatSerializer; import net.minecraft.server.v1_7_R1.IChatBaseComponent; import net.minecraft.server.v1_7_R1.NBTCompressedStreamTools; +import net.minecraft.server.v1_7_R1.ServerPing; +import net.minecraft.server.v1_7_R1.ServerPingPlayerSample; +import net.minecraft.server.v1_7_R1.ServerPingServerData; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -37,11 +40,26 @@ public class MinecraftReflectionTest { @Test public void testChatComponent() { - assertEquals(IChatBaseComponent.class, MinecraftReflection.getIChatBaseComponent()); + assertEquals(IChatBaseComponent.class, MinecraftReflection.getIChatBaseComponentClass()); } @Test public void testChatSerializer() { - assertEquals(ChatSerializer.class, MinecraftReflection.getChatSerializer()); + assertEquals(ChatSerializer.class, MinecraftReflection.getChatSerializerClass()); + } + + @Test + public void testServerPing() { + assertEquals(ServerPing.class, MinecraftReflection.getServerPingClass()); + } + + @Test + public void testServerPingPlayerSample() { + assertEquals(ServerPingPlayerSample.class, MinecraftReflection.getServerPingPlayerSampleClass()); + } + + @Test + public void testServerPingServerData() { + assertEquals(ServerPingServerData.class, MinecraftReflection.getServerPingServerDataClass()); } } diff --git a/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/WrappedServerPingTest.java b/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/WrappedServerPingTest.java new file mode 100644 index 00000000..38801baa --- /dev/null +++ b/ProtocolLib/src/test/java/com/comphenix/protocol/wrappers/WrappedServerPingTest.java @@ -0,0 +1,41 @@ +package com.comphenix.protocol.wrappers; + +import static org.junit.Assert.*; + +import java.io.IOException; + +import org.junit.BeforeClass; +import org.junit.Test; + +import com.comphenix.protocol.BukkitInitialization; +import com.comphenix.protocol.wrappers.WrappedServerPing.CompressedImage; +import com.google.common.io.Resources; + +public class WrappedServerPingTest { + @BeforeClass + public static void initializeBukkit() throws IllegalAccessException { + BukkitInitialization.initializePackage(); + } + + @Test + public void test() throws IOException { + CompressedImage tux = CompressedImage.fromPng(Resources.getResource("tux.png").openStream()); + byte[] original = tux.getDataCopy(); + + WrappedServerPing serverPing = new WrappedServerPing(); + serverPing.setMotD("Hello, this is a test."); + serverPing.setPlayersOnline(5); + serverPing.setPlayersMaximum(10); + serverPing.setVersionName("Minecraft 123"); + serverPing.setVersionProtocol(4); + serverPing.setFavicon(tux); + + assertEquals(5, serverPing.getPlayersOnline()); + assertEquals(10, serverPing.getPlayersMaximum()); + assertEquals("Minecraft 123", serverPing.getVersionName()); + assertEquals(4, serverPing.getVersionProtocol()); + + assertArrayEquals(original, serverPing.getFavicon().getDataCopy()); + } + +}