From e60e2063a87c8904b5ad89680aa51698b1ef8a0c Mon Sep 17 00:00:00 2001 From: Adrian <68704415+4drian3d@users.noreply.github.com> Date: Sun, 16 Jun 2024 10:56:09 -0500 Subject: [PATCH 1/3] Add Configuration State API (#1261) * Implement Configuration State events * Implement PlayerFinishedConfigurationEvent * Fixed PlayerFinishConfigurationEvent execution * Apply suggestions * Fix keep alive when blocking PlayerFinishConfigurationEvent * Add ServerConnection to configuration events * Separate PlayPacketQueueHandler to fix AutoReadHolderHandler --------- Co-authored-by: Gero --- .../PlayerEnterConfigurationEvent.java | 27 ++++++ .../PlayerFinishConfigurationEvent.java | 26 +++++ .../PlayerFinishedConfigurationEvent.java | 26 +++++ .../velocitypowered/proxy/VelocityServer.java | 2 +- .../proxy/connection/MinecraftConnection.java | 29 +++--- .../backend/BackendPlaySessionHandler.java | 4 +- .../backend/BungeeCordMessageResponder.java | 8 +- .../backend/ConfigSessionHandler.java | 28 +++--- .../backend/LoginSessionHandler.java | 7 +- .../client/ClientConfigSessionHandler.java | 10 +- .../client/ClientPlaySessionHandler.java | 4 +- .../connection/client/ConnectedPlayer.java | 39 ++++---- .../bundle}/BundleDelimiterHandler.java | 3 +- .../VelocityResourcePackInfo.java | 2 +- .../Legacy117ResourcePackHandler.java | 2 +- .../LegacyResourcePackHandler.java | 3 +- .../ModernResourcePackHandler.java | 3 +- .../{ => handler}/ResourcePackHandler.java | 5 +- .../proxy/network/Connections.java | 3 +- .../protocol/netty/MinecraftDecoder.java | 7 +- .../netty/PlayPacketQueueInboundHandler.java | 94 +++++++++++++++++++ ...va => PlayPacketQueueOutboundHandler.java} | 20 ++-- .../packet/ResourcePackRequestPacket.java | 2 +- 23 files changed, 264 insertions(+), 90 deletions(-) create mode 100644 api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnterConfigurationEvent.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishConfigurationEvent.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishedConfigurationEvent.java rename proxy/src/main/java/com/velocitypowered/proxy/connection/{client => player/bundle}/BundleDelimiterHandler.java (95%) rename proxy/src/main/java/com/velocitypowered/proxy/connection/player/{ => resourcepack}/VelocityResourcePackInfo.java (98%) rename proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/{ => handler}/Legacy117ResourcePackHandler.java (99%) rename proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/{ => handler}/LegacyResourcePackHandler.java (98%) rename proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/{ => handler}/ModernResourcePackHandler.java (98%) rename proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/{ => handler}/ResourcePackHandler.java (97%) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueInboundHandler.java rename proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/{PlayPacketQueueHandler.java => PlayPacketQueueOutboundHandler.java} (86%) diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnterConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnterConfigurationEvent.java new file mode 100644 index 000000000..3b108c6a7 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnterConfigurationEvent.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.event.player.configuration; + +import com.velocitypowered.api.network.ProtocolState; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import org.jetbrains.annotations.NotNull; + +/** + * This event is executed when a player with version 1.20.2 or higher enters the configuration phase. + *

From this moment on, until the {@link PlayerFinishedConfigurationEvent} is executed, + * the {@linkplain Player#getProtocolState()} method is guaranteed + * to return {@link ProtocolState#CONFIGURATION}.

+ * + * @param player The player that has entered the configuration phase. + * @param server The server that will now (re-)configure the player. + * @since 3.3.0 + * @sinceMinecraft 1.20.2 + */ +public record PlayerEnterConfigurationEvent(@NotNull Player player, ServerConnection server) { +} diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishConfigurationEvent.java new file mode 100644 index 000000000..f6249b897 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishConfigurationEvent.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.event.player.configuration; + +import com.velocitypowered.api.event.annotation.AwaitingEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import org.jetbrains.annotations.NotNull; + +/** + * This event is executed when the player is about to finish the Configuration state. + *

Velocity will wait for this event to finish the configuration phase on the client.

+ * + * @param player The player who is about to complete the configuration phase. + * @param server The server that is currently (re-)configuring the player. + * @since 3.3.0 + * @sinceMinecraft 1.20.2 + */ +@AwaitingEvent +public record PlayerFinishConfigurationEvent(@NotNull Player player, @NotNull ServerConnection server) { +} diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishedConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishedConfigurationEvent.java new file mode 100644 index 000000000..09e76104f --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishedConfigurationEvent.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.event.player.configuration; + +import com.velocitypowered.api.network.ProtocolState; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import org.jetbrains.annotations.NotNull; + +/** + * Event executed when a player of version 1.20.2 or higher finishes the Configuration state. + *

From this moment on, the {@link Player#getProtocolState()} method + * will return {@link ProtocolState#PLAY}.

+ * + * @param player The player who has completed the Configuration state + * @param server The server that has (re-)configured the player. + * @since 3.3.0 + * @sinceMinecraft 1.20.2 + */ +public record PlayerFinishedConfigurationEvent(@NotNull Player player, @NotNull ServerConnection server) { +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index ef8217c69..004243616 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -45,7 +45,7 @@ import com.velocitypowered.proxy.command.builtin.ShutdownCommand; import com.velocitypowered.proxy.command.builtin.VelocityCommand; import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo; import com.velocitypowered.proxy.connection.util.ServerListPingHandler; import com.velocitypowered.proxy.console.VelocityConsole; import com.velocitypowered.proxy.crypto.EncryptionUtils; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java index b1c91ebdb..c91739078 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -47,7 +47,8 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftCompressorAndLengthEnco import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; -import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueHandler; +import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueInboundHandler; +import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueOutboundHandler; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.util.except.QuietDecoderException; import io.netty.buffer.ByteBuf; @@ -148,13 +149,11 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { return; } - if (msg instanceof MinecraftPacket) { - MinecraftPacket pkt = (MinecraftPacket) msg; + if (msg instanceof MinecraftPacket pkt) { if (!pkt.handle(activeSessionHandler)) { activeSessionHandler.handleGeneric((MinecraftPacket) msg); } - } else if (msg instanceof HAProxyMessage) { - HAProxyMessage proxyMessage = (HAProxyMessage) msg; + } else if (msg instanceof HAProxyMessage proxyMessage) { this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(), proxyMessage.sourcePort()); } else if (msg instanceof ByteBuf) { @@ -383,9 +382,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { if (state == StateRegistry.CONFIG) { // Activate the play packet queue addPlayPacketQueueHandler(); - } else if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE) != null) { + } else { // Remove the queue - this.channel.pipeline().remove(Connections.PLAY_PACKET_QUEUE); + if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) != null) { + this.channel.pipeline().remove(Connections.PLAY_PACKET_QUEUE_OUTBOUND); + } + if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) != null) { + this.channel.pipeline().remove(Connections.PLAY_PACKET_QUEUE_INBOUND); + } } } @@ -393,10 +397,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { * Adds the play packet queue handler. */ public void addPlayPacketQueueHandler() { - if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE) == null) { - this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE, - new PlayPacketQueueHandler(this.protocolVersion, - channel.pipeline().get(MinecraftEncoder.class).getDirection())); + if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) == null) { + this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE_OUTBOUND, + new PlayPacketQueueOutboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftEncoder.class).getDirection())); + } + if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) == null) { + this.channel.pipeline().addAfter(Connections.MINECRAFT_DECODER, Connections.PLAY_PACKET_QUEUE_INBOUND, + new PlayPacketQueueInboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftDecoder.class).getDirection())); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index 5b83e6549..14989b1a3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -40,8 +40,8 @@ import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; -import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackHandler; +import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java index 3c2691781..ea28d2c97 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java @@ -37,7 +37,6 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.util.Optional; import java.util.StringJoiner; -import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.ComponentSerializer; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; @@ -143,7 +142,7 @@ public class BungeeCordMessageResponder { out.writeUTF("PlayerList"); out.writeUTF(info.getServerInfo().getName()); - StringJoiner joiner = new StringJoiner(", "); + final StringJoiner joiner = new StringJoiner(", "); for (Player online : info.getPlayersConnected()) { joiner.add(online.getUsername()); } @@ -187,10 +186,9 @@ public class BungeeCordMessageResponder { Component messageComponent = serializer.deserialize(message); if (target.equals("ALL")) { - proxy.sendMessage(Identity.nil(), messageComponent); + proxy.sendMessage(messageComponent); } else { - proxy.getPlayer(target).ifPresent(player -> player.sendMessage(Identity.nil(), - messageComponent)); + proxy.getPlayer(target).ifPresent(player -> player.sendMessage(messageComponent)); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java index 3f4325e52..a19854eb9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java @@ -29,7 +29,7 @@ import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.client.ClientConfigSessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; @@ -132,7 +132,8 @@ public class ConfigSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(KeepAlivePacket packet) { - serverConn.ensureConnected().write(packet); + serverConn.getPendingPings().put(packet.getRandomId(), System.nanoTime()); + serverConn.getPlayer().getConnection().write(packet); return true; } @@ -193,30 +194,25 @@ public class ConfigSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(FinishedUpdatePacket packet) { - MinecraftConnection smc = serverConn.ensureConnected(); - ConnectedPlayer player = serverConn.getPlayer(); - ClientConfigSessionHandler configHandler = - (ClientConfigSessionHandler) player.getConnection().getActiveSessionHandler(); + final MinecraftConnection smc = serverConn.ensureConnected(); + final ConnectedPlayer player = serverConn.getPlayer(); + final ClientConfigSessionHandler configHandler = (ClientConfigSessionHandler) player.getConnection().getActiveSessionHandler(); - smc.setAutoReading(false); - // Even when not auto reading messages are still decoded. Decode them with the correct state smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.PLAY); - configHandler.handleBackendFinishUpdate(serverConn).thenAcceptAsync((unused) -> { + //noinspection DataFlowIssue + configHandler.handleBackendFinishUpdate(serverConn).thenRunAsync(() -> { + smc.write(FinishedUpdatePacket.INSTANCE); if (serverConn == player.getConnectedServer()) { smc.setActiveSessionHandler(StateRegistry.PLAY); - player.sendPlayerListHeaderAndFooter( - player.getPlayerListHeader(), player.getPlayerListFooter()); + player.sendPlayerListHeaderAndFooter(player.getPlayerListHeader(), player.getPlayerListFooter()); // The client cleared the tab list. TODO: Restore changes done via TabList API player.getTabList().clearAllSilent(); } else { - smc.setActiveSessionHandler(StateRegistry.PLAY, - new TransitionSessionHandler(server, serverConn, resultFuture)); + smc.setActiveSessionHandler(StateRegistry.PLAY, new TransitionSessionHandler(server, serverConn, resultFuture)); } - if (player.resourcePackHandler().getFirstAppliedPack() == null - && resourcePackToApply != null) { + if (player.resourcePackHandler().getFirstAppliedPack() == null && resourcePackToApply != null) { player.resourcePackHandler().queueResourcePack(resourcePackToApply); } - smc.setAutoReading(true); }, smc.eventLoop()); return true; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java index a672c9174..a2d2205ef 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java @@ -166,12 +166,9 @@ public class LoginSessionHandler implements MinecraftSessionHandler { if (player.getClientSettingsPacket() != null) { smc.write(player.getClientSettingsPacket()); } - if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler) { + if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler clientPlaySessionHandler) { smc.setAutoReading(false); - ((ClientPlaySessionHandler) player.getConnection() - .getActiveSessionHandler()).doSwitch().thenAcceptAsync((unused) -> { - smc.setAutoReading(true); - }, smc.eventLoop()); + clientPlaySessionHandler.doSwitch().thenAcceptAsync((unused) -> smc.setAutoReading(true), smc.eventLoop()); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java index 52b67f7b5..3d955e903 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java @@ -19,6 +19,8 @@ package com.velocitypowered.proxy.connection.client; import com.velocitypowered.api.event.player.CookieReceiveEvent; import com.velocitypowered.api.event.player.PlayerClientBrandEvent; +import com.velocitypowered.api.event.player.configuration.PlayerFinishConfigurationEvent; +import com.velocitypowered.api.event.player.configuration.PlayerFinishedConfigurationEvent; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; @@ -246,13 +248,11 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { smc.write(brandPacket); } - player.getConnection().eventLoop().execute(() -> { + server.getEventManager().fire(new PlayerFinishConfigurationEvent(player, serverConn)).thenAcceptAsync(event -> { player.getConnection().write(FinishedUpdatePacket.INSTANCE); player.getConnection().getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.PLAY); - }); - - smc.write(FinishedUpdatePacket.INSTANCE); - smc.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.PLAY); + server.getEventManager().fireAndForget(new PlayerFinishedConfigurationEvent(player, serverConn)); + }, player.getConnection().eventLoop()); return configSwitchFuture; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 2a595b167..a76f054e7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -27,6 +27,7 @@ import com.velocitypowered.api.event.player.CookieReceiveEvent; import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent; import com.velocitypowered.api.event.player.PlayerClientBrandEvent; import com.velocitypowered.api.event.player.TabCompleteEvent; +import com.velocitypowered.api.event.player.configuration.PlayerEnterConfigurationEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; @@ -406,6 +407,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // Complete client switch player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG); VelocityServerConnection serverConnection = player.getConnectedServer(); + server.getEventManager().fireAndForget(new PlayerEnterConfigurationEvent(player, serverConnection)); if (serverConnection != null) { MinecraftConnection smc = serverConnection.ensureConnected(); CompletableFuture.runAsync(() -> { @@ -512,7 +514,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { * @return a future that completes when the switch is complete */ public CompletableFuture doSwitch() { - VelocityServerConnection existingConnection = player.getConnectedServer(); + final VelocityServerConnection existingConnection = player.getConnectedServer(); if (existingConnection != null) { // Shut down the existing server connection. diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index a471e2b53..de828382e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -37,6 +37,7 @@ import com.velocitypowered.api.event.player.KickedFromServerEvent.ServerKickResu import com.velocitypowered.api.event.player.PlayerModInfoEvent; import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent; import com.velocitypowered.api.event.player.ServerPreConnectEvent; +import com.velocitypowered.api.event.player.configuration.PlayerEnterConfigurationEvent; import com.velocitypowered.api.network.ProtocolState; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.permission.PermissionFunction; @@ -59,8 +60,9 @@ import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; -import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; -import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackHandler; +import com.velocitypowered.proxy.connection.player.bundle.BundleDelimiterHandler; +import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.connection.util.VelocityInboundConnection; @@ -806,7 +808,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, }, connection.eventLoop()); } else if (event.getResult() instanceof final Notify res) { if (event.kickedDuringServerConnect() && previousConnection != null) { - sendMessage(Identity.nil(), res.getMessageComponent()); + sendMessage(res.getMessageComponent()); } else { disconnect(res.getMessageComponent()); } @@ -1224,11 +1226,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, CompletableFuture.runAsync(() -> { connection.write(StartUpdatePacket.INSTANCE); connection.getChannel().pipeline() - .get(MinecraftEncoder.class).setState(StateRegistry.CONFIG); + .get(MinecraftEncoder.class).setState(StateRegistry.CONFIG); // Make sure we don't send any play packets to the player after update start connection.addPlayPacketQueueHandler(); + server.getEventManager().fireAndForget(new PlayerEnterConfigurationEvent(this, connectionInFlight)); }, connection.eventLoop()).exceptionally((ex) -> { - logger.error("Error switching player connection to config state:", ex); + logger.error("Error switching player connection to config state", ex); return null; }); } @@ -1363,24 +1366,20 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } switch (status.getStatus()) { - case ALREADY_CONNECTED: - sendMessage(Identity.nil(), ConnectionMessages.ALREADY_CONNECTED); - break; - case CONNECTION_IN_PROGRESS: - sendMessage(Identity.nil(), ConnectionMessages.IN_PROGRESS); - break; - case CONNECTION_CANCELLED: + case ALREADY_CONNECTED -> sendMessage(ConnectionMessages.ALREADY_CONNECTED); + case CONNECTION_IN_PROGRESS -> sendMessage(ConnectionMessages.IN_PROGRESS); + case CONNECTION_CANCELLED -> { // Ignored; the plugin probably already handled this. - break; - case SERVER_DISCONNECTED: - Component reason = status.getReasonComponent() - .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); + } + case SERVER_DISCONNECTED -> { + final Component reason = status.getReasonComponent() + .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); handleConnectionException(toConnect, - DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()), status.isSafe()); - break; - default: + DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()), status.isSafe()); + } + default -> { // The only remaining value is successful (no need to do anything!) - break; + } } }, connection.eventLoop()).thenApply(Result::isSuccessful); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/BundleDelimiterHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/bundle/BundleDelimiterHandler.java similarity index 95% rename from proxy/src/main/java/com/velocitypowered/proxy/connection/client/BundleDelimiterHandler.java rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/bundle/BundleDelimiterHandler.java index e92d10587..d5d52ef03 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/BundleDelimiterHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/bundle/BundleDelimiterHandler.java @@ -15,10 +15,11 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.connection.client; +package com.velocitypowered.proxy.connection.player.bundle; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import java.util.concurrent.CompletableFuture; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/VelocityResourcePackInfo.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/VelocityResourcePackInfo.java similarity index 98% rename from proxy/src/main/java/com/velocitypowered/proxy/connection/player/VelocityResourcePackInfo.java rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/VelocityResourcePackInfo.java index 67f90fd15..4292710e0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/VelocityResourcePackInfo.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/VelocityResourcePackInfo.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.connection.player; +package com.velocitypowered.proxy.connection.player.resourcepack; import com.google.common.base.Preconditions; import com.velocitypowered.api.proxy.player.ResourcePackInfo; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/Legacy117ResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/Legacy117ResourcePackHandler.java similarity index 99% rename from proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/Legacy117ResourcePackHandler.java rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/Legacy117ResourcePackHandler.java index 385bdce4c..a1af567dd 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/Legacy117ResourcePackHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/Legacy117ResourcePackHandler.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.connection.player.resourcepack; +package com.velocitypowered.proxy.connection.player.resourcepack.handler; import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; import com.velocitypowered.proxy.VelocityServer; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/LegacyResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/LegacyResourcePackHandler.java similarity index 98% rename from proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/LegacyResourcePackHandler.java rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/LegacyResourcePackHandler.java index b1b325484..53fa2421c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/LegacyResourcePackHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/LegacyResourcePackHandler.java @@ -15,13 +15,14 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.connection.player.resourcepack; +package com.velocitypowered.proxy.connection.player.resourcepack.handler; import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle; import java.util.ArrayDeque; import java.util.Arrays; import java.util.Collection; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ModernResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java similarity index 98% rename from proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ModernResourcePackHandler.java rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java index 5ba74a273..077ce701d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ModernResourcePackHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.connection.player.resourcepack; +package com.velocitypowered.proxy.connection.player.resourcepack.handler; import com.google.common.collect.ListMultimap; import com.google.common.collect.Multimaps; @@ -23,6 +23,7 @@ import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle; import java.util.Arrays; import java.util.Collection; import java.util.LinkedList; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java similarity index 97% rename from proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackHandler.java rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java index 4e6e72505..e5df9f659 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java @@ -15,14 +15,15 @@ * along with this program. If not, see . */ -package com.velocitypowered.proxy.connection.player.resourcepack; +package com.velocitypowered.proxy.connection.player.resourcepack.handler; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle; +import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequestPacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java b/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java index 27ec4ba8b..ca4e6b1b2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java @@ -35,7 +35,8 @@ public class Connections { public static final String MINECRAFT_DECODER = "minecraft-decoder"; public static final String MINECRAFT_ENCODER = "minecraft-encoder"; public static final String READ_TIMEOUT = "read-timeout"; - public static final String PLAY_PACKET_QUEUE = "play-packet-queue"; + public static final String PLAY_PACKET_QUEUE_OUTBOUND = "play-packet-queue-outbound"; + public static final String PLAY_PACKET_QUEUE_INBOUND = "play-packet-queue-inbound"; private Connections() { throw new AssertionError(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java index f8362cc09..d75cb46ca 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java @@ -56,8 +56,7 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof ByteBuf) { - ByteBuf buf = (ByteBuf) msg; + if (msg instanceof ByteBuf buf) { tryDecode(ctx, buf); } else { ctx.fireChannelRead(msg); @@ -147,4 +146,8 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter { this.state = state; this.setProtocolVersion(registry.version); } + + public ProtocolUtils.Direction getDirection() { + return direction; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueInboundHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueInboundHandler.java new file mode 100644 index 000000000..fe553f76a --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueInboundHandler.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.netty; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.StateRegistry; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.PlatformDependent; +import java.util.Queue; +import org.jetbrains.annotations.NotNull; + +/** + * Queues up any pending PLAY packets while the client is in the CONFIG state. + * + *

Much of the Velocity API (i.e. chat messages) utilize PLAY packets, however the client is + * incapable of receiving these packets during the CONFIG state. Certain events such as the + * ServerPreConnectEvent may be called during this time, and we need to ensure that any API that + * uses these packets will work as expected. + * + *

This handler will queue up any packets that are sent to the client during this time, and send + * them once the client has (re)entered the PLAY state. + */ +public class PlayPacketQueueInboundHandler extends ChannelDuplexHandler { + + private final StateRegistry.PacketRegistry.ProtocolRegistry registry; + private final Queue queue = PlatformDependent.newMpscQueue(); + + /** + * Provides registries for client & server bound packets. + * + * @param version the protocol version + */ + public PlayPacketQueueInboundHandler(ProtocolVersion version, ProtocolUtils.Direction direction) { + this.registry = StateRegistry.CONFIG.getProtocolRegistry(direction, version); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof final MinecraftPacket packet) { + // If the packet exists in the CONFIG state, we want to always + // ensure that it gets handled by the current handler + if (this.registry.containsPacket(packet)) { + ctx.fireChannelRead(msg); + return; + } + } + + // Otherwise, queue the packet + this.queue.offer(msg); + } + + @Override + public void channelInactive(@NotNull ChannelHandlerContext ctx) throws Exception { + this.releaseQueue(ctx, false); + + super.channelInactive(ctx); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + this.releaseQueue(ctx, ctx.channel().isActive()); + } + + private void releaseQueue(ChannelHandlerContext ctx, boolean active) { + // Handle all the queued packets + Object msg; + while ((msg = this.queue.poll()) != null) { + if (active) { + ctx.fireChannelRead(msg); + } else { + ReferenceCountUtil.release(msg); + } + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueOutboundHandler.java similarity index 86% rename from proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueHandler.java rename to proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueOutboundHandler.java index 990985c25..d5764ef6a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueOutboundHandler.java @@ -40,7 +40,7 @@ import org.jetbrains.annotations.NotNull; *

This handler will queue up any packets that are sent to the client during this time, and send * them once the client has (re)entered the PLAY state. */ -public class PlayPacketQueueHandler extends ChannelDuplexHandler { +public class PlayPacketQueueOutboundHandler extends ChannelDuplexHandler { private final StateRegistry.PacketRegistry.ProtocolRegistry registry; private final Queue queue = PlatformDependent.newMpscQueue(); @@ -50,28 +50,26 @@ public class PlayPacketQueueHandler extends ChannelDuplexHandler { * * @param version the protocol version */ - public PlayPacketQueueHandler(ProtocolVersion version, ProtocolUtils.Direction direction) { - this.registry = - StateRegistry.CONFIG.getProtocolRegistry(direction, version); + public PlayPacketQueueOutboundHandler(ProtocolVersion version, ProtocolUtils.Direction direction) { + this.registry = StateRegistry.CONFIG.getProtocolRegistry(direction, version); } @Override - public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) - throws Exception { - if (!(msg instanceof MinecraftPacket)) { + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (!(msg instanceof final MinecraftPacket packet)) { ctx.write(msg, promise); return; } // If the packet exists in the CONFIG state, we want to always // ensure that it gets sent out to the client - if (this.registry.containsPacket(((MinecraftPacket) msg))) { + if (this.registry.containsPacket(packet)) { ctx.write(msg, promise); return; } // Otherwise, queue the packet - this.queue.offer((MinecraftPacket) msg); + this.queue.offer(packet); } @Override @@ -87,10 +85,6 @@ public class PlayPacketQueueHandler extends ChannelDuplexHandler { } private void releaseQueue(ChannelHandlerContext ctx, boolean active) { - if (this.queue.isEmpty()) { - return; - } - // Send out all the queued packets MinecraftPacket packet; while ((packet = this.queue.poll()) != null) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequestPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequestPacket.java index 07f9f776a..a0f86aed1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequestPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequestPacket.java @@ -21,7 +21,7 @@ import com.google.common.base.Preconditions; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; -import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; From 9d25d309d38a1e3c818166f0c5f994b8af3785cd Mon Sep 17 00:00:00 2001 From: William Date: Tue, 18 Jun 2024 15:29:58 +0100 Subject: [PATCH 2/3] feat: Add basic API for server links (#1353) * feat: add basic server links API * refactor: more precondition checking on links * refactor: remove whitespace * refactor: adjust method order, JDs in ServerLink * refactor: remove "throws" from constructors * refactor: add @NotNull annotations * refactor: requested changes * refactor: just use `List#copyOf` --- .../com/velocitypowered/api/proxy/Player.java | 13 +++ .../velocitypowered/api/util/ServerLink.java | 101 ++++++++++++++++++ .../connection/client/ConnectedPlayer.java | 18 ++++ .../config/ClientboundServerLinksPacket.java | 8 ++ 4 files changed, 140 insertions(+) create mode 100644 api/src/main/java/com/velocitypowered/api/util/ServerLink.java diff --git a/api/src/main/java/com/velocitypowered/api/proxy/Player.java b/api/src/main/java/com/velocitypowered/api/proxy/Player.java index dfe9a2bc7..04e65c849 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -21,6 +21,7 @@ import com.velocitypowered.api.proxy.player.TabList; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.ModInfo; +import com.velocitypowered.api.util.ServerLink; import java.net.InetSocketAddress; import java.util.Collection; import java.util.List; @@ -461,4 +462,16 @@ public interface Player extends * @sinceMinecraft 1.20.5 */ void requestCookie(Key key); + + /** + * Send the player a list of custom links to display in their client's pause menu. + * + *

Note that later packets sent by the backend server may override links sent by the proxy. + * + * @param links an ordered list of {@link ServerLink}s to send to the player + * @throws IllegalArgumentException if the player is from a version lower than 1.21 + * @since 3.3.0 + * @sinceMinecraft 1.21 + */ + void setServerLinks(@NotNull List links); } \ No newline at end of file diff --git a/api/src/main/java/com/velocitypowered/api/util/ServerLink.java b/api/src/main/java/com/velocitypowered/api/util/ServerLink.java new file mode 100644 index 000000000..9eb04a980 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/util/ServerLink.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2021-2024 Velocity Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocitypowered.api.util; + +import com.google.common.base.Preconditions; +import java.net.URI; +import java.util.Optional; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.Nullable; + +/** + * Represents a custom URL servers can show in player pause menus. + * Links can be of a built-in type or use a custom component text label. + */ +public final class ServerLink { + + private @Nullable Type type; + private @Nullable Component label; + private final URI url; + + private ServerLink(Component label, String url) { + this.label = Preconditions.checkNotNull(label, "label"); + this.url = URI.create(url); + } + + private ServerLink(Type type, String url) { + this.type = Preconditions.checkNotNull(type, "type"); + this.url = URI.create(url); + } + + /** + * Construct a server link with a custom component label. + * + * @param label a custom component label to display + * @param link the URL to open when clicked + */ + public static ServerLink serverLink(Component label, String link) { + return new ServerLink(label, link); + } + + /** + * Construct a server link with a built-in type. + * + * @param type the {@link Type built-in type} of link + * @param link the URL to open when clicked + */ + public static ServerLink serverLink(Type type, String link) { + return new ServerLink(type, link); + } + + /** + * Get the type of the server link. + * + * @return the type of the server link + */ + public Optional getBuiltInType() { + return Optional.ofNullable(type); + } + + /** + * Get the custom component label of the server link. + * + * @return the custom component label of the server link + */ + public Optional getCustomLabel() { + return Optional.ofNullable(label); + } + + /** + * Get the link {@link URI}. + * + * @return the link {@link URI} + */ + public URI getUrl() { + return url; + } + + /** + * Built-in types of server links. + * + * @apiNote {@link Type#BUG_REPORT} links are shown on the connection error screen + */ + public enum Type { + BUG_REPORT, + COMMUNITY_GUIDELINES, + SUPPORT, + STATUS, + FEEDBACK, + COMMUNITY, + WEBSITE, + FORUMS, + NEWS, + ANNOUNCEMENTS + } + +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index de828382e..8522dfce7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -55,6 +55,7 @@ import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.ModInfo; +import com.velocitypowered.api.util.ServerLink; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation; import com.velocitypowered.proxy.connection.MinecraftConnection; @@ -83,6 +84,7 @@ import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket; import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory; import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChatPacket; +import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket; import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.util.ByteBufDataOutput; @@ -1059,6 +1061,22 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, }, connection.eventLoop()); } + @Override + public void setServerLinks(final @NotNull List links) { + Preconditions.checkNotNull(links, "links"); + Preconditions.checkArgument( + this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21), + "Player version must be at least 1.21 to be able to set server links"); + + if (connection.getState() != StateRegistry.PLAY + && connection.getState() != StateRegistry.CONFIG) { + throw new IllegalStateException("Can only send server links in CONFIGURATION or PLAY protocol"); + } + + connection.write(new ClientboundServerLinksPacket(List.copyOf(links).stream() + .map(l -> new ClientboundServerLinksPacket.ServerLink(l, getProtocolVersion())).toList())); + } + @Override public void addCustomChatCompletions(@NotNull Collection completions) { Preconditions.checkNotNull(completions, "completions"); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ClientboundServerLinksPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ClientboundServerLinksPacket.java index bee080ee8..274bbb8f9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ClientboundServerLinksPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ClientboundServerLinksPacket.java @@ -18,6 +18,7 @@ package com.velocitypowered.proxy.protocol.packet.config; import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.util.ServerLink; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; @@ -66,6 +67,13 @@ public class ClientboundServerLinksPacket implements MinecraftPacket { } public record ServerLink(int id, ComponentHolder displayName, String url) { + + public ServerLink(com.velocitypowered.api.util.ServerLink link, ProtocolVersion protocolVersion) { + this(link.getBuiltInType().map(Enum::ordinal).orElse(-1), + link.getCustomLabel().map(c -> new ComponentHolder(protocolVersion, c)).orElse(null), + link.getUrl().toString()); + } + private static ServerLink read(ByteBuf buf, ProtocolVersion version) { if (buf.readBoolean()) { return new ServerLink(ProtocolUtils.readVarInt(buf), null, ProtocolUtils.readString(buf)); From 410636a305f1e43961ba021cea5f1d602d0db0f1 Mon Sep 17 00:00:00 2001 From: Oliwier Miodun <41339226+Nacioszeczek@users.noreply.github.com> Date: Sun, 23 Jun 2024 18:57:40 +0200 Subject: [PATCH 3/3] [ci skip] Add Issue Forms (#1364) * copy issue contact links from Paper * bug report template * bug report template fixes * why no code block * feature request issue template and fixes to bug report template * make fields required --- .github/ISSUE_TEMPLATE/bug-report.yml | 69 ++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 10 ++++ .github/ISSUE_TEMPLATE/feature-request.yml | 48 +++++++++++++++ 3 files changed, 127 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.yml diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 000000000..82cada2b6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,69 @@ +name: Bug Report +description: Report issues with Velocity not working properly. +labels: ["type: bug"] +body: + - type: textarea + attributes: + label: Expected Behavior + description: What you expected to work and how. + validations: + required: true + + - type: textarea + attributes: + label: Actual Behavior + description: What actually happens. + validations: + required: true + + - type: textarea + attributes: + label: Steps to Reproduce + description: Information on how we can reproduce this bug on our own, this can be e.g. just an explanation, a video or your Velocity config. + validations: + required: true + + - type: textarea + attributes: + label: Plugin List + description: | + All plugins running on your proxy and the backend server you're experiencing this issue on. + Use `/velocity plugins` to list plugins on Velocity and `/plugins` to list plugins on your backend server. + validations: + required: true + + - type: textarea + attributes: + label: Velocity Version + description: | + The full, unmodified output of running `/velocity info`. + *"Latest"* is not a version. We require you to paste the text, not a screenshot. +

+ Example + + ``` + [17:44:10 INFO]: Velocity 3.3.0-SNAPSHOT (git-9d25d309-b400) + [17:44:10 INFO]: Copyright 2018-2023 Velocity Contributors. Velocity is licensed under the terms of the GNU General Public License v3. + [17:44:10 INFO]: velocitypowered.com - GitHub + ``` +
+ validations: + required: true + + - type: textarea + attributes: + label: Additional Information + description: Anything else you think is helpful. + validations: + required: false + + - type: markdown + attributes: + value: | + Before submitting this issue, please ensure the following: + + 1. You are running the latest version of Velocity from [our downloads page](https://papermc.io/downloads/velocity). + 2. You searched for and ensured there isn't already an open issue regarding this. + + If you think you have a bug, but are not sure, feel free to ask in the `#velocity-help` channel on our + [Discord](https://discord.gg/papermc). \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..faf9d2e70 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,10 @@ +blank_issues_enabled: false +contact_links: + - name: PaperMC Discord + url: https://discord.gg/papermc + about: If you are having issues with the proxy not connecting to servers or have other minor issues, come ask us on our Discord server! + - name: Exploit Report + url: https://discord.gg/papermc + about: | + Due to GitHub not currently allowing private issues, exploit reports are currently handled via our Discord. + To report an exploit, see the #paper-exploit-report channel. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml new file mode 100644 index 000000000..f47b06d00 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -0,0 +1,48 @@ +name: Feature Request +description: Request for a feature to be implemented into Velocity. +labels: ["type: feature"] +body: + - type: textarea + attributes: + label: Requested Feature + description: | + Please describe as best as you can what you'd like to be added to Velocity. + validations: + required: true + + - type: textarea + attributes: + label: Why is this needed? + description: | + Please describe why do you need this feature. + Do you think it could be useful? Is it due to another problem? + validations: + required: true + + - type: textarea + attributes: + label: Alternative Solutions + description: | + Are there any alternative solutions to implementing a new feature? + What have you tried instead? + validations: + required: true + + - type: textarea + attributes: + label: Additional Information + description: Anything else you want to add. + validations: + required: false + + - type: markdown + attributes: + value: | + Before submitting this request, please ensure the following: + + 1. You are running the latest version of Velocity from [our downloads page](https://papermc.io/downloads/velocity). + 2. You searched for and ensured there isn't already an open issue regarding this. + 3. The feature you're requesting has to be implemented on Velocity and not on the backend server. + + If you are unsure whether your problem can already be fixed in another way, feel free to ask in the `#velocity-help` channel on our + [Discord](https://discord.gg/papermc). \ No newline at end of file