> PROFILE_CONVERT =
+ BukkitConverters.getArrayConverter(GAME_PROFILE, BukkitConverters.getWrappedGameProfileConverter());
+
+ // Get profile from player
+ private static final FieldAccessor ENTITY_HUMAN_PROFILE = Accessors.getFieldAccessor(
+ MinecraftReflection.getEntityPlayerClass().getSuperclass(), GAME_PROFILE, true);
+
+ private static final Class> GAME_PROFILE_ARRAY = MinecraftReflection.getArrayClass(GAME_PROFILE);
+
+ // Server ping fields
+ private static final Class> SERVER_PING = MinecraftReflection.getServerPingClass();
+ private static final ConstructorAccessor SERVER_PING_CONSTRUCTOR = Accessors.getConstructorAccessor(SERVER_PING);
+ private static final FieldAccessor DESCRIPTION = Accessors.getFieldAccessor(SERVER_PING, MinecraftReflection.getIChatBaseComponentClass(), true);
+ private static final FieldAccessor PLAYERS = Accessors.getFieldAccessor(SERVER_PING, MinecraftReflection.getServerPingPlayerSampleClass(), true);
+ private static final FieldAccessor VERSION = Accessors.getFieldAccessor(SERVER_PING, MinecraftReflection.getServerPingServerDataClass(), true);
+ private static final FieldAccessor FAVICON = Accessors.getFieldAccessor(SERVER_PING, String.class, true);
+ private static final FieldAccessor[] BOOLEAN_ACCESSORS = Accessors.getFieldAccessorArray(SERVER_PING, boolean.class, true);
+
+ // Server ping player sample fields
+ private static final Class> PLAYERS_CLASS = MinecraftReflection.getServerPingPlayerSampleClass();
+ private static final ConstructorAccessor PLAYERS_CONSTRUCTOR = Accessors.getConstructorAccessor(PLAYERS_CLASS, int.class, int.class);
+ private static final FieldAccessor[] PLAYERS_INTS = Accessors.getFieldAccessorArray(PLAYERS_CLASS, int.class, true);
+ private static final FieldAccessor PLAYERS_PROFILES = Accessors.getFieldAccessor(PLAYERS_CLASS, GAME_PROFILE_ARRAY, true);
+ private static final FieldAccessor PLAYERS_MAXIMUM = PLAYERS_INTS[0];
+ private static final FieldAccessor PLAYERS_ONLINE = PLAYERS_INTS[1];
+
+ // Server ping serialization
+ private static final Class> GSON_CLASS = MinecraftReflection.getMinecraftGsonClass();
+ private static final MethodAccessor GSON_TO_JSON = Accessors.getMethodAccessor(GSON_CLASS, "toJson", Object.class);
+ private static final MethodAccessor GSON_FROM_JSON = Accessors.getMethodAccessor(GSON_CLASS, "fromJson", String.class, Class.class);
+ private static final FieldAccessor PING_GSON = Accessors.getMemorizing(Accessors.getFieldAccessor(
+ PacketType.Status.Server.SERVER_INFO.getPacketClass(), GSON_CLASS, true
+ ));
+
+ // Server data fields
+ private static final Class> VERSION_CLASS = MinecraftReflection.getServerPingServerDataClass();
+ private static final ConstructorAccessor VERSION_CONSTRUCTOR = Accessors.getConstructorAccessor(VERSION_CLASS, String.class, int.class);
+ private static final FieldAccessor VERSION_NAME = Accessors.getFieldAccessor(VERSION_CLASS, String.class, true);
+ private static final FieldAccessor VERSION_PROTOCOL = Accessors.getFieldAccessor(VERSION_CLASS, int.class, true);
+
+
+ // Inner class
+ private Object players; // may be NULL
+ private Object version;
+
+ /**
+ * Construct a new server ping initialized with a zero player count, and zero maximum.
+ *
+ * Note that the version string is set to 1.9.4.
+ */
+ public LegacyServerPing() {
+ super(MinecraftReflection.getServerPingClass());
+ setHandle(SERVER_PING_CONSTRUCTOR.invoke());
+ resetPlayers();
+ resetVersion();
+ }
+
+ public LegacyServerPing(Object handle) {
+ super(MinecraftReflection.getServerPingClass());
+ setHandle(handle);
+ this.players = PLAYERS.get(handle);
+ this.version = VERSION.get(handle);
+ }
+
+ /**
+ * Set the player count and player maximum to the default values.
+ */
+ public void resetPlayers() {
+ players = PLAYERS_CONSTRUCTOR.invoke(0, 0);
+ PLAYERS.set(handle, players);
+ }
+
+ /**
+ * Reset the version string to the default state.
+ */
+ public void resetVersion() {
+ MinecraftVersion minecraftVersion = MinecraftVersion.getCurrentVersion();
+ version = VERSION_CONSTRUCTOR.invoke(minecraftVersion.toString(), MinecraftProtocolVersion.getCurrentVersion());
+ VERSION.set(handle, version);
+ }
+
+ /**
+ * Construct a wrapped server ping from a native NMS object.
+ * @param handle - the native object.
+ * @return The wrapped server ping object.
+ */
+ public static LegacyServerPing fromHandle(Object handle) {
+ return new LegacyServerPing(handle);
+ }
+
+ /**
+ * Construct a wrapper server ping from an encoded JSON string.
+ * @param json - the JSON string.
+ * @return The wrapped server ping.
+ */
+ public static LegacyServerPing 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.
+ */
+ public WrappedChatComponent getMotD() {
+ return WrappedChatComponent.fromHandle(DESCRIPTION.get(handle));
+ }
+
+ /**
+ * Set the message of the day.
+ * @param description - message of the day.
+ */
+ public void setMotD(Object description) {
+ DESCRIPTION.set(handle, description);
+ }
+
+ /**
+ * Set the message of the day.
+ * @param message - the message.
+ */
+ public void setMotD(String message) {
+ setMotD(WrappedChatComponent.fromLegacyText(message));
+ }
+
+ /**
+ * Retrieve the compressed PNG file that is being displayed as a favicon.
+ * @return The favicon, or NULL if no favicon will be displayed.
+ */
+ public String getFavicon() {
+ return (String) FAVICON.get(handle);
+ }
+
+ /**
+ * Set the compressed PNG file that is being displayed.
+ * @param image - the new compressed image or NULL if no favicon should be displayed.
+ */
+ public void setFavicon(String image) {
+ FAVICON.set(handle, image);
+ }
+
+ /**
+ * Retrieve whether chat preview is enabled on the server.
+ * @return whether chat preview is enabled on the server.
+ * @since 1.19
+ * @deprecated Removed in 1.19.3
+ */
+ @Deprecated
+ public boolean isChatPreviewEnabled() {
+ return (Boolean) BOOLEAN_ACCESSORS[0].get(handle);
+ }
+
+ /**
+ * Sets whether chat preview is enabled on the server.
+ * @param chatPreviewEnabled true if enabled, false otherwise.
+ * @since 1.19
+ * @deprecated Removed in 1.19.3
+ */
+ @Deprecated
+ public void setChatPreviewEnabled(boolean chatPreviewEnabled) {
+ BOOLEAN_ACCESSORS[0].set(handle, chatPreviewEnabled);
+ }
+
+ /**
+ * Sets whether the server enforces secure chat.
+ * @return whether the server enforces secure chat.
+ * @since 1.19.1
+ */
+ public boolean isEnforceSecureChat() {
+ int index = MinecraftVersion.FEATURE_PREVIEW_UPDATE.atOrAbove() ? 0 : 1;
+ return (Boolean) BOOLEAN_ACCESSORS[index].get(handle);
+ }
+
+ /**
+ * Sets whether the server enforces secure chat.
+ * @param enforceSecureChat true if enabled, false otherwise.
+ * @since 1.19.1
+ */
+ public void setEnforceSecureChat(boolean enforceSecureChat) {
+ int index = MinecraftVersion.FEATURE_PREVIEW_UPDATE.atOrAbove() ? 0 : 1;
+ BOOLEAN_ACCESSORS[index].set(handle, enforceSecureChat);
+ }
+
+ /**
+ * Retrieve the displayed number of online players.
+ * @return The displayed number.
+ * @throws IllegalStateException If the player count has been hidden via {@link #setPlayersVisible(boolean)}.
+ * @see #setPlayersOnline(int)
+ */
+ public int getPlayersOnline() {
+ if (players == null)
+ throw new IllegalStateException("The player count has been hidden.");
+ return (Integer) PLAYERS_ONLINE.get(players);
+ }
+
+ /**
+ * Set the displayed number of online players.
+ *
+ * As of 1.7.2, this is completely unrestricted, and can be both positive and
+ * negative, as well as higher than the player maximum.
+ * @param online - online players.
+ */
+ public void setPlayersOnline(int online) {
+ if (players == null)
+ resetPlayers();
+ PLAYERS_ONLINE.set(players, online);
+ }
+
+ /**
+ * Retrieve the displayed maximum number of players.
+ * @return The maximum number.
+ * @throws IllegalStateException If the player maximum has been hidden via {@link #setPlayersVisible(boolean)}.
+ * @see #setPlayersMaximum(int)
+ */
+ public int getPlayersMaximum() {
+ if (players == null)
+ throw new IllegalStateException("The player maximum has been hidden.");
+ return (Integer) PLAYERS_MAXIMUM.get(players);
+ }
+
+ /**
+ * Set the displayed maximum number of players.
+ *
+ * The 1.7.2 accepts any value as a player maximum, positive or negative. It even permits a player maximum that
+ * is less than the player count.
+ * @param maximum - maximum player count.
+ */
+ public void setPlayersMaximum(int maximum) {
+ if (players == null)
+ resetPlayers();
+ PLAYERS_MAXIMUM.set(players, maximum);
+ }
+
+ /**
+ * Set whether or not the player count and player maximum is visible.
+ *
+ * Note that this may set the current player count and maximum to their respective real values.
+ * @param visible - TRUE if it should be visible, FALSE otherwise.
+ */
+ public void setPlayersVisible(boolean visible) {
+ if (arePlayersVisible() != visible) {
+ if (visible) {
+ // Recreate the count and maximum
+ Server server = Bukkit.getServer();
+ setPlayersMaximum(server.getMaxPlayers());
+ setPlayersOnline(Bukkit.getOnlinePlayers().size());
+ } else {
+ PLAYERS.set(handle, players = null);
+ }
+ }
+ }
+
+ /**
+ * Determine if the player count and maximum is visible.
+ *
+ * If not, the client will display ??? in the same location.
+ * @return TRUE if the player statistics is visible, FALSE otherwise.
+ */
+ public boolean arePlayersVisible() {
+ return players != null;
+ }
+
+ /**
+ * Retrieve a copy of all the logged in players.
+ * @return Logged in players or an empty list if no player names will be displayed.
+ */
+ public ImmutableList getPlayers() {
+ if (players == null)
+ return ImmutableList.of();
+ Object playerProfiles = PLAYERS_PROFILES.get(players);
+ if (playerProfiles == null)
+ return ImmutableList.of();
+ return ImmutableList.copyOf(PROFILE_CONVERT.getSpecific(playerProfiles));
+ }
+
+ /**
+ * Set the displayed list of logged in players.
+ * @param profile - every logged in player.
+ */
+ public void setPlayers(Object profile) {
+ if (players == null)
+ resetPlayers();
+ PLAYERS_PROFILES.set(players, profile);
+ }
+
+ /**
+ * Set the displayed lst of logged in players.
+ * @param players - the players to display.
+ */
+ public void setBukkitPlayers(Iterable extends Player> players) {
+ final List profiles = new ArrayList<>();
+
+ for (Player player : players) {
+ Object profile = ENTITY_HUMAN_PROFILE.get(BukkitUnwrapper.getInstance().unwrapItem(player));
+ profiles.add(WrappedGameProfile.fromHandle(profile));
+ }
+
+ 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);
+ }
+
+ /**
+ * Retrieve a deep copy of the current wrapper object.
+ * @return The current object.
+ */
+ public LegacyServerPing deepClone() {
+ LegacyServerPing copy = new LegacyServerPing();
+ WrappedChatComponent motd = getMotD();
+
+ copy.setPlayers(getPlayers());
+ copy.setFavicon(getFavicon());
+ copy.setMotD(motd != null ? motd.deepClone() : null);
+ copy.setVersionName(getVersionName());
+ copy.setVersionProtocol(getVersionProtocol());
+
+ if (arePlayersVisible()) {
+ copy.setPlayersMaximum(getPlayersMaximum());
+ copy.setPlayersOnline(getPlayersOnline());
+ } else {
+ copy.setPlayersVisible(false);
+ }
+ 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);
+ }
+
+ @Override
+ public String toString() {
+ return "WrappedServerPing< " + toJson() + ">";
+ }
+}
diff --git a/src/main/java/com/comphenix/protocol/wrappers/ping/ServerPingImpl.java b/src/main/java/com/comphenix/protocol/wrappers/ping/ServerPingImpl.java
new file mode 100644
index 00000000..7606c3f3
--- /dev/null
+++ b/src/main/java/com/comphenix/protocol/wrappers/ping/ServerPingImpl.java
@@ -0,0 +1,42 @@
+package com.comphenix.protocol.wrappers.ping;
+
+import java.util.List;
+
+import com.comphenix.protocol.wrappers.WrappedChatComponent;
+import com.comphenix.protocol.wrappers.WrappedGameProfile;
+import com.comphenix.protocol.wrappers.WrappedServerPing.CompressedImage;
+
+public interface ServerPingImpl {
+ Object getMotD();
+ void setMotD(Object description);
+ int getPlayersMaximum();
+ void setPlayersMaximum(int maxPlayers);
+ int getPlayersOnline();
+ void setPlayersOnline(int onlineCount);
+ Object getPlayers();
+ void setPlayers(Object playerSample);
+ String getVersionName();
+ void setVersionName(String versionName);
+ int getVersionProtocol();
+ void setVersionProtocol(int protocolVersion);
+ String getFavicon();
+ void setFavicon(String favicon);
+ boolean isEnforceSecureChat();
+ void setEnforceSecureChat(boolean safeChat);
+
+ void resetPlayers();
+ void resetVersion();
+
+ default boolean isChatPreviewEnabled() {
+ return false;
+ }
+
+ default void setChatPreviewEnabled(boolean enabled) {
+
+ }
+
+ boolean arePlayersVisible();
+ void setPlayersVisible(boolean visible);
+
+ Object getHandle();
+}
\ No newline at end of file
diff --git a/src/main/java/com/comphenix/protocol/wrappers/ping/ServerPingRecord.java b/src/main/java/com/comphenix/protocol/wrappers/ping/ServerPingRecord.java
new file mode 100644
index 00000000..da15146d
--- /dev/null
+++ b/src/main/java/com/comphenix/protocol/wrappers/ping/ServerPingRecord.java
@@ -0,0 +1,264 @@
+package com.comphenix.protocol.wrappers.ping;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.List;
+import java.util.Optional;
+
+import com.comphenix.protocol.events.AbstractStructure;
+import com.comphenix.protocol.events.InternalStructure;
+import com.comphenix.protocol.reflect.StructureModifier;
+import com.comphenix.protocol.reflect.accessors.Accessors;
+import com.comphenix.protocol.reflect.accessors.ConstructorAccessor;
+import com.comphenix.protocol.utility.MinecraftProtocolVersion;
+import com.comphenix.protocol.utility.MinecraftReflection;
+import com.comphenix.protocol.utility.MinecraftVersion;
+import com.comphenix.protocol.wrappers.*;
+
+import org.bukkit.Bukkit;
+
+public class ServerPingRecord implements ServerPingImpl {
+ private static Class> SERVER_PING;
+ private static Class> PLAYER_SAMPLE_CLASS;
+ private static Class> SERVER_DATA_CLASS;
+
+ private static WrappedChatComponent DEFAULT_DESCRIPTION;
+
+ private static ConstructorAccessor PING_CTOR;
+
+ private static boolean initialized;
+
+ private static void initialize() {
+ if (initialized) {
+ return;
+ }
+
+ initialized = true;
+
+ try {
+ SERVER_PING = MinecraftReflection.getServerPingClass();
+ PLAYER_SAMPLE_CLASS = MinecraftReflection.getServerPingPlayerSampleClass();
+ SERVER_DATA_CLASS = MinecraftReflection.getServerPingServerDataClass();
+
+ PING_CTOR = Accessors.getConstructorAccessor(SERVER_PING.getConstructors()[0]);
+
+ DATA_WRAPPER = AutoWrapper
+ .wrap(ServerData.class, SERVER_DATA_CLASS)
+ .field(0, Converters.passthrough(String.class))
+ .field(1, Converters.passthrough(int.class));
+
+ SAMPLE_WRAPPER = AutoWrapper
+ .wrap(PlayerSample.class, PLAYER_SAMPLE_CLASS)
+ .field(0, Converters.passthrough(int.class))
+ .field(1, Converters.passthrough(int.class))
+ .field(2, Converters.passthrough(Object.class));
+
+ FAVICON_WRAPPER = AutoWrapper
+ .wrap(Favicon.class, MinecraftReflection.getMinecraftClass("network.protocol.status.ServerPing$a"))
+ .field(0, Converters.passthrough(byte[].class));
+
+ DEFAULT_DESCRIPTION = WrappedChatComponent.fromLegacyText("A Minecraft Server");
+ } catch (Exception ex) {
+ ex.printStackTrace(); // TODO
+ }
+ }
+
+ private static class PlayerSample {
+ public int max;
+ public int online;
+ public Object sample;
+ }
+
+ private static class ServerData {
+ public String name;
+ public int protocol;
+ }
+
+ private static class Favicon {
+ public byte[] iconBytes;
+ }
+
+ private static AutoWrapper SAMPLE_WRAPPER;
+
+ private static AutoWrapper DATA_WRAPPER;
+
+ private static AutoWrapper FAVICON_WRAPPER;
+
+ private Object description;
+ private PlayerSample playerSample;
+ private ServerData serverData;
+ private Favicon favicon;
+ private boolean enforceSafeChat;
+ private boolean playersVisible = true;
+
+ private static ServerData defaultData() {
+ ServerData data = new ServerData();
+ data.name = MinecraftVersion.getCurrentVersion().toString();
+ data.protocol = MinecraftProtocolVersion.getCurrentVersion();
+ return data;
+ }
+
+ private static PlayerSample defaultSample() {
+ PlayerSample sample = new PlayerSample();
+ sample.max = Bukkit.getMaxPlayers();
+ sample.online = Bukkit.getOnlinePlayers().size();
+ sample.sample = null;
+ return sample;
+ }
+
+ private static Favicon defaultFavicon() {
+ Favicon favicon = new Favicon();
+ favicon.iconBytes = new byte[0];
+ return favicon;
+ }
+
+ public ServerPingRecord(Object handle) {
+ initialize();
+
+ StructureModifier