Make legacy server pings use the ServerListPingEvent

This commit is contained in:
Kieran Wallbanks 2021-04-19 15:47:46 +01:00
parent 42e1811b7c
commit a15e3aef44
8 changed files with 286 additions and 237 deletions

View File

@ -1,29 +1,59 @@
package net.minestom.server.event.server;
import net.minestom.server.MinecraftServer;
import net.minestom.server.event.CancellableEvent;
import net.minestom.server.event.Event;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.ping.ResponseData;
import net.minestom.server.ping.ResponseDataConsumer;
import net.minestom.server.ping.ServerListPingVersion;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
/**
* Called when a {@link PlayerConnection} sends a status packet,
* usually to display information on the server list.
*/
public class ServerListPingEvent extends Event implements CancellableEvent {
private boolean cancelled = false;
private final ResponseData responseData;
private final PlayerConnection connection;
private final ServerListPingVersion version;
private boolean cancelled = false;
private ResponseData responseData;
public ServerListPingEvent(ResponseData responseData, PlayerConnection connection) {
this.responseData = responseData;
this.connection = connection;
/**
* Creates a new server list ping event with no player connection.
*
* @param version the ping version to respond with
*/
public ServerListPingEvent(@NotNull ServerListPingVersion version) {
this(null, version);
}
/**
* ResponseData being returned.
* Creates a new server list ping event.
*
* @param connection the player connection, if the ping version is modern
* @param version the ping version to respond with
*/
public ServerListPingEvent(@Nullable PlayerConnection connection, @NotNull ServerListPingVersion version) {
//noinspection deprecation we need to continue doing this until the consumer is removed
ResponseDataConsumer consumer = MinecraftServer.getResponseDataConsumer();
this.responseData = new ResponseData();
if (consumer != null) {
consumer.accept(connection, responseData);
}
this.connection = connection;
this.version = version;
}
/**
* Gets the response data that is sent to the client.
* This is mutable and can be modified to change what is returned.
*
* @return the response data being returned
*/
@ -32,16 +62,34 @@ public class ServerListPingEvent extends Event implements CancellableEvent {
}
/**
* PlayerConnection of received packet.
* Sets the response data, overwriting the exiting data.
*
* @param responseData the new data
*/
public void setResponseData(@NotNull ResponseData responseData) {
this.responseData = Objects.requireNonNull(responseData);
}
/**
* PlayerConnection of received packet.
* Note that the player has not joined the server at this time.
* This will be null for legacy server list pings.
*
* @return the playerConnection.
*/
public @NotNull PlayerConnection getConnection() {
public @Nullable PlayerConnection getConnection() {
return connection;
}
/**
* Gets the ping version that the client is pinging with.
*
* @return the ping version
* @see ServerListPingVersion
*/
public @NotNull ServerListPingVersion getPingVersion() {
return version;
}
@Override
public boolean isCancelled() {
@ -57,5 +105,4 @@ public class ServerListPingEvent extends Event implements CancellableEvent {
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
}

View File

@ -6,22 +6,23 @@ import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import net.minestom.server.MinecraftServer;
import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.ping.ServerListPingVersion;
import org.jetbrains.annotations.NotNull;
import java.nio.charset.StandardCharsets;
// Copied from original minecraft :(
public class LegacyPingHandler extends ChannelInboundHandlerAdapter {
private ByteBuf buf;
@Override
public void channelRead(@NotNull ChannelHandlerContext ctx, @NotNull Object object) {
ByteBuf buf = (ByteBuf) object;
final ByteBuf buf = (ByteBuf) object;
if (this.buf != null) {
try {
readLegacy1_6(ctx, buf);
handle1_6(ctx, buf);
} finally {
buf.release();
}
@ -38,19 +39,17 @@ public class LegacyPingHandler extends ChannelInboundHandlerAdapter {
switch (length) {
case 0:
this.writeResponse(ctx, this.createResponse(formatResponse(-2)));
if (trySendResponse(ServerListPingVersion.LEGACY, ctx)) return;
break;
case 1:
if (buf.readUnsignedByte() != 1) {
return;
}
if (buf.readUnsignedByte() != 1) return;
this.writeResponse(ctx, this.createResponse(formatResponse(-1)));
if (trySendResponse(ServerListPingVersion.LEGACY_VERSIONED, ctx)) return;
break;
default:
if (buf.readUnsignedByte() != 0x01 || buf.readUnsignedByte() != 0xFA) return;
readLegacy1_6(ctx, buf);
handle1_6(ctx, buf);
break;
}
@ -66,19 +65,7 @@ public class LegacyPingHandler extends ChannelInboundHandlerAdapter {
}
}
private static String readLegacyString(ByteBuf buf) {
int size = buf.readShort() * Character.BYTES;
if (!buf.isReadable(size)) {
return null;
}
final String result = buf.toString(buf.readerIndex(), size, StandardCharsets.UTF_16BE);
buf.skipBytes(size);
return result;
}
private void readLegacy1_6(ChannelHandlerContext ctx, ByteBuf part) {
private void handle1_6(ChannelHandlerContext ctx, ByteBuf part) {
ByteBuf buf = this.buf;
if (buf == null) {
@ -127,27 +114,7 @@ public class LegacyPingHandler extends ChannelInboundHandlerAdapter {
this.buf = null;
this.writeResponse(ctx, this.createResponse(formatResponse(protocolVersion)));
}
private String formatResponse(int playerProtocol) {
final String motd = MinecraftServer.getBrandName();
final String version = MinecraftServer.VERSION_NAME;
final int online = MinecraftServer.getConnectionManager().getOnlinePlayers().size();
final int max = 0;
final int protocol = MinecraftServer.PROTOCOL_VERSION;
if (playerProtocol == -2) {
return String.format(
"%s\u00a7%d\u00a7%d",
motd, online, max
);
}
return String.format(
"\u00a71\u0000%d\u0000%s\u0000%s\u0000%d\u0000%d",
protocol, version, motd, online, max
);
trySendResponse(ServerListPingVersion.LEGACY_VERSIONED, ctx);
}
private void removeHandler(ChannelHandlerContext ctx) {
@ -167,22 +134,51 @@ public class LegacyPingHandler extends ChannelInboundHandlerAdapter {
}
}
private void writeResponse(ChannelHandlerContext ctx, ByteBuf buf) {
ctx.pipeline().firstContext().writeAndFlush(buf).addListener(ChannelFutureListener.CLOSE);
/**
* Calls a {@link ServerListPingEvent} and sends the response, if the event was not cancelled.
*
* @param version the version
* @param ctx the context
* @return {@code true} if the response was cancelled, {@code false} otherwise
*/
private static boolean trySendResponse(@NotNull ServerListPingVersion version, @NotNull ChannelHandlerContext ctx) {
final ServerListPingEvent event = new ServerListPingEvent(version);
MinecraftServer.getGlobalEventHandler().callEvent(ServerListPingEvent.class, event);
if (event.isCancelled()) {
return true;
} else {
// get the response string
String s = version.getPingResponse(event.getResponseData());
// create the buffer
ByteBuf response = Unpooled.buffer();
response.writeByte(255);
final char[] chars = s.toCharArray();
response.writeShort(chars.length);
for (char c : chars) {
response.writeChar(c);
}
// write the buffer
ctx.pipeline().firstContext().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
return false;
}
}
private ByteBuf createResponse(String s) {
ByteBuf response = Unpooled.buffer();
response.writeByte(255);
final char[] chars = s.toCharArray();
response.writeShort(chars.length);
for (char c : chars) {
response.writeChar(c);
private static String readLegacyString(ByteBuf buf) {
int size = buf.readShort() * Character.BYTES;
if (!buf.isReadable(size)) {
return null;
}
return response;
final String result = buf.toString(buf.readerIndex(), size, StandardCharsets.UTF_16BE);
buf.skipBytes(size);
return result;
}
}

View File

@ -1,55 +1,27 @@
package net.minestom.server.network.packet.client.status;
import net.kyori.adventure.text.Component;
import net.minestom.server.MinecraftServer;
import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.network.packet.client.ClientPreplayPacket;
import net.minestom.server.network.packet.server.handshake.ResponsePacket;
import net.minestom.server.network.player.PlayerConnection;
import net.minestom.server.ping.ResponseData;
import net.minestom.server.ping.ResponseDataConsumer;
import net.minestom.server.ping.ServerListPingVersion;
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();
ResponseData responseData = new ResponseData();
// Fill default params
responseData.setVersion(MinecraftServer.VERSION_NAME);
responseData.setProtocol(MinecraftServer.PROTOCOL_VERSION);
responseData.setMaxPlayer(0);
responseData.setOnline(0);
responseData.setDescription(Component.text("Minestom Server"));
responseData.setFavicon("");
if (consumer != null) {
consumer.accept(connection, responseData);
}
// Call event
ServerListPingEvent statusRequestEvent = new ServerListPingEvent(responseData, connection);
MinecraftServer.getGlobalEventHandler().callCancellableEvent(ServerListPingEvent.class, statusRequestEvent,
() -> {
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);
});
final ServerListPingVersion pingVersion = ServerListPingVersion.fromModernProtocolVersion(connection.getProtocolVersion());
final ServerListPingEvent statusRequestEvent = new ServerListPingEvent(connection, pingVersion);
MinecraftServer.getGlobalEventHandler().callCancellableEvent(ServerListPingEvent.class, statusRequestEvent, () -> {
final ResponsePacket responsePacket = new ResponsePacket();
responsePacket.jsonResponse = pingVersion.getPingResponse(statusRequestEvent.getResponseData());
connection.sendPacket(responsePacket);
});
}
@Override

View File

@ -1,51 +0,0 @@
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

@ -4,6 +4,7 @@ import com.google.gson.JsonObject;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.Player;
import net.minestom.server.event.server.ServerListPingEvent;
import net.minestom.server.utils.identity.NamedAndIdentified;
@ -18,13 +19,15 @@ import java.util.stream.Collectors;
* @see ServerListPingEvent
*/
public class ResponseData {
private static final Component DEFAULT_DESCRIPTION = Component.text("Minestom Server");
private final List<NamedAndIdentified> entries;
private String version;
private int protocol;
private int maxPlayer;
private int online;
private Component description;
private String favicon;
/**
@ -32,6 +35,12 @@ public class ResponseData {
*/
public ResponseData() {
this.entries = new ArrayList<>();
this.version = MinecraftServer.VERSION_NAME;
this.protocol = MinecraftServer.PROTOCOL_VERSION;
this.online = MinecraftServer.getConnectionManager().getOnlinePlayers().size();
this.maxPlayer = this.online + 1;
this.description = DEFAULT_DESCRIPTION;
this.favicon = "";
}
/**
@ -289,11 +298,11 @@ public class ResponseData {
* Converts the response data into a {@link JsonObject}.
*
* @return The converted response data as a json tree.
* @deprecated Use {@link PingResponse#getResponse(ResponseData)}
* @deprecated Use {@link ServerListPingVersion#getPingResponse(ResponseData)}
*/
@Deprecated
public @NotNull JsonObject build() {
return PingResponse.FULL_RGB.getResponse(this);
return ServerListPingVersion.getModernPingResponse(this, true);
}
/**

View File

@ -0,0 +1,135 @@
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.MinecraftServer;
import net.minestom.server.utils.identity.NamedAndIdentified;
import org.jetbrains.annotations.NotNull;
import java.util.function.Function;
/**
* An enum containing the different types of server list ping responses.
*
* @see <a href="https://wiki.vg/Server_List_Ping">https://wiki.vg/Server_List_Ping</a>
*/
public enum ServerListPingVersion {
/**
* The client is on version 1.16 or higher and supports full RGB with JSON text formatting.
*/
MODERN_FULL_RGB(data -> getModernPingResponse(data, true).toString()),
/**
* The client is on version 1.7 or higher and doesn't support full RGB but does support JSON text formatting.
*/
MODERN_NAMED_COLORS(data -> getModernPingResponse(data, false).toString()),
/**
* The client is on version 1.6 and supports a description, the player count and the version information.
*/
LEGACY_VERSIONED(data -> getLegacyPingResponse(data, true)),
/**
* The client is on version 1.5 or lower and supports a description and the player count.
*/
LEGACY(data -> getLegacyPingResponse(data, false));
private final Function<ResponseData, String> pingResponseCreator;
ServerListPingVersion(@NotNull Function<ResponseData, String> pingResponseCreator) {
this.pingResponseCreator = pingResponseCreator;
}
/**
* Gets the ping response for this version.
*
* @param responseData the response data
* @return the response
*/
public @NotNull String getPingResponse(@NotNull ResponseData responseData) {
return this.pingResponseCreator.apply(responseData);
}
private static final GsonComponentSerializer FULL_RGB = GsonComponentSerializer.gson(),
NAMED_RGB = GsonComponentSerializer.colorDownsamplingGson();
private static final LegacyComponentSerializer SECTION = LegacyComponentSerializer.legacySection();
/**
* Creates a legacy ping response for client versions below the Netty rewrite (1.6-).
*
* @param data the response data
* @param supportsVersions if the client supports recieving the versions of the server
* @return the response
*/
public static @NotNull String getLegacyPingResponse(@NotNull ResponseData data, boolean supportsVersions) {
final String motd = SECTION.serialize(data.getDescription());
if (supportsVersions) {
return String.format("\u00a71\u0000%d\u0000%s\u0000%s\u0000%d\u0000%d",
data.getProtocol(), data.getVersion(), motd, data.getOnline(), data.getMaxPlayer());
} else {
return String.format("%s\u00a7%d\u00a7%d", motd, data.getOnline(), data.getMaxPlayer());
}
}
/**
* Creates a modern ping response for client versions above the Netty rewrite (1.7+).
*
* @param data the response data
* @param supportsFullRgb if the client supports full RGB
* @return the response
*/
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", NAMED_RGB.serializeToTree(data.getDescription()));
}
return jsonObject;
}
/**
* Gets the server list ping version from the protocol version.
* This only works for modern ping responses since the Netty rewrite.
*
* @param version the protocol version
* @return the corresponding server list ping version
*/
public static @NotNull ServerListPingVersion fromModernProtocolVersion(int version) {
if (version >= 713) {
return MODERN_FULL_RGB;
} else {
return MODERN_NAMED_COLORS;
}
}
}

View File

@ -1,64 +0,0 @@
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

@ -5,8 +5,10 @@ import demo.blocks.CustomBlockSample;
import demo.blocks.UpdatableBlockDemo;
import demo.commands.*;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.minestom.server.MinecraftServer;
import net.minestom.server.command.CommandManager;
import net.minestom.server.event.server.ServerListPingEvent;
@ -67,29 +69,32 @@ public class Main {
MinecraftServer.getGlobalEventHandler().addEventCallback(ServerListPingEvent.class, event -> {
ResponseData responseData = event.getResponseData();
responseData.setMaxPlayer(0);
responseData.setOnline(MinecraftServer.getConnectionManager().getOnlinePlayers().size());
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()));
// on modern versions, you can obtain the player connection directly from the event
if (event.getConnection() != null) {
responseData.addEntry(NamedAndIdentified.named("IP test: " + event.getConnection().getRemoteAddress().toString()));
responseData.addEntry(NamedAndIdentified.named("Connection Info:"));
String ip = event.getConnection().getServerAddress();
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()))));
}
// 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.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()))));
responseData.addEntry(NamedAndIdentified.named(Component.text("You can use ").append(Component.text("styling too!", NamedTextColor.RED, TextDecoration.BOLD))));
// 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)));
// on legacy versions, colors will be converted to the section format so it'll work there too
responseData.setDescription(Component.text("This is a Minestom Server", TextColor.color(0x66b3ff)));
});
PlayerInit.init();