Switch to per-version response data generation and implement NamedAndIdentified in ResponseData

This commit is contained in:
Kieran Wallbanks 2021-04-18 22:47:07 +01:00
parent 0ac6d1aa37
commit 42e1811b7c
5 changed files with 227 additions and 76 deletions

View File

@ -12,8 +12,11 @@ import net.minestom.server.utils.binary.BinaryReader;
import net.minestom.server.utils.binary.BinaryWriter;
import org.jetbrains.annotations.NotNull;
import static net.minestom.server.ping.PingResponse.*;
public class StatusRequestPacket implements ClientPreplayPacket {
@SuppressWarnings("deprecation") // we need to continue handling the ResponseDataConsumer until it's removed
@Override
public void process(@NotNull PlayerConnection connection) {
ResponseDataConsumer consumer = MinecraftServer.getResponseDataConsumer();
@ -27,15 +30,22 @@ public class StatusRequestPacket implements ClientPreplayPacket {
responseData.setDescription(Component.text("Minestom Server"));
responseData.setFavicon("");
if (consumer != null)
if (consumer != null) {
consumer.accept(connection, responseData);
}
// Call event
ServerListPingEvent statusRequestEvent = new ServerListPingEvent(responseData, connection);
MinecraftServer.getGlobalEventHandler().callCancellableEvent(ServerListPingEvent.class, statusRequestEvent,
() -> {
ResponsePacket responsePacket = new ResponsePacket();
responsePacket.jsonResponse = responseData.build().toString();
final ResponsePacket responsePacket = new ResponsePacket();
// check if we need to use a legacy response
if (connection.getProtocolVersion() >= 713) {
responsePacket.jsonResponse = FULL_RGB.getResponse(statusRequestEvent.getResponseData()).toString();
} else {
responsePacket.jsonResponse = NAMED_COLORS.getResponse(statusRequestEvent.getResponseData()).toString();
}
connection.sendPacket(responsePacket);
});

View File

@ -0,0 +1,51 @@
package net.minestom.server.ping;
import com.google.gson.JsonObject;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
/**
* Separation between the versions of response, used to determine how {@link ResponseData}
* is serialized.
*
* @param <T> the type of the data returned in the response
*/
@ApiStatus.NonExtendable
@FunctionalInterface
public interface PingResponse<T> {
/**
* Indicates the client supports full RGB with JSON text formatting.
*/
PingResponse<JsonObject> FULL_RGB = data -> VanillaPingResponses.getModernPingResponse(data, true);
/**
* Indicates the client doesn't support full RGB but does support JSON text formatting.
*/
PingResponse<JsonObject> NAMED_COLORS = data -> VanillaPingResponses.getModernPingResponse(data, true);
/**
* Indicates the client is incompatible with the Netty rewrite.
*
* @see <a href="https://wiki.vg/Server_List_Ping#1.6">https://wiki.vg/Server_List_Ping#1.6</a>
* @deprecated This is not yet supported in Minestom
*/
@Deprecated(forRemoval = false)
PingResponse<Object> LEGACY_PING = null;
/**
* Indicates the client is on a beta version of Minecraft.
*
* @see <a href="https://wiki.vg/Server_List_Ping#Beta_1.8_to_1.3">https://wiki.vg/Server_List_Ping#Beta_1.8_to_1.3</a>
* @deprecated This is not yet supported in Minestom
*/
@Deprecated(forRemoval = false)
PingResponse<Object> LEGACY_PING_BETA = null;
/**
* Creates a response from some data.
*
* @param data the data
* @return the response
*/
@NotNull T getResponse(@NotNull ResponseData data);
}

View File

@ -1,24 +1,24 @@
package net.minestom.server.ping;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer;
import net.minestom.server.entity.Player;
import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.utils.identity.NamedAndIdentified;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.*;
import java.util.stream.Collectors;
/**
* Represents the data sent to the player when refreshing the server list.
*
* <p>Edited by listening to the {@link net.minestom.server.event.server.ServerListPingEvent}.
* @see ServerListPingEvent
*/
public class ResponseData {
private final List<PingPlayer> pingPlayers;
private final List<NamedAndIdentified> entries;
private String version;
private int protocol;
private int maxPlayer;
@ -31,7 +31,7 @@ public class ResponseData {
* Constructs a new {@link ResponseData}.
*/
public ResponseData() {
this.pingPlayers = new ArrayList<>();
this.entries = new ArrayList<>();
}
/**
@ -121,7 +121,9 @@ public class ResponseData {
* Adds some players to the response.
*
* @param players the players
* @deprecated See {@link #addEntries(Collection)}}
*/
@Deprecated
public void addPlayer(Iterable<Player> players) {
for (Player player : players) {
this.addPlayer(player);
@ -132,9 +134,11 @@ public class ResponseData {
* Adds a player to the response.
*
* @param player the player
* @deprecated See {@link #addEntry(NamedAndIdentified)}
*/
@Deprecated
public void addPlayer(Player player) {
this.addPlayer(player.getUsername(), player.getUuid());
this.addEntry(player);
}
/**
@ -142,10 +146,11 @@ public class ResponseData {
*
* @param name The name of the player.
* @param uuid The unique identifier of the player.
* @deprecated See {@link #addEntry(NamedAndIdentified)} using {@link NamedAndIdentified#of(String, UUID)}
*/
@Deprecated
public void addPlayer(String name, UUID uuid) {
PingPlayer pingPlayer = PingPlayer.of(name, uuid);
this.pingPlayers.add(pingPlayer);
this.addEntry(NamedAndIdentified.of(name, uuid));
}
/**
@ -154,27 +159,35 @@ public class ResponseData {
* {@link UUID#randomUUID()} is used as the player's UUID.
*
* @param name The name of the player.
* @deprecated See {@link #addEntry(NamedAndIdentified)} using {@link NamedAndIdentified#named(String)}
*/
@Deprecated
public void addPlayer(String name) {
PingPlayer pingPlayer = PingPlayer.of(name, UUID.randomUUID());
this.pingPlayers.add(pingPlayer);
this.addEntry(NamedAndIdentified.named(name));
}
/**
* Removes all of the ping players from this {@link #pingPlayers}. The {@link #pingPlayers} list
* Removes all of the ping players from this {@link #entries}. The {@link #entries} list
* will be empty this call returns.
*
* @deprecated See {@link #clearEntries()}
*/
@Deprecated
public void clearPlayers() {
this.pingPlayers.clear();
this.clearEntries();
}
/**
* Get the list of the response players.
*
* @return the list of the response players.
* @deprecated See {@link #getEntries()}. This return value is now unmodifiable and this operation is incredibly costly.
*/
@Deprecated(forRemoval = true) // to throw an error for people using it - this method is *horrible*
public List<PingPlayer> getPlayers() {
return pingPlayers;
return this.entries.stream()
.map(entry -> PingPlayer.of(PlainComponentSerializer.plain().serialize(entry.getName()), entry.getUuid()))
.collect(Collectors.toUnmodifiableList());
}
/**
@ -226,48 +239,68 @@ public class ResponseData {
return favicon;
}
/**
* Adds an entry to the response data sample. This can be a player or a custom object.
*
* @param entry the entry
* @see NamedAndIdentified
*/
public void addEntry(@NotNull NamedAndIdentified entry) {
this.entries.add(entry);
}
/**
* Adds a series of entries to the response data sample. These can be players or a custom object.
*
* @param entries the entries
* @see NamedAndIdentified
*/
public void addEntries(@NotNull NamedAndIdentified... entries) {
this.addEntries(Arrays.asList(entries));
}
/**
* Adds a series of entries to the response data sample. These can be players or a custom object.
*
* @param entries the entries
* @see NamedAndIdentified
*/
public void addEntries(@NotNull Collection<? extends NamedAndIdentified> entries) {
this.entries.addAll(entries);
}
/**
* Clears the entries.
*/
public void clearEntries() {
this.entries.clear();
}
/**
* Gets a modifiable collection of the current entries.
*
* @return the entries
*/
public @NotNull Collection<NamedAndIdentified> getEntries() {
return this.entries;
}
/**
* Converts the response data into a {@link JsonObject}.
*
* @return The converted response data as a json tree.
* @deprecated Use {@link PingResponse#getResponse(ResponseData)}
*/
@NotNull
public JsonObject build() {
// version
final JsonObject versionObject = new JsonObject();
versionObject.addProperty("name", this.version);
versionObject.addProperty("protocol", this.protocol);
// players info
final JsonObject playersObject = new JsonObject();
playersObject.addProperty("max", this.maxPlayer);
playersObject.addProperty("online", this.online);
// individual players
final JsonArray sampleArray = new JsonArray();
for (PingPlayer pingPlayer : this.pingPlayers) {
JsonObject pingPlayerObject = new JsonObject();
pingPlayerObject.addProperty("name", pingPlayer.name);
pingPlayerObject.addProperty("id", pingPlayer.uuid.toString());
sampleArray.add(pingPlayerObject);
}
playersObject.add("sample", sampleArray);
final JsonObject descriptionObject = GsonComponentSerializer.gson().serializer()
.toJsonTree(this.description).getAsJsonObject();
final JsonObject jsonObject = new JsonObject();
jsonObject.add("version", versionObject);
jsonObject.add("players", playersObject);
jsonObject.add("description", descriptionObject);
jsonObject.addProperty("favicon", this.favicon);
return jsonObject;
@Deprecated
public @NotNull JsonObject build() {
return PingResponse.FULL_RGB.getResponse(this);
}
/**
* Represents a player line in the server list hover.
* @deprecated See {@link NamedAndIdentified}
*/
@Deprecated
public static class PingPlayer {
private static @NotNull PingPlayer of(@NotNull String name, @NotNull UUID uuid) {

View File

@ -0,0 +1,64 @@
package net.minestom.server.ping;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.minestom.server.utils.identity.NamedAndIdentified;
import org.jetbrains.annotations.NotNull;
/**
* Vanilla ping responses.
*/
public class VanillaPingResponses {
private static final GsonComponentSerializer FULL_RGB = GsonComponentSerializer.gson(),
DOWNSAMPLE_RGB = GsonComponentSerializer.colorDownsamplingGson();
private static final LegacyComponentSerializer SECTION = LegacyComponentSerializer.legacySection();
private VanillaPingResponses() { }
/**
* Creates a modern ping response for client versions above the Netty rewrite.
*
* @param data the response data
* @param supportsFullRgb if the client supports full RGB
* @return the response object
*/
public static @NotNull JsonObject getModernPingResponse(@NotNull ResponseData data, boolean supportsFullRgb) {
// version
final JsonObject versionObject = new JsonObject();
versionObject.addProperty("name", data.getVersion());
versionObject.addProperty("protocol", data.getProtocol());
// players info
final JsonObject playersObject = new JsonObject();
playersObject.addProperty("max", data.getMaxPlayer());
playersObject.addProperty("online", data.getOnline());
// individual players
final JsonArray sampleArray = new JsonArray();
for (NamedAndIdentified entry : data.getEntries()) {
JsonObject playerObject = new JsonObject();
playerObject.addProperty("name", SECTION.serialize(entry.getName()));
playerObject.addProperty("id", entry.getUuid().toString());
sampleArray.add(playerObject);
}
playersObject.add("sample", sampleArray);
final JsonObject jsonObject = new JsonObject();
jsonObject.add("version", versionObject);
jsonObject.add("players", playersObject);
jsonObject.addProperty("favicon", data.getFavicon());
// description
if (supportsFullRgb) {
jsonObject.add("description", FULL_RGB.serializeToTree(data.getDescription()));
} else {
jsonObject.add("description", DOWNSAMPLE_RGB.serializeToTree(data.getDescription()));
}
return jsonObject;
}
}

View File

@ -16,6 +16,7 @@ import net.minestom.server.instance.block.rule.vanilla.RedstonePlacementRule;
import net.minestom.server.ping.ResponseData;
import net.minestom.server.storage.StorageManager;
import net.minestom.server.storage.systems.FileStorageSystem;
import net.minestom.server.utils.identity.NamedAndIdentified;
import net.minestom.server.utils.time.TimeUnit;
import net.minestom.server.utils.time.UpdateOption;
@ -68,35 +69,27 @@ public class Main {
ResponseData responseData = event.getResponseData();
responseData.setMaxPlayer(0);
responseData.setOnline(MinecraftServer.getConnectionManager().getOnlinePlayers().size());
responseData.addPlayer("The first line is separated from the others", UUID.randomUUID());
responseData.addPlayer("Could be a name, or a message", UUID.randomUUID());
responseData.addPlayer("IP test: " + event.getConnection().getRemoteAddress().toString(), UUID.randomUUID());
responseData.addPlayer("Use " + (char)0x00a7 + "7section characters", UUID.randomUUID());
responseData.addPlayer((char)0x00a7 + "7" + (char)0x00a7 + "ofor formatting" + (char)0x00a7 + "r: (" + (char)0x00a7 + "6char" + (char)0x00a7 + "r)" + (char)0x00a7 + "90x00a7", UUID.randomUUID());
responseData.addEntry(NamedAndIdentified.named("The first line is separated from the others"));
responseData.addEntry(NamedAndIdentified.named("Could be a name, or a message"));
responseData.addEntry(NamedAndIdentified.named("IP test: " + event.getConnection().getRemoteAddress().toString()));
responseData.addPlayer("Connection Info:");
// components will be converted the legacy section sign format so they are displayed in the client
responseData.addEntry(NamedAndIdentified.named(Component.text("You can use").append(Component.text("styling too!", NamedTextColor.RED))));
responseData.addEntry(NamedAndIdentified.named("Connection Info:"));
String ip = event.getConnection().getServerAddress();
responseData.addPlayer((char)0x00a7 + "8- " + (char)0x00a7 +"7IP: " + (char)0x00a7 + "e" + (ip != null ? ip : "???"));
responseData.addPlayer((char)0x00a7 + "8- " + (char)0x00a7 +"7PORT: " + (char)0x00a7 + "e" + event.getConnection().getServerPort());
responseData.addPlayer((char)0x00a7 + "8- " + (char)0x00a7 +"7VERSION: " + (char)0x00a7 + "e" + event.getConnection().getProtocolVersion());
// Check if client supports RGB color
if (event.getConnection().getProtocolVersion() >= 713) { // Snapshot 20w17a
responseData.setDescription(Component.text("You can do ")
.append(Component.text("RGB", TextColor.color(0x66b3ff)))
.append(Component.text(" color here")));
} else {
responseData.setDescription(Component.text("You can do ")
.append(Component.text("RGB", NamedTextColor.nearestTo(TextColor.color(0x66b3ff))))
.append(Component.text(" color here,"))
.append(Component.newline())
.append(Component.text("if you are on 1.16 or up"))
);
}
responseData.addEntry(NamedAndIdentified.named(Component.text('-', NamedTextColor.DARK_GRAY)
.append(Component.text("IP: ", NamedTextColor.GRAY))
.append(Component.text(ip != null ? ip : "???", NamedTextColor.YELLOW))));
responseData.addEntry(NamedAndIdentified.named(Component.text('-', NamedTextColor.DARK_GRAY)
.append(Component.text("PORT: ", NamedTextColor.GRAY))
.append(Component.text(event.getConnection().getServerPort()))));
responseData.addEntry(NamedAndIdentified.named(Component.text('-', NamedTextColor.DARK_GRAY)
.append(Component.text("VERSION: ", NamedTextColor.GRAY))
.append(Component.text(event.getConnection().getProtocolVersion()))));
// the data will be automatically converted to the correct format on response, so you can do RGB and it'll be downsampled!
responseData.setDescription(Component.text("This will be downsampled on older versions!", TextColor.color(0x66b3ff)));
});
PlayerInit.init();