diff --git a/build.gradle b/build.gradle index d6c733786..d8f431fc1 100644 --- a/build.gradle +++ b/build.gradle @@ -128,7 +128,7 @@ dependencies { api 'io.netty:netty-codec:4.1.63.Final' api 'io.netty:netty-transport-native-epoll:4.1.63.Final:linux-x86_64' api 'io.netty:netty-transport-native-kqueue:4.1.63.Final:osx-x86_64' - api 'io.netty.incubator:netty-incubator-transport-native-io_uring:0.0.4.Final:linux-x86_64' + api 'io.netty.incubator:netty-incubator-transport-native-io_uring:0.0.5.Final:linux-x86_64' // https://mvnrepository.com/artifact/org.apache.commons/commons-text compile group: 'org.apache.commons', name: 'commons-text', version: '1.9' diff --git a/src/main/java/net/minestom/server/MinecraftServer.java b/src/main/java/net/minestom/server/MinecraftServer.java index 8701fc915..5abf69e3e 100644 --- a/src/main/java/net/minestom/server/MinecraftServer.java +++ b/src/main/java/net/minestom/server/MinecraftServer.java @@ -626,7 +626,9 @@ public final class MinecraftServer { * Gets the consumer executed to show server-list data. * * @return the response data consumer + * @deprecated listen to the {@link net.minestom.server.event.server.ServerListPingEvent} instead */ + @Deprecated public static ResponseDataConsumer getResponseDataConsumer() { checkInitStatus(responseDataConsumer); return responseDataConsumer; @@ -752,15 +754,30 @@ public final class MinecraftServer { * @param port the server port * @param responseDataConsumer the response data consumer, can be null * @throws IllegalStateException if called before {@link #init()} or if the server is already running + * @deprecated use {@link #start(String, int)} and listen to the {@link net.minestom.server.event.server.ServerListPingEvent} event instead of ResponseDataConsumer */ + @Deprecated public void start(@NotNull String address, int port, @Nullable ResponseDataConsumer responseDataConsumer) { + MinecraftServer.responseDataConsumer = responseDataConsumer; + start(address, port); + } + + /** + * Starts the server. + *

+ * It should be called after {@link #init()} and probably your own initialization code. + * + * @param address the server address + * @param port the server port + * @throws IllegalStateException if called before {@link #init()} or if the server is already running + */ + public void start(@NotNull String address, int port) { Check.stateCondition(!initialized, "#start can only be called after #init"); Check.stateCondition(started, "The server is already started"); MinecraftServer.started = true; LOGGER.info("Starting Minestom server."); - MinecraftServer.responseDataConsumer = responseDataConsumer; updateManager.start(); @@ -788,17 +805,6 @@ public final class MinecraftServer { MinestomTerminal.start(); } - /** - * Starts the server. - * - * @param address the server address - * @param port the server port - * @see #start(String, int, ResponseDataConsumer) - */ - public void start(@NotNull String address, int port) { - start(address, port, null); - } - /** * Stops this server properly (saves if needed, kicking players, etc.) */ diff --git a/src/main/java/net/minestom/server/event/server/ServerListPingEvent.java b/src/main/java/net/minestom/server/event/server/ServerListPingEvent.java new file mode 100644 index 000000000..490a10d91 --- /dev/null +++ b/src/main/java/net/minestom/server/event/server/ServerListPingEvent.java @@ -0,0 +1,61 @@ +package net.minestom.server.event.server; + +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 org.jetbrains.annotations.NotNull; + +/** + * 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; + + + public ServerListPingEvent(ResponseData responseData, PlayerConnection connection) { + this.responseData = responseData; + this.connection = connection; + } + + /** + * ResponseData being returned. + * + * @return the response data being returned + */ + public @NotNull ResponseData getResponseData() { + return responseData; + } + + /** + * PlayerConnection of received packet. + * + * Note that the player has not joined the server at this time. + * + * @return the playerConnection. + */ + public @NotNull PlayerConnection getConnection() { + return connection; + } + + + @Override + public boolean isCancelled() { + return cancelled; + } + + /** + * Cancelling this event will cause you server to appear offline in the vanilla server list. + * + * @param cancel true if the event should be cancelled, false otherwise + */ + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + +} diff --git a/src/main/java/net/minestom/server/network/packet/client/handshake/HandshakePacket.java b/src/main/java/net/minestom/server/network/packet/client/handshake/HandshakePacket.java index efe60b9b8..e6f8cbe34 100644 --- a/src/main/java/net/minestom/server/network/packet/client/handshake/HandshakePacket.java +++ b/src/main/java/net/minestom/server/network/packet/client/handshake/HandshakePacket.java @@ -81,7 +81,6 @@ public class HandshakePacket implements ClientPreplayPacket { nettyPlayerConnection.UNSAFE_setBungeeUuid(playerUuid); nettyPlayerConnection.UNSAFE_setBungeeSkin(playerSkin); - } else { nettyPlayerConnection.sendPacket(new LoginDisconnectPacket(INVALID_BUNGEE_FORWARDING)); nettyPlayerConnection.disconnect(); @@ -93,6 +92,11 @@ public class HandshakePacket implements ClientPreplayPacket { } } + if (connection instanceof NettyPlayerConnection) { + // Give to the connection the server info that the client used + ((NettyPlayerConnection) connection).refreshServerInformation(serverAddress, serverPort, protocolVersion); + } + switch (nextState) { case 1: connection.setConnectionState(ConnectionState.STATUS); @@ -100,11 +104,6 @@ public class HandshakePacket implements ClientPreplayPacket { case 2: if (protocolVersion == MinecraftServer.PROTOCOL_VERSION) { connection.setConnectionState(ConnectionState.LOGIN); - - if (connection instanceof NettyPlayerConnection) { - // Give to the connection the server info that the client used - ((NettyPlayerConnection) connection).refreshServerInformation(serverAddress, serverPort); - } } else { // Incorrect client version connection.sendPacket(new LoginDisconnectPacket(INVALID_VERSION_TEXT)); diff --git a/src/main/java/net/minestom/server/network/packet/client/status/StatusRequestPacket.java b/src/main/java/net/minestom/server/network/packet/client/status/StatusRequestPacket.java index 24da7a1f0..f7f0c9622 100644 --- a/src/main/java/net/minestom/server/network/packet/client/status/StatusRequestPacket.java +++ b/src/main/java/net/minestom/server/network/packet/client/status/StatusRequestPacket.java @@ -2,6 +2,7 @@ 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; @@ -29,10 +30,16 @@ public class StatusRequestPacket implements ClientPreplayPacket { if (consumer != null) consumer.accept(connection, responseData); - ResponsePacket responsePacket = new ResponsePacket(); - responsePacket.jsonResponse = responseData.build().toString(); + // Call event + ServerListPingEvent statusRequestEvent = new ServerListPingEvent(responseData, connection); + MinecraftServer.getGlobalEventHandler().callCancellableEvent(ServerListPingEvent.class, statusRequestEvent, + () -> { + ResponsePacket responsePacket = new ResponsePacket(); + responsePacket.jsonResponse = responseData.build().toString(); + + connection.sendPacket(responsePacket); + }); - connection.sendPacket(responsePacket); } @Override diff --git a/src/main/java/net/minestom/server/network/player/NettyPlayerConnection.java b/src/main/java/net/minestom/server/network/player/NettyPlayerConnection.java index 70f570ed1..5d7167d70 100644 --- a/src/main/java/net/minestom/server/network/player/NettyPlayerConnection.java +++ b/src/main/java/net/minestom/server/network/player/NettyPlayerConnection.java @@ -51,6 +51,7 @@ public class NettyPlayerConnection extends PlayerConnection { private String loginUsername; private String serverAddress; private int serverPort; + private int protocolVersion; // Used for the login plugin request packet, to retrieve the channel from a message id, // cleared once the player enters the play state @@ -228,6 +229,7 @@ public class NettyPlayerConnection extends PlayerConnection { this.remoteAddress = remoteAddress; } + @Override public void disconnect() { this.channel.close(); @@ -266,8 +268,8 @@ public class NettyPlayerConnection extends PlayerConnection { * * @return the server address used */ - @Nullable - public String getServerAddress() { + @Override + public @Nullable String getServerAddress() { return serverAddress; } @@ -278,10 +280,36 @@ public class NettyPlayerConnection extends PlayerConnection { * * @return the server port used */ + @Override public int getServerPort() { return serverPort; } + /** + * Gets the protocol version of a client. + * + * @return protocol version of client. + */ + @Override + public int getProtocolVersion() { + return protocolVersion; + } + + + /** + * Used in {@link net.minestom.server.network.packet.client.handshake.HandshakePacket} to change the internal fields. + * + * @param serverAddress the server address which the client used + * @param serverPort the server port which the client used + * @param protocolVersion the protocol version which the client used + */ + public void refreshServerInformation(@Nullable String serverAddress, int serverPort, int protocolVersion) { + this.serverAddress = serverAddress; + this.serverPort = serverPort; + this.protocolVersion = protocolVersion; + } + + @Nullable public UUID getBungeeUuid() { return bungeeUuid; @@ -339,16 +367,6 @@ public class NettyPlayerConnection extends PlayerConnection { } } - /** - * Used in {@link net.minestom.server.network.packet.client.handshake.HandshakePacket} to change the internal fields. - * - * @param serverAddress the server address which the client used - * @param serverPort the server port which the client used - */ - public void refreshServerInformation(@Nullable String serverAddress, int serverPort) { - this.serverAddress = serverAddress; - this.serverPort = serverPort; - } @NotNull public ByteBuf getTickBuffer() { diff --git a/src/main/java/net/minestom/server/network/player/PlayerConnection.java b/src/main/java/net/minestom/server/network/player/PlayerConnection.java index e183c772a..730836dbd 100644 --- a/src/main/java/net/minestom/server/network/player/PlayerConnection.java +++ b/src/main/java/net/minestom/server/network/player/PlayerConnection.java @@ -117,6 +117,40 @@ public abstract class PlayerConnection { @NotNull public abstract SocketAddress getRemoteAddress(); + + /** + * Gets protocol version of client. + * + * @return the protocol version + */ + public int getProtocolVersion() { + return MinecraftServer.PROTOCOL_VERSION; + } + + /** + * Gets the server address that the client used to connect. + *

+ * WARNING: it is given by the client, it is possible for it to be wrong. + * + * @return the server address used + */ + public @Nullable String getServerAddress() { + return MinecraftServer.getNettyServer().getAddress(); + } + + + /** + * Gets the server port that the client used to connect. + *

+ * WARNING: it is given by the client, it is possible for it to be wrong. + * + * @return the server port used + */ + public int getServerPort() { + return MinecraftServer.getNettyServer().getPort(); + } + + /** * Forcing the player to disconnect. */ diff --git a/src/main/java/net/minestom/server/ping/ResponseData.java b/src/main/java/net/minestom/server/ping/ResponseData.java index 8c5f43ed0..6c2a03b6d 100644 --- a/src/main/java/net/minestom/server/ping/ResponseData.java +++ b/src/main/java/net/minestom/server/ping/ResponseData.java @@ -15,8 +15,7 @@ import java.util.UUID; /** * Represents the data sent to the player when refreshing the server list. * - *

Filled by {@link ResponseDataConsumer} and specified in {@link - * net.minestom.server.MinecraftServer#start(String, int, ResponseDataConsumer)}. + *

Edited by listening to the {@link net.minestom.server.event.server.ServerListPingEvent}. */ public class ResponseData { private final List pingPlayers; @@ -55,6 +54,15 @@ public class ResponseData { this.version = version; } + /** + * Get the version name for the response. + * + * @return the version name for the response. + */ + public String getVersion() { + return version; + } + /** * Sets the response protocol version. * @@ -64,6 +72,15 @@ public class ResponseData { this.protocol = protocol; } + /** + * Get the response protocol version. + * + * @return the response protocol version. + */ + public int getProtocol() { + return protocol; + } + /** * Sets the response maximum player count. * @@ -73,6 +90,15 @@ public class ResponseData { this.maxPlayer = maxPlayer; } + /** + * Get the response maximum player count. + * + * @return the response maximum player count. + */ + public int getMaxPlayer() { + return maxPlayer; + } + /** * Sets the response online count. * @@ -82,6 +108,15 @@ public class ResponseData { this.online = online; } + /** + * Get the response online count. + * + * @return the response online count. + */ + public int getOnline() { + return online; + } + /** * Adds some players to the response. * @@ -109,9 +144,19 @@ public class ResponseData { * @param uuid The unique identifier of the player. */ public void addPlayer(String name, UUID uuid) { - PingPlayer pingPlayer = new PingPlayer(); - pingPlayer.name = name; - pingPlayer.uuid = uuid; + PingPlayer pingPlayer = PingPlayer.of(name, uuid); + this.pingPlayers.add(pingPlayer); + } + + /** + * Adds a player to the response. + *

+ * {@link UUID#randomUUID()} is used as the player's UUID. + * + * @param name The name of the player. + */ + public void addPlayer(String name) { + PingPlayer pingPlayer = PingPlayer.of(name, UUID.randomUUID()); this.pingPlayers.add(pingPlayer); } @@ -123,6 +168,15 @@ public class ResponseData { this.pingPlayers.clear(); } + /** + * Get the list of the response players. + * + * @return the list of the response players. + */ + public List getPlayers() { + return pingPlayers; + } + /** * Sets the response description. * @@ -143,8 +197,19 @@ public class ResponseData { this.description = description; } + /** + * Get the response description + * + * @return the response description + */ + public Component getDescription() { + return description; + } + /** * Sets the response favicon. + *

+ * MUST start with "data:image/png;base64," * * @param favicon The favicon for the response data. */ @@ -152,6 +217,16 @@ public class ResponseData { this.favicon = favicon; } + /** + * Get the response favicon. + * + * @return the response favicon. + */ + public String getFavicon() { + return favicon; + } + + /** * Converts the response data into a {@link JsonObject}. * @@ -193,8 +268,26 @@ public class ResponseData { /** * Represents a player line in the server list hover. */ - private static class PingPlayer { - private String name; - private UUID uuid; + public static class PingPlayer { + + private static @NotNull PingPlayer of(@NotNull String name, @NotNull UUID uuid) { + return new PingPlayer(name, uuid); + } + + private final String name; + private final UUID uuid; + + private PingPlayer(@NotNull String name, @NotNull UUID uuid) { + this.name = name; + this.uuid = uuid; + } + + public @NotNull String getName() { + return name; + } + + public @NotNull UUID getUuid() { + return uuid; + } } } diff --git a/src/main/java/net/minestom/server/ping/ResponseDataConsumer.java b/src/main/java/net/minestom/server/ping/ResponseDataConsumer.java index 251ef7179..21795703f 100644 --- a/src/main/java/net/minestom/server/ping/ResponseDataConsumer.java +++ b/src/main/java/net/minestom/server/ping/ResponseDataConsumer.java @@ -7,15 +7,18 @@ import net.minestom.server.network.player.PlayerConnection; * *

Can be specified in {@link net.minestom.server.MinecraftServer#start(String, int, * ResponseDataConsumer)}. + * + * @deprecated listen to the {@link net.minestom.server.event.server.ServerListPingEvent} instead */ @FunctionalInterface +@Deprecated public interface ResponseDataConsumer { - /** - * A method to fill the data of the response. - * - * @param playerConnection The player connection to which the response should be sent. - * @param responseData The data for the response. - */ - void accept(PlayerConnection playerConnection, ResponseData responseData); + /** + * A method to fill the data of the response. + * + * @param playerConnection The player connection to which the response should be sent. + * @param responseData The data for the response. + */ + void accept(PlayerConnection playerConnection, ResponseData responseData); } diff --git a/src/main/java/net/minestom/server/terminal/MinestomTerminal.java b/src/main/java/net/minestom/server/terminal/MinestomTerminal.java index b0123f899..cd5c1efe1 100644 --- a/src/main/java/net/minestom/server/terminal/MinestomTerminal.java +++ b/src/main/java/net/minestom/server/terminal/MinestomTerminal.java @@ -17,11 +17,11 @@ public class MinestomTerminal { private static final CommandManager COMMAND_MANAGER = MinecraftServer.getCommandManager(); private static final String PROMPT = "> "; + private static volatile Terminal terminal; private static volatile boolean running = false; @ApiStatus.Internal public static void start() { - Terminal terminal = null; try { terminal = TerminalBuilder.terminal(); } catch (IOException e) { @@ -47,6 +47,13 @@ public class MinestomTerminal { @ApiStatus.Internal public static void stop() { running = false; + if (terminal != null) { + try { + terminal.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } } } diff --git a/src/test/java/demo/Main.java b/src/test/java/demo/Main.java index ac677a688..850b93833 100644 --- a/src/test/java/demo/Main.java +++ b/src/test/java/demo/Main.java @@ -6,16 +6,21 @@ import demo.blocks.UpdatableBlockDemo; import demo.commands.*; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; import net.minestom.server.MinecraftServer; import net.minestom.server.command.CommandManager; +import net.minestom.server.event.server.ServerListPingEvent; import net.minestom.server.extras.optifine.OptifineSupport; import net.minestom.server.instance.block.BlockManager; 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.time.TimeUnit; import net.minestom.server.utils.time.UpdateOption; +import java.util.UUID; + public class Main { @@ -59,6 +64,41 @@ public class Main { MinecraftServer.getSchedulerManager().buildShutdownTask(() -> System.out.println("Good night")).schedule(); + MinecraftServer.getGlobalEventHandler().addEventCallback(ServerListPingEvent.class, event -> { + 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.addPlayer("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")) + ); + } + + + + + }); + PlayerInit.init(); OptifineSupport.enable(); @@ -68,7 +108,7 @@ public class Main { //MojangAuth.init(); - minecraftServer.start("0.0.0.0", 25565, PlayerInit.getResponseDataConsumer()); + minecraftServer.start("0.0.0.0", 25565); //Runtime.getRuntime().addShutdownHook(new Thread(MinecraftServer::stopCleanly)); } diff --git a/src/test/java/demo/PlayerInit.java b/src/test/java/demo/PlayerInit.java index d654fc1cc..043319dcc 100644 --- a/src/test/java/demo/PlayerInit.java +++ b/src/test/java/demo/PlayerInit.java @@ -4,6 +4,7 @@ import com.google.common.util.concurrent.AtomicDouble; import demo.generator.ChunkGeneratorDemo; import demo.generator.NoiseTestGenerator; import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; import net.minestom.server.MinecraftServer; import net.minestom.server.adventure.audience.Audiences; import net.minestom.server.chat.ColoredText; @@ -292,14 +293,5 @@ public class PlayerInit { }); } - public static ResponseDataConsumer getResponseDataConsumer() { - return (playerConnection, responseData) -> { - responseData.setMaxPlayer(0); - responseData.setOnline(MinecraftServer.getConnectionManager().getOnlinePlayers().size()); - responseData.addPlayer("A name", UUID.randomUUID()); - responseData.addPlayer("Could be some message", UUID.randomUUID()); - responseData.setDescription("IP test: " + playerConnection.getRemoteAddress()); - }; - } }