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 0000000000..3b108c6a74 --- /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 0000000000..f6249b8974 --- /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 0000000000..09e76104f6 --- /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 ef8217c696..0042436169 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.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 b1c91ebdb9..c917390787 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.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 void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception 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 void setState(StateRegistry state) { 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 void setState(StateRegistry state) { * 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 5b83e65490..14989b1a3c 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.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 3c2691781a..ea28d2c97c 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.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 @@ private void processPlayerList(ByteBufDataInput in) { 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 @@ private void processMessage0(ByteBufDataInput in, 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 baff6017b9..8c5abbb303 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.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; @@ -118,7 +118,8 @@ public boolean handle(TagsUpdatePacket packet) { @Override public boolean handle(KeepAlivePacket packet) { - serverConn.ensureConnected().write(packet); + serverConn.getPendingPings().put(packet.getRandomId(), System.nanoTime()); + serverConn.getPlayer().getConnection().write(packet); return true; } @@ -179,30 +180,25 @@ public boolean handle(final ResourcePackRequestPacket packet) { @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 a672c9174c..a2d2205ef2 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 boolean handle(ServerLoginSuccessPacket packet) { 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 52b67f7b58..3d955e9037 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 @@ 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 CompletableFutureMuch 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