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 extends T> 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 extends WrappedGameProfile> 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 extends Player> 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());
+ }
+
+}