ProtocolLib/src/main/java/com/comphenix/protocol/wrappers/WrappedServerPing.java

550 lines
15 KiB
Java

package com.comphenix.protocol.wrappers;
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.FieldAccessor;
import com.comphenix.protocol.reflect.accessors.MethodAccessor;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.utility.MinecraftVersion;
import com.comphenix.protocol.wrappers.ping.LegacyServerPing;
import com.comphenix.protocol.wrappers.ping.ServerPingImpl;
import com.comphenix.protocol.wrappers.ping.ServerPingRecord;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.io.ByteStreams;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.base64.Base64;
import org.bukkit.entity.Player;
import javax.imageio.ImageIO;
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.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a server ping packet data.
* @author Kristian
*/
public class WrappedServerPing implements ClonableWrapper {
private static final Class<?> GAME_PROFILE = MinecraftReflection.getGameProfileClass();
// Get profile from player
private static final FieldAccessor ENTITY_HUMAN_PROFILE = Accessors.getFieldAccessor(
MinecraftReflection.getEntityPlayerClass().getSuperclass(), GAME_PROFILE, true);
private final ServerPingImpl impl;
/**
* Construct a new server ping initialized with a zero player count, and zero maximum.
* <p>
* Note that the version string is set to 1.9.4.
*/
public WrappedServerPing() {
this.impl = newImpl();
}
private WrappedServerPing(Object handle) {
this.impl = newImpl(handle);
}
private static ServerPingImpl newImpl() {
if (MinecraftVersion.FEATURE_PREVIEW_2.atOrAbove()) {
return new ServerPingRecord();
}
return new LegacyServerPing();
}
private static ServerPingImpl newImpl(Object handle) {
if (MinecraftVersion.FEATURE_PREVIEW_2.atOrAbove()) {
return new ServerPingRecord(handle);
}
return new LegacyServerPing(handle);
}
/**
* Set the player count and player maximum to the default values.
*/
private void resetPlayers() {
impl.resetPlayers();
}
/**
* Reset the version string to the default state.
*/
private void resetVersion() {
impl.resetVersion();
}
/**
* 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);
}
/**
* 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));
return null;
}
/**
* Retrieve the message of the day.
* @return The message of the day.
*/
public WrappedChatComponent getMotD() {
return impl.getMotD();
}
/**
* Set the message of the day.
* @param description - message of the day.
*/
public void setMotD(WrappedChatComponent description) {
impl.setMotD(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 CompressedImage getFavicon() {
String favicon = impl.getFavicon();
return (favicon != null) ? CompressedImage.fromEncodedText(favicon) : null;
}
/**
* 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(CompressedImage image) {
impl.setFavicon((image != null) ? image.toEncodedText() : null);
}
/**
* 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 impl.isChatPreviewEnabled();
}
/**
* 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) {
impl.setChatPreviewEnabled(chatPreviewEnabled);
}
/**
* Sets whether the server enforces secure chat.
* @return whether the server enforces secure chat.
* @since 1.19.1
*/
public boolean isEnforceSecureChat() {
return impl.isEnforceSecureChat();
}
/**
* Sets whether the server enforces secure chat.
* @param enforceSecureChat true if enabled, false otherwise.
* @since 1.19.1
*/
public void setEnforceSecureChat(boolean enforceSecureChat) {
impl.setEnforceSecureChat(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() {
return impl.getPlayersOnline();
}
/**
* Set the displayed number of online players.
* <p>
* 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) {
impl.setPlayersOnline(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() {
return impl.getPlayersMaximum();
}
/**
* Set the displayed maximum number of players.
* <p>
* 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) {
impl.setPlayersMaximum(maximum);
}
/**
* Set whether or not the player count and player maximum is visible.
* <p>
* 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 (isPlayersVisible() != visible) {
impl.setPlayersVisible(visible);
}
}
/**
* Determine if the player count and maximum is visible.
* <p>
* If not, the client will display ??? in the same location.
* @return TRUE if the player statistics is visible, FALSE otherwise.
*/
public boolean isPlayersVisible() {
return impl.arePlayersVisible();
}
/**
* 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<WrappedGameProfile> getPlayers() {
return impl.getPlayers();
}
/**
* Set the displayed list of logged in players.
* @param profile - every logged in player.
*/
public void setPlayers(Iterable<? extends WrappedGameProfile> profile) {
if (!isPlayersVisible())
resetPlayers();
impl.setPlayers(profile);
}
/**
* Set the displayed lst of logged in players.
* @param players - the players to display.
*/
public void setBukkitPlayers(Iterable<? extends Player> players) {
final List<WrappedGameProfile> 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 impl.getVersionName();
}
/**
* Set the version name of the current server.
* @param name - the new version name.
*/
public void setVersionName(String name) {
impl.setVersionName(name);
}
/**
* Retrieve the protocol number.
* @return The protocol.
*/
public int getVersionProtocol() {
return impl.getVersionProtocol();
}
/**
* Set the version protocol
* @param protocol - the protocol number.
*/
public void setVersionProtocol(int protocol) {
impl.setVersionProtocol(protocol);
}
@Override
public Object getHandle() {
return impl.getHandle();
}
/**
* Retrieve a deep copy of the current wrapper object.
* @return The current object.
*/
public WrappedServerPing deepClone() {
WrappedServerPing copy = new WrappedServerPing();
WrappedChatComponent motd = getMotD();
copy.setPlayers(getPlayers());
copy.setFavicon(getFavicon());
copy.setMotD(motd != null ? motd.deepClone() : null);
copy.setVersionName(getVersionName());
copy.setVersionProtocol(getVersionProtocol());
if (isPlayersVisible()) {
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 null;
// return (String) GSON_TO_JSON.invoke(PING_GSON.get(null), getHandle());
}
@Override
public String toString() {
return "WrappedServerPing< " + toJson() + ">";
}
/**
* Represents a compressed favicon.
* @author Kristian
*/
// Should not have been an inner class ... oh well.
public static class CompressedImage {
protected volatile String mime;
protected volatile byte[] data;
protected volatile String encoded;
/**
* Represents a compressed image with no content.
*/
protected CompressedImage() {
// Derived class should initialize some of the fields
}
/**
* 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 a base-64 encoded PNG file.
* @param base64 - the base 64-encoded PNG.
* @return The compressed image.
*/
public static CompressedImage fromBase64Png(String base64) {
try {
return new EncodedCompressedImage("data:image/png;base64," + base64);
} catch (IllegalArgumentException e) {
// Remind the caller
throw new IllegalArgumentException("Must be a pure base64 encoded string. Cannot be an encoded text.", e);
}
}
/**
* Retrieve a compressed image from an image.
* @param image - the image.
* @return A compressed image from an 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) {
return new EncodedCompressedImage(text);
}
/**
* Retrieve the MIME type of the image.
* <p>
* 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 getData().clone();
}
/**
* Retrieve the underlying data, with no copying.
* @return The underlying data.
*/
protected byte[] getData() {
return data;
}
/**
* 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(getData()));
}
/**
* Convert the compressed image to encoded text.
* @return The encoded text.
*/
public String toEncodedText() {
if (encoded == null) {
final ByteBuf buffer = Unpooled.wrappedBuffer(getDataCopy());
encoded = "data:" + getMime() + ";base64," +
Base64.encode(buffer).toString(StandardCharsets.UTF_8);
}
return encoded;
}
}
/**
* Represents a compressed image that starts out as an encoded base 64 string.
* @author Kristian
*/
private static class EncodedCompressedImage extends CompressedImage {
public EncodedCompressedImage(String encoded) {
this.encoded = Preconditions.checkNotNull(encoded, "encoded favicon cannot be NULL");
}
/**
* Ensure that we have decoded the content of the encoded text.
*/
protected void initialize() {
if (mime == null || data == null) {
decode();
}
}
/**
* Decode the encoded text.
*/
protected void decode() {
for (String segment : Splitter.on(";").split(encoded)) {
if (segment.startsWith("data:")) {
this.mime = segment.substring(5);
} else if (segment.startsWith("base64,")) {
byte[] encoded = segment.substring(7).getBytes(StandardCharsets.UTF_8);
ByteBuf decoded = Base64.decode(Unpooled.wrappedBuffer(encoded));
// Read into a byte array
byte[] data = new byte[decoded.readableBytes()];
decoded.readBytes(data);
this.data = data;
} else {
// We will ignore these segments
}
}
}
@Override
protected byte[] getData() {
initialize();
return super.getData();
}
@Override
public String getMime() {
initialize();
return super.getMime();
}
@Override
public String toEncodedText() {
return encoded;
}
}
}