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 01/29] 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 00000000..3b108c6a --- /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 00000000..f6249b89 --- /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 00000000..09e76104 --- /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 ef8217c6..00424361 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 b1c91ebd..c9173907 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 5b83e654..14989b1a 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 3c269178..ea28d2c9 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 3f4325e5..a19854eb 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 a672c917..a2d2205e 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 52b67f7b..3d955e90 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 2a595b16..a76f054e 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 a471e2b5..de828382 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 e92d1058..d5d52ef0 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 67f90fd1..4292710e 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 385bdce4..a1af567d 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 b1b32548..53fa2421 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 5ba74a27..077ce701 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 4e6e7250..e5df9f65 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 27ec4ba8..ca4e6b1b 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 f8362cc0..d75cb46c 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 00000000..fe553f76 --- /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 990985c2..d5764ef6 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 07f9f776..a0f86aed 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 02/29] 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 dfe9a2bc..04e65c84 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 00000000..9eb04a98 --- /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 de828382..8522dfce 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 bee080ee..274bbb8f 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 03/29] [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 00000000..82cada2b --- /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 00000000..faf9d2e7 --- /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 00000000..f47b06d0 --- /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 From aa4e8780bdafc3149497fc2e097b1269d8e40f21 Mon Sep 17 00:00:00 2001 From: wallenjos01 <32561173+wallenjos01@users.noreply.github.com> Date: Thu, 4 Jul 2024 02:57:30 -0600 Subject: [PATCH 04/29] Assign ServerConnection to Player before enabling auto read (#1309) --- .../connection/backend/TransitionSessionHandler.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java index 6655e3fb..a74f7c56 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java @@ -137,12 +137,14 @@ public class TransitionSessionHandler implements MinecraftSessionHandler { smc.setActiveSessionHandler(StateRegistry.PLAY, new BackendPlaySessionHandler(server, serverConn)); - // Clean up disabling auto-read while the connected event was being processed. - smc.setAutoReading(true); - // Now set the connected server. serverConn.getPlayer().setConnectedServer(serverConn); + // Clean up disabling auto-read while the connected event was being processed. + // Do this after setting the connection, so no incoming packets are processed before + // the API knows which server the player is connected to. + smc.setAutoReading(true); + // Send client settings. In 1.20.2+ this is done in the config state. if (smc.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2) && player.getClientSettingsPacket() != null) { From 6b03b28a61d24b770bdf04c9eaf9424409f7b378 Mon Sep 17 00:00:00 2001 From: Gegy Date: Mon, 27 Nov 2023 18:26:59 +0100 Subject: [PATCH 05/29] Refactor ChatQueue to track and expose general chat state --- .../connection/client/ConnectedPlayer.java | 11 +- .../proxy/protocol/packet/chat/ChatQueue.java | 140 ++++++------------ 2 files changed, 49 insertions(+), 102 deletions(-) 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 8522dfce..6beeb9cf 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 @@ -83,6 +83,7 @@ import com.velocitypowered.proxy.protocol.packet.chat.ChatType; 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.builder.ChatBuilderV2; 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; @@ -1108,11 +1109,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, "input cannot be greater than " + LegacyChatPacket.MAX_SERVERBOUND_MESSAGE_LENGTH + " characters in length"); if (getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19)) { - this.chatQueue.hijack(getChatBuilderFactory().builder().asPlayer(this).message(input), - (instant, item) -> { - item.setTimestamp(instant); - return item.toServer(); - }); + ChatBuilderV2 message = getChatBuilderFactory().builder().asPlayer(this).message(input); + this.chatQueue.queuePacket(chatState -> { + message.setTimestamp(chatState.lastTimestamp); + return message.toServer(); + }); } else { ensureBackendConnection().write(getChatBuilderFactory().builder() .asPlayer(this).message(input).toServer()); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatQueue.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatQueue.java index 2ddcff17..07480f54 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatQueue.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatQueue.java @@ -32,9 +32,10 @@ import java.util.function.Function; */ public class ChatQueue { - private final Object internalLock; + private final Object internalLock = new Object(); private final ConnectedPlayer player; - private CompletableFuture packetFuture; + private final ChatState chatState = new ChatState(); + private CompletableFuture head = CompletableFuture.completedFuture(null); /** * Instantiates a {@link ChatQueue} for a specific {@link ConnectedPlayer}. @@ -43,8 +44,19 @@ public class ChatQueue { */ public ChatQueue(ConnectedPlayer player) { this.player = player; - this.packetFuture = CompletableFuture.completedFuture(new WrappedPacket(Instant.EPOCH, null)); - this.internalLock = new Object(); + } + + private void queueTask(Task task) { + synchronized (internalLock) { + MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected(); + head = head.thenCompose(v -> { + try { + return task.update(chatState, smc).exceptionally(ignored -> null); + } catch (Throwable ignored) { + return CompletableFuture.completedFuture(null); + } + }); + } } /** @@ -53,120 +65,54 @@ public class ChatQueue { * and messages. All entries are locked through an internal object lock. * * @param nextPacket the {@link CompletableFuture} which will provide the next-processed packet. - * @param timestamp the {@link Instant} timestamp of this packet so we can allow piggybacking. + * @param timestamp the new {@link Instant} timestamp of this packet to update the internal chat state. */ - public void queuePacket(CompletableFuture nextPacket, Instant timestamp) { - synchronized (internalLock) { // wait for the lock to resolve - we don't want to drop packets - MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected(); - - CompletableFuture nextInLine = WrappedPacket.wrap(timestamp, nextPacket); - this.packetFuture = awaitChat(smc, this.packetFuture, - nextInLine); // we await chat, binding `this.packetFuture` -> `nextInLine` - } + public void queuePacket(CompletableFuture nextPacket, @Nullable Instant timestamp) { + queueTask((chatState, smc) -> { + chatState.update(timestamp); + return nextPacket.thenCompose(packet -> writePacket(packet, smc)); + }); } /** - * Hijacks the latest sent packet's timestamp to provide an in-order packet without polling the + * Hijacks the latest sent packet's chat state to provide an in-order packet without polling the * physical, or prior packets sent through the stream. * - * @param packet the {@link MinecraftPacket} to send. - * @param instantMapper the {@link InstantPacketMapper} which maps the prior timestamp and current - * packet to a new packet. - * @param the type of base to expect when mapping the packet. - * @param the type of packet for instantMapper type-checking. + * @param packetFunction a function that maps the prior {@link ChatState} into a new packet. + * @param the type of packet to send. */ - public void hijack(K packet, - InstantPacketMapper instantMapper) { - synchronized (internalLock) { - CompletableFuture trueFuture = CompletableFuture.completedFuture(packet); - MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected(); - - this.packetFuture = hijackCurrentPacket(smc, this.packetFuture, trueFuture, instantMapper); - } + public void queuePacket(Function packetFunction) { + queueTask((chatState, smc) -> { + T packet = packetFunction.apply(chatState); + return writePacket(packet, smc); + }); } - private static Function writePacket(MinecraftConnection connection) { - return wrappedPacket -> { - if (!connection.isClosed()) { - ChannelFuture future = wrappedPacket.write(connection); + private static CompletableFuture writePacket(T packet, MinecraftConnection smc) { + return CompletableFuture.runAsync(() -> { + if (!smc.isClosed()) { + ChannelFuture future = smc.write(packet); if (future != null) { future.awaitUninterruptibly(); } } - - return wrappedPacket; - }; + }, smc.eventLoop()); } - private static CompletableFuture awaitChat( - MinecraftConnection connection, - CompletableFuture binder, - CompletableFuture future - ) { - // the binder will run -> then the future will get the `write packet` caller - return binder.thenCompose(ignored -> future.thenApply(writePacket(connection))); + private interface Task { + CompletableFuture update(ChatState chatState, MinecraftConnection smc); } - private static CompletableFuture hijackCurrentPacket( - MinecraftConnection connection, - CompletableFuture binder, - CompletableFuture future, - InstantPacketMapper packetMapper - ) { - CompletableFuture awaitedFuture = new CompletableFuture<>(); - // the binder will complete -> then the future will get the `write packet` caller - binder.whenComplete((previous, ignored) -> { - // map the new packet into a better "designed" packet with the hijacked packet's timestamp - WrappedPacket.wrap(previous.timestamp, - future.thenApply(item -> packetMapper.map(previous.timestamp, item))) - .thenApplyAsync(writePacket(connection), connection.eventLoop()) - .whenComplete( - (packet, throwable) -> awaitedFuture.complete(throwable != null ? null : packet)); - }); - return awaitedFuture; - } + public static class ChatState { + public volatile Instant lastTimestamp = Instant.EPOCH; - /** - * Provides an {@link Instant} based timestamp mapper from an existing object to create a packet. - * - * @param The base object type to map. - * @param The resulting packet type. - */ - public interface InstantPacketMapper { - - /** - * Maps a value into a packet with it and a timestamp. - * - * @param nextInstant the {@link Instant} timestamp to use for tracking. - * @param currentObject the current item to map to the packet. - * @return The resulting packet from the mapping. - */ - V map(Instant nextInstant, K currentObject); - } - - private static class WrappedPacket { - - private final Instant timestamp; - private final MinecraftPacket packet; - - private WrappedPacket(Instant timestamp, MinecraftPacket packet) { - this.timestamp = timestamp; - this.packet = packet; + private ChatState() { } - @Nullable - public ChannelFuture write(MinecraftConnection connection) { - if (packet != null) { - return connection.write(packet); + public void update(@Nullable Instant timestamp) { + if (timestamp != null) { + this.lastTimestamp = timestamp; } - return null; - } - - private static CompletableFuture wrap(Instant timestamp, - CompletableFuture nextPacket) { - return nextPacket - .thenApply(pkt -> new WrappedPacket(timestamp, pkt)) - .exceptionally(ignored -> new WrappedPacket(timestamp, null)); } } } From be73131d5dea81e9283c4a83e6b39521ae2e1b71 Mon Sep 17 00:00:00 2001 From: Gegy Date: Mon, 27 Nov 2023 18:33:35 +0100 Subject: [PATCH 06/29] Remove unneeded check SessionCommandHandler is only constructed for >= 1.19.3. --- .../protocol/packet/chat/session/SessionCommandHandler.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java index 6984970b..c0d12f5f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java @@ -18,7 +18,6 @@ package com.velocitypowered.proxy.protocol.packet.chat.session; import com.velocitypowered.api.event.command.CommandExecuteEvent; -import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket; @@ -55,7 +54,7 @@ public class SessionCommandHandler implements CommandHandler Date: Mon, 27 Nov 2023 18:36:08 +0100 Subject: [PATCH 07/29] Extract common command handler paths --- .../chat/session/SessionCommandHandler.java | 93 +++++++++---------- 1 file changed, 43 insertions(+), 50 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java index c0d12f5f..477e48c3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java @@ -20,10 +20,12 @@ package com.velocitypowered.proxy.protocol.packet.chat.session; import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket; import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; import java.util.concurrent.CompletableFuture; import net.kyori.adventure.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; public class SessionCommandHandler implements CommandHandler { @@ -40,6 +42,42 @@ public class SessionCommandHandler implements CommandHandler { @@ -53,64 +91,19 @@ public class SessionCommandHandler implements CommandHandler { - if (!hasRun) { - if (packet.isSigned() && commandToRun.equals(packet.command)) { - return packet; - } else { - if (packet.isSigned()) { - logger.fatal("A plugin tried to change a command with signed component(s). " - + "This is not supported. " - + "Disconnecting player " + player.getUsername() + ". Command packet: " + packet); - player.disconnect(Component.text( - "A proxy plugin caused an illegal protocol state. " - + "Contact your network administrator.")); - return null; - } - - return this.player.getChatBuilderFactory() - .builder() - .setTimestamp(packet.timeStamp) - .asPlayer(this.player) - .message("/" + commandToRun) - .toServer(); - } + if (hasRun) { + return consumeCommand(packet); } - if (packet.lastSeenMessages != null) { - return new ChatAcknowledgementPacket(packet.lastSeenMessages.getOffset()); - } - return null; + return forwardCommand(packet, commandToRun); }); }, packet.command, packet.timeStamp); } From 3b7ffa837ed3db0d3a5e79929d3646bbca83ead3 Mon Sep 17 00:00:00 2001 From: Gegy Date: Mon, 27 Nov 2023 18:38:55 +0100 Subject: [PATCH 08/29] Don't require command to be signed to pass it through as-is --- .../protocol/packet/chat/session/SessionCommandHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java index 477e48c3..0bb2ff1a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java @@ -52,7 +52,7 @@ public class SessionCommandHandler implements CommandHandler Date: Mon, 27 Nov 2023 18:40:45 +0100 Subject: [PATCH 09/29] Explicitly reject consuming signed command when denied by event This would fail down the line anyway due to inconsistent chat state (if holding a 'last seen' update) --- .../chat/session/SessionCommandHandler.java | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java index 0bb2ff1a..16a02a78 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java @@ -44,10 +44,23 @@ public class SessionCommandHandler implements CommandHandler { CommandExecuteEvent.CommandResult result = event.getResult(); if (result == CommandExecuteEvent.CommandResult.denied()) { - if (packet.isSigned()) { - logger.fatal("A plugin tried to deny a command with signable component(s). " - + "This is not supported. " - + "Disconnecting player " + player.getUsername() + ". Command packet: " + packet); - player.disconnect(Component.text( - "A proxy plugin caused an illegal protocol state. " - + "Contact your network administrator.")); - } return CompletableFuture.completedFuture(consumeCommand(packet)); } From b5ee9dd20a9ddd72832c2533e4d1fc7dac40d26e Mon Sep 17 00:00:00 2001 From: Gegy Date: Thu, 22 Feb 2024 22:33:49 +0100 Subject: [PATCH 10/29] Capture a consistent and reusable 'last seen' state for use in command packets --- .../client/ClientPlaySessionHandler.java | 10 +++ .../chat/ChatAcknowledgementPacket.java | 4 + .../proxy/protocol/packet/chat/ChatQueue.java | 74 +++++++++++++++++-- .../protocol/packet/chat/CommandHandler.java | 15 ++-- .../packet/chat/LastSeenMessages.java | 16 +++- .../packet/chat/keyed/KeyedChatHandler.java | 5 +- .../chat/keyed/KeyedCommandHandler.java | 4 +- .../chat/legacy/LegacyCommandHandler.java | 4 +- .../chat/session/SessionChatHandler.java | 10 ++- .../chat/session/SessionCommandHandler.java | 16 ++-- .../chat/session/SessionPlayerChatPacket.java | 11 +++ .../session/SessionPlayerCommandPacket.java | 17 +++++ .../session/UnsignedPlayerCommandPacket.java | 7 ++ 13 files changed, 164 insertions(+), 29 deletions(-) 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 a76f054e..f2df78a7 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 @@ -54,6 +54,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer; +import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket; import com.velocitypowered.proxy.protocol.packet.chat.ChatHandler; import com.velocitypowered.proxy.protocol.packet.chat.ChatTimeKeeper; import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; @@ -423,6 +424,15 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return true; } + @Override + public boolean handle(ChatAcknowledgementPacket packet) { + if (player.getCurrentServer().isEmpty()) { + return true; + } + player.getChatQueue().handleAcknowledgement(packet.offset()); + return true; + } + @Override public boolean handle(ServerboundCookieResponsePacket packet) { server.getEventManager() diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatAcknowledgementPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatAcknowledgementPacket.java index 6ca648a7..b0718090 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatAcknowledgementPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatAcknowledgementPacket.java @@ -54,4 +54,8 @@ public class ChatAcknowledgementPacket implements MinecraftPacket { "offset=" + offset + '}'; } + + public int offset() { + return offset; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatQueue.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatQueue.java index 07480f54..5928bf36 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatQueue.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatQueue.java @@ -23,7 +23,9 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import io.netty.channel.ChannelFuture; import org.checkerframework.checker.nullness.qual.Nullable; import java.time.Instant; +import java.util.BitSet; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; /** @@ -64,13 +66,15 @@ public class ChatQueue { * packets. This maintains order on the server-level for the client insertions of commands * and messages. All entries are locked through an internal object lock. * - * @param nextPacket the {@link CompletableFuture} which will provide the next-processed packet. - * @param timestamp the new {@link Instant} timestamp of this packet to update the internal chat state. + * @param nextPacket a function mapping {@link LastSeenMessages} state to a {@link CompletableFuture} that will + * provide the next-processed packet. This should include the fixed {@link LastSeenMessages}. + * @param timestamp the new {@link Instant} timestamp of this packet to update the internal chat state. + * @param lastSeenMessages the new {@link LastSeenMessages} last seen messages to update the internal chat state. */ - public void queuePacket(CompletableFuture nextPacket, @Nullable Instant timestamp) { + public void queuePacket(Function> nextPacket, @Nullable Instant timestamp, @Nullable LastSeenMessages lastSeenMessages) { queueTask((chatState, smc) -> { - chatState.update(timestamp); - return nextPacket.thenCompose(packet -> writePacket(packet, smc)); + LastSeenMessages newLastSeenMessages = chatState.updateFromMessage(timestamp, lastSeenMessages); + return nextPacket.apply(newLastSeenMessages).thenCompose(packet -> writePacket(packet, smc)); }); } @@ -88,6 +92,16 @@ public class ChatQueue { }); } + public void handleAcknowledgement(int offset) { + queueTask((chatState, smc) -> { + int ackCountToForward = chatState.accumulateAckCount(offset); + if (ackCountToForward > 0) { + return writePacket(new ChatAcknowledgementPacket(ackCountToForward), smc); + } + return CompletableFuture.completedFuture(null); + }); + } + private static CompletableFuture writePacket(T packet, MinecraftConnection smc) { return CompletableFuture.runAsync(() -> { if (!smc.isClosed()) { @@ -103,16 +117,64 @@ public class ChatQueue { CompletableFuture update(ChatState chatState, MinecraftConnection smc); } + /** + * Tracks the last Secure Chat state that we received from the client. This is important to always have a valid 'last + * seen' state that is consistent with future and past updates from the client (which may be signed). This state is + * used to construct 'spoofed' command packets from the proxy to the server. + *
    + *
  • If we last forwarded a chat or command packet from the client, we have a known 'last seen' that we can + * reuse.
  • + *
  • If we last forwarded a {@link ChatAcknowledgementPacket}, the previous 'last seen' cannot be reused. We + * cannot predict an up-to-date 'last seen', as we do not know which messages the client actually saw.
  • + *
  • Therefore, we need to hold back any acknowledgement packets so that we can continue to reuse the last valid + * 'last seen' state.
  • + *
  • However, there is a limit to the number of messages that can remain unacknowledged on the server.
  • + *
  • To address this, we know that if the client has moved its 'last seen' window far enough, we can fill in the + * gap with dummy 'last seen', and it will never be checked.
  • + *
+ * + * Note that this is effectively unused for 1.20.5+ clients, as commands without any signature do not send 'last seen' + * updates. + */ public static class ChatState { + private static final int MINIMUM_DELAYED_ACK_COUNT = LastSeenMessages.WINDOW_SIZE; + private static final BitSet DUMMY_LAST_SEEN_MESSAGES = new BitSet(); + public volatile Instant lastTimestamp = Instant.EPOCH; + private volatile BitSet lastSeenMessages = new BitSet(); + private final AtomicInteger delayedAckCount = new AtomicInteger(); private ChatState() { } - public void update(@Nullable Instant timestamp) { + @Nullable + public LastSeenMessages updateFromMessage(@Nullable Instant timestamp, @Nullable LastSeenMessages lastSeenMessages) { if (timestamp != null) { this.lastTimestamp = timestamp; } + if (lastSeenMessages != null) { + // We held back some acknowledged messages, so flush that out now that we have a known 'last seen' state again + int delayedAckCount = this.delayedAckCount.getAndSet(0); + this.lastSeenMessages = lastSeenMessages.getAcknowledged(); + return lastSeenMessages.offset(delayedAckCount); + } + return null; + } + + public int accumulateAckCount(int ackCount) { + int delayedAckCount = this.delayedAckCount.addAndGet(ackCount); + int ackCountToForward = delayedAckCount - MINIMUM_DELAYED_ACK_COUNT; + if (ackCountToForward >= LastSeenMessages.WINDOW_SIZE) { + // Because we only forward acknowledgements above the window size, we don't have to shift the previous 'last seen' state + this.lastSeenMessages = DUMMY_LAST_SEEN_MESSAGES; + this.delayedAckCount.set(MINIMUM_DELAYED_ACK_COUNT); + return ackCountToForward; + } + return 0; + } + + public LastSeenMessages createLastSeen() { + return new LastSeenMessages(0, lastSeenMessages); } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/CommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/CommandHandler.java index c778eecd..9786fe14 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/CommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/CommandHandler.java @@ -23,11 +23,13 @@ import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.MinecraftPacket; import java.time.Instant; import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; import java.util.function.Function; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.Nullable; public interface CommandHandler { @@ -53,11 +55,12 @@ public interface CommandHandler { } default void queueCommandResult(VelocityServer server, ConnectedPlayer player, - Function> futurePacketCreator, - String message, Instant timestamp) { - player.getChatQueue().queuePacket( - server.getCommandManager().callCommandEvent(player, message) - .thenComposeAsync(futurePacketCreator) + BiFunction> futurePacketCreator, + String message, Instant timestamp, @Nullable LastSeenMessages lastSeenMessages) { + CompletableFuture eventFuture = server.getCommandManager().callCommandEvent(player, message); + player.getChatQueue().queuePacket( + newLastSeenMessages -> eventFuture + .thenComposeAsync(event -> futurePacketCreator.apply(event, newLastSeenMessages)) .thenApply(pkt -> { if (server.getConfiguration().isLogCommandExecutions()) { logger.info("{} -> executed command /{}", player, message); @@ -68,6 +71,6 @@ public interface CommandHandler { player.sendMessage( Component.translatable("velocity.command.generic-error", NamedTextColor.RED)); return null; - }), timestamp); + }), timestamp, lastSeenMessages); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java index a1ed539d..24e87183 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java @@ -24,7 +24,8 @@ import java.util.BitSet; public class LastSeenMessages { - private static final int DIV_FLOOR = -Math.floorDiv(-20, 8); + public static final int WINDOW_SIZE = 20; + private static final int DIV_FLOOR = -Math.floorDiv(-WINDOW_SIZE, 8); private int offset; private BitSet acknowledged; @@ -33,6 +34,11 @@ public class LastSeenMessages { this.acknowledged = new BitSet(); } + public LastSeenMessages(int offset, BitSet acknowledged) { + this.offset = offset; + this.acknowledged = acknowledged; + } + public LastSeenMessages(ByteBuf buf) { this.offset = ProtocolUtils.readVarInt(buf); @@ -54,6 +60,14 @@ public class LastSeenMessages { return this.offset; } + public BitSet getAcknowledged() { + return acknowledged; + } + + public LastSeenMessages offset(final int offset) { + return new LastSeenMessages(this.offset + offset, acknowledged); + } + @Override public String toString() { return "LastSeenMessages{" + diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedChatHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedChatHandler.java index f4964e6c..f8fc906a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedChatHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedChatHandler.java @@ -91,11 +91,12 @@ public class KeyedChatHandler implements }); } chatQueue.queuePacket( - chatFuture.exceptionally((ex) -> { + newLastSeen -> chatFuture.exceptionally((ex) -> { logger.error("Exception while handling player chat for {}", player, ex); return null; }), - packet.getExpiry() + packet.getExpiry(), + null ); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java index bef75247..1d3751e4 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java @@ -43,7 +43,7 @@ public class KeyedCommandHandler implements CommandHandler { + queueCommandResult(this.server, this.player, (event, newLastSeenMessages) -> { CommandExecuteEvent.CommandResult result = event.getResult(); IdentifiedKey playerKey = player.getIdentifiedKey(); if (result == CommandExecuteEvent.CommandResult.denied()) { @@ -111,6 +111,6 @@ public class KeyedCommandHandler implements CommandHandler { @Override public void handlePlayerCommandInternal(LegacyChatPacket packet) { String command = packet.getMessage().substring(1); - queueCommandResult(this.server, this.player, event -> { + queueCommandResult(this.server, this.player, (event, newLastSeenMessages) -> { CommandExecuteEvent.CommandResult result = event.getResult(); if (result == CommandExecuteEvent.CommandResult.denied()) { return CompletableFuture.completedFuture(null); @@ -62,6 +62,6 @@ public class LegacyCommandHandler implements CommandHandler { } return null; }); - }, command, Instant.now()); + }, command, Instant.now(), null); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java index 20268f90..3598c630 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java @@ -29,6 +29,8 @@ import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import java.util.concurrent.CompletableFuture; + public class SessionChatHandler implements ChatHandler { private static final Logger logger = LogManager.getLogger(SessionChatHandler.class); @@ -51,8 +53,9 @@ public class SessionChatHandler implements ChatHandler ChatQueue chatQueue = this.player.getChatQueue(); EventManager eventManager = this.server.getEventManager(); PlayerChatEvent toSend = new PlayerChatEvent(player, packet.getMessage()); + CompletableFuture eventFuture = eventManager.fire(toSend); chatQueue.queuePacket( - eventManager.fire(toSend) + newLastSeenMessages -> eventFuture .thenApply(pme -> { PlayerChatEvent.ChatResult chatResult = pme.getResult(); if (!chatResult.isAllowed()) { @@ -72,13 +75,14 @@ public class SessionChatHandler implements ChatHandler .setTimestamp(packet.timestamp) .toServer(); } - return packet; + return packet.withLastSeenMessages(newLastSeenMessages); }) .exceptionally((ex) -> { logger.error("Exception while handling player chat for {}", player, ex); return null; }), - packet.getTimestamp() + packet.getTimestamp(), + packet.getLastSeenMessages() ); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java index 16a02a78..41a710b3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java @@ -93,23 +93,25 @@ public class SessionCommandHandler implements CommandHandler { + queueCommandResult(this.server, this.player, (event, newLastSeenMessages) -> { + SessionPlayerCommandPacket fixedPacket = packet.withLastSeenMessages(newLastSeenMessages); + CommandExecuteEvent.CommandResult result = event.getResult(); if (result == CommandExecuteEvent.CommandResult.denied()) { - return CompletableFuture.completedFuture(consumeCommand(packet)); + return CompletableFuture.completedFuture(consumeCommand(fixedPacket)); } - String commandToRun = result.getCommand().orElse(packet.command); + String commandToRun = result.getCommand().orElse(fixedPacket.command); if (result.isForwardToServer()) { - return CompletableFuture.completedFuture(forwardCommand(packet, commandToRun)); + return CompletableFuture.completedFuture(forwardCommand(fixedPacket, commandToRun)); } return runCommand(this.server, this.player, commandToRun, hasRun -> { if (hasRun) { - return consumeCommand(packet); + return consumeCommand(fixedPacket); } - return forwardCommand(packet, commandToRun); + return forwardCommand(fixedPacket, commandToRun); }); - }, packet.command, packet.timeStamp); + }, packet.command, packet.timeStamp, packet.lastSeenMessages); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerChatPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerChatPacket.java index c49170fd..41f37366 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerChatPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerChatPacket.java @@ -99,4 +99,15 @@ public class SessionPlayerChatPacket implements MinecraftPacket { buf.readBytes(signature); return signature; } + + public SessionPlayerChatPacket withLastSeenMessages(LastSeenMessages lastSeenMessages) { + SessionPlayerChatPacket packet = new SessionPlayerChatPacket(); + packet.message = message; + packet.timestamp = timestamp; + packet.salt = salt; + packet.signed = signed; + packet.signature = signature; + packet.lastSeenMessages = lastSeenMessages; + return packet; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java index c3b1ae3c..f8ac8560 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java @@ -25,6 +25,8 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages; import com.velocitypowered.proxy.util.except.QuietDecoderException; import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.Nullable; + import java.time.Instant; import java.util.List; @@ -83,6 +85,21 @@ public class SessionPlayerCommandPacket implements MinecraftPacket { '}'; } + public SessionPlayerCommandPacket withLastSeenMessages(@Nullable LastSeenMessages lastSeenMessages) { + if (lastSeenMessages == null) { + UnsignedPlayerCommandPacket packet = new UnsignedPlayerCommandPacket(); + packet.command = command; + return packet; + } + SessionPlayerCommandPacket packet = new SessionPlayerCommandPacket(); + packet.command = command; + packet.timeStamp = timeStamp; + packet.salt = salt; + packet.argumentSignatures = argumentSignatures; + packet.lastSeenMessages = lastSeenMessages; + return packet; + } + public static class ArgumentSignatures { private final List entries; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/UnsignedPlayerCommandPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/UnsignedPlayerCommandPacket.java index dd59e3ff..b4e26fe0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/UnsignedPlayerCommandPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/UnsignedPlayerCommandPacket.java @@ -19,7 +19,9 @@ package com.velocitypowered.proxy.protocol.packet.chat.session; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages; import io.netty.buffer.ByteBuf; +import org.checkerframework.checker.nullness.qual.Nullable; public class UnsignedPlayerCommandPacket extends SessionPlayerCommandPacket { @@ -33,6 +35,11 @@ public class UnsignedPlayerCommandPacket extends SessionPlayerCommandPacket { ProtocolUtils.writeString(buf, this.command); } + @Override + public SessionPlayerCommandPacket withLastSeenMessages(@Nullable LastSeenMessages lastSeenMessages) { + return this; + } + public boolean isSigned() { return false; } From 077968089eecb6ce653518a948a2025aad6fbae4 Mon Sep 17 00:00:00 2001 From: Gegy Date: Mon, 27 Nov 2023 18:30:32 +0100 Subject: [PATCH 11/29] Fix: pass through last seen update in a few missing places This is hardly useful in the case of signed player chat messages, as these fundamentally cannot be spoofed or modified - however, for any unsigned input (particularly commands without any signed arguments), 'last seen' updates are still required and should be passed through in a consistent state. --- .../proxy/connection/client/ConnectedPlayer.java | 1 + .../proxy/protocol/packet/chat/builder/ChatBuilderV2.java | 7 +++++++ .../protocol/packet/chat/session/SessionChatBuilder.java | 7 ++++--- .../protocol/packet/chat/session/SessionChatHandler.java | 1 + .../packet/chat/session/SessionCommandHandler.java | 1 + 5 files changed, 14 insertions(+), 3 deletions(-) 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 6beeb9cf..df6769ff 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 @@ -1112,6 +1112,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, ChatBuilderV2 message = getChatBuilderFactory().builder().asPlayer(this).message(input); this.chatQueue.queuePacket(chatState -> { message.setTimestamp(chatState.lastTimestamp); + message.setLastSeenMessages(chatState.createLastSeen()); return message.toServer(); }); } else { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/builder/ChatBuilderV2.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/builder/ChatBuilderV2.java index e9e24c2a..9ac02864 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/builder/ChatBuilderV2.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/builder/ChatBuilderV2.java @@ -21,6 +21,7 @@ import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.chat.ChatType; +import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages; import java.time.Instant; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; @@ -36,6 +37,7 @@ public abstract class ChatBuilderV2 { protected @Nullable Identity senderIdentity; protected Instant timestamp; protected ChatType type = ChatType.CHAT; + protected @Nullable LastSeenMessages lastSeenMessages; protected ChatBuilderV2(ProtocolVersion version) { this.version = version; @@ -77,6 +79,11 @@ public abstract class ChatBuilderV2 { return this; } + public ChatBuilderV2 setLastSeenMessages(LastSeenMessages lastSeenMessages) { + this.lastSeenMessages = lastSeenMessages; + return this; + } + public abstract MinecraftPacket toClient(); public abstract MinecraftPacket toServer(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatBuilder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatBuilder.java index 9a4fdc72..eb74123f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatBuilder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatBuilder.java @@ -41,6 +41,7 @@ public class SessionChatBuilder extends ChatBuilderV2 { @Override public MinecraftPacket toServer() { + LastSeenMessages lastSeenMessages = this.lastSeenMessages != null ? this.lastSeenMessages : new LastSeenMessages(); if (message.startsWith("/")) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) { UnsignedPlayerCommandPacket command = new UnsignedPlayerCommandPacket(); @@ -52,7 +53,7 @@ public class SessionChatBuilder extends ChatBuilderV2 { command.salt = 0L; command.timeStamp = timestamp; command.argumentSignatures = new SessionPlayerCommandPacket.ArgumentSignatures(); - command.lastSeenMessages = new LastSeenMessages(); + command.lastSeenMessages = lastSeenMessages; return command; } } else { @@ -62,8 +63,8 @@ public class SessionChatBuilder extends ChatBuilderV2 { chat.signature = new byte[0]; chat.timestamp = timestamp; chat.salt = 0L; - chat.lastSeenMessages = new LastSeenMessages(); + chat.lastSeenMessages = lastSeenMessages; return chat; } } -} \ No newline at end of file +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java index 3598c630..0731f64e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java @@ -73,6 +73,7 @@ public class SessionChatHandler implements ChatHandler } return this.player.getChatBuilderFactory().builder().message(packet.message) .setTimestamp(packet.timestamp) + .setLastSeenMessages(newLastSeenMessages) .toServer(); } return packet.withLastSeenMessages(newLastSeenMessages); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java index 41a710b3..51eada65 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java @@ -86,6 +86,7 @@ public class SessionCommandHandler implements CommandHandler Date: Mon, 27 Nov 2023 18:41:57 +0100 Subject: [PATCH 12/29] Fix: only treat command as signed if it has signatures The Vanilla client will send 'last seen' even if the message is not signed, so this is much stricter than it needs to be. This should allow commands without any signed arguments to be modified/consumed by the proxy. --- .../packet/chat/session/SessionPlayerCommandPacket.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java index f8ac8560..07374c62 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java @@ -65,8 +65,7 @@ public class SessionPlayerCommandPacket implements MinecraftPacket { } public boolean isSigned() { - if (salt == 0) return false; - return !lastSeenMessages.isEmpty() || !argumentSignatures.isEmpty(); + return !argumentSignatures.isEmpty(); } @Override From 4eae510fb79338f61c1dd58e60376ebe6515031f Mon Sep 17 00:00:00 2001 From: Gegy Date: Mon, 27 Nov 2023 18:42:11 +0100 Subject: [PATCH 13/29] Don't forward chat acknowledgement if the client didn't acknowledge any chat --- .../proxy/protocol/packet/chat/LastSeenMessages.java | 4 ---- .../protocol/packet/chat/session/SessionCommandHandler.java | 6 +++++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java index 24e87183..18a743c8 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java @@ -52,10 +52,6 @@ public class LastSeenMessages { buf.writeBytes(Arrays.copyOf(acknowledged.toByteArray(), DIV_FLOOR)); } - public boolean isEmpty() { - return acknowledged.isEmpty(); - } - public int getOffset() { return this.offset; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java index 51eada65..0e47feed 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java @@ -60,7 +60,11 @@ public class SessionCommandHandler implements CommandHandler Date: Fri, 5 Jul 2024 22:01:31 -0400 Subject: [PATCH 14/29] Don't try to load a candidate plugin with the same ID more than once Fixes #1372. Honestly, this was an oversight that I'm somewhat surprised that nobody caught until recently. --- .../proxy/plugin/VelocityPluginManager.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java index 2c6f752f..367219c6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java @@ -43,13 +43,13 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; +import java.util.HashMap; import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; -import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -107,14 +107,25 @@ public class VelocityPluginManager implements PluginManager { List sortedPlugins = PluginDependencyUtils.sortCandidates(found); - Set loadedPluginsById = new HashSet<>(); + Map loadedCandidates = new HashMap<>(); Map pluginContainers = new LinkedHashMap<>(); // Now load the plugins pluginLoad: for (PluginDescription candidate : sortedPlugins) { + // If we found a duplicate candidate (with the same ID), don't load it. + PluginDescription existingCandidate = loadedCandidates.get(candidate.getId()); + if (existingCandidate != null) { + logger.error("Refusing to load plugin at path {} since we already " + + "loaded a plugin with the same ID {} from {}", + candidate.getSource().map(Objects::toString).orElse(""), + candidate.getId(), + existingCandidate.getSource().map(Objects::toString).orElse("")); + continue; + } + // Verify dependencies for (PluginDependency dependency : candidate.getDependencies()) { - if (!dependency.isOptional() && !loadedPluginsById.contains(dependency.getId())) { + if (!dependency.isOptional() && !loadedCandidates.containsKey(dependency.getId())) { logger.error("Can't load plugin {} due to missing dependency {}", candidate.getId(), dependency.getId()); continue pluginLoad; @@ -125,7 +136,7 @@ public class VelocityPluginManager implements PluginManager { PluginDescription realPlugin = loader.createPluginFromCandidate(candidate); VelocityPluginContainer container = new VelocityPluginContainer(realPlugin); pluginContainers.put(container, loader.createModule(container)); - loadedPluginsById.add(realPlugin.getId()); + loadedCandidates.put(realPlugin.getId(), realPlugin); } catch (Throwable e) { logger.error("Can't create module for plugin {}", candidate.getId(), e); } From 5154f029108e3d3ba804b9d4555903347f2376c2 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Fri, 5 Jul 2024 22:07:15 -0400 Subject: [PATCH 15/29] Improved fix for #1372 Realized that the precondition that gets violated happens way too early, in `Maps.uniqueIndex()`. --- .../proxy/plugin/VelocityPluginManager.java | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java index 367219c6..0af477c4 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java @@ -86,43 +86,45 @@ public class VelocityPluginManager implements PluginManager { checkNotNull(directory, "directory"); checkArgument(directory.toFile().isDirectory(), "provided path isn't a directory"); - List found = new ArrayList<>(); + Map foundCandidates = new LinkedHashMap<>(); JavaPluginLoader loader = new JavaPluginLoader(server, directory); try (DirectoryStream stream = Files.newDirectoryStream(directory, p -> p.toFile().isFile() && p.toString().endsWith(".jar"))) { for (Path path : stream) { try { - found.add(loader.loadCandidate(path)); + PluginDescription candidate = loader.loadCandidate(path); + + // If we found a duplicate candidate (with the same ID), don't load it. + PluginDescription maybeExistingCandidate = foundCandidates.putIfAbsent( + candidate.getId(), candidate); + + if (maybeExistingCandidate != null) { + logger.error("Refusing to load plugin at path {} since we already " + + "loaded a plugin with the same ID {} from {}", + candidate.getSource().map(Objects::toString).orElse(""), + candidate.getId(), + maybeExistingCandidate.getSource().map(Objects::toString).orElse("")); + } } catch (Throwable e) { logger.error("Unable to load plugin {}", path, e); } } } - if (found.isEmpty()) { + if (foundCandidates.isEmpty()) { // No plugins found return; } - List sortedPlugins = PluginDependencyUtils.sortCandidates(found); + List sortedPlugins = PluginDependencyUtils.sortCandidates( + new ArrayList<>(foundCandidates.values())); Map loadedCandidates = new HashMap<>(); Map pluginContainers = new LinkedHashMap<>(); // Now load the plugins pluginLoad: for (PluginDescription candidate : sortedPlugins) { - // If we found a duplicate candidate (with the same ID), don't load it. - PluginDescription existingCandidate = loadedCandidates.get(candidate.getId()); - if (existingCandidate != null) { - logger.error("Refusing to load plugin at path {} since we already " - + "loaded a plugin with the same ID {} from {}", - candidate.getSource().map(Objects::toString).orElse(""), - candidate.getId(), - existingCandidate.getSource().map(Objects::toString).orElse("")); - continue; - } - // Verify dependencies for (PluginDependency dependency : candidate.getDependencies()) { if (!dependency.isOptional() && !loadedCandidates.containsKey(dependency.getId())) { From 82e81909d8e77f5206a75fa5567cf4b7a7e0b986 Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Wed, 10 Jul 2024 23:00:08 +0100 Subject: [PATCH 16/29] Add handling for KickPlayerRaw --- .../connection/backend/BungeeCordMessageResponder.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 ea28d2c9..b047d186 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 @@ -260,6 +260,13 @@ public class BungeeCordMessageResponder { }); } + private void processKickRaw(ByteBufDataInput in) { + proxy.getPlayer(in.readUTF()).ifPresent(player -> { + String kickReason = in.readUTF(); + player.disconnect(GsonComponentSerializer.gson().deserialize(kickReason)); + }); + } + private void processForwardToPlayer(ByteBufDataInput in) { Optional player = proxy.getPlayer(in.readUTF()); if (player.isPresent()) { @@ -372,6 +379,9 @@ public class BungeeCordMessageResponder { case "KickPlayer": this.processKick(in); break; + case "KickPlayerRaw": + this.processKickRaw(in); + break; default: // Do nothing, unknown command break; From 79a5634dfee7a34848aeb24346ef45fd8021a42d Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Wed, 10 Jul 2024 23:01:08 +0100 Subject: [PATCH 17/29] [ci skip] fix some trivial javadoc warns --- api/build.gradle.kts | 2 +- .../java/com/velocitypowered/api/command/CommandManager.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/build.gradle.kts b/api/build.gradle.kts index aa6778fc..e7dd2bf4 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -67,7 +67,7 @@ tasks { "https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/", "https://docs.oracle.com/en/java/javase/17/docs/api/", "https://jd.advntr.dev/api/${libs.adventure.bom.get().version}/", - "https://javadoc.io/doc/com.github.ben-manes.caffeine/caffeine" + "https://javadoc.io/doc/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/" ) o.tags( diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java index 6cf90a79..257fc9e6 100644 --- a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java +++ b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java @@ -44,7 +44,7 @@ public interface CommandManager { * @param otherAliases additional aliases * @throws IllegalArgumentException if one of the given aliases is already registered, or * the given command does not implement a registrable {@link Command} subinterface - * @see Command for a list of registrable {@link Command} subinterfaces + * @see Command for a list of registrable Command subinterfaces */ default void register(String alias, Command command, String... otherAliases) { register(metaBuilder(alias).aliases(otherAliases).build(), command); @@ -65,7 +65,7 @@ public interface CommandManager { * @param command the command to register * @throws IllegalArgumentException if one of the given aliases is already registered, or * the given command does not implement a registrable {@link Command} subinterface - * @see Command for a list of registrable {@link Command} subinterfaces + * @see Command for a list of registrable Command subinterfaces */ void register(CommandMeta meta, Command command); From e0f74a8493962e03918b879a14a5f9e1834f7eb6 Mon Sep 17 00:00:00 2001 From: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Date: Wed, 10 Jul 2024 21:15:28 -0700 Subject: [PATCH 18/29] Bump Adventure to 4.17 (#1376) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a3d0523d..510c4d62 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,7 +11,7 @@ shadow = "io.github.goooler.shadow:8.1.5" spotless = "com.diffplug.spotless:6.25.0" [libraries] -adventure-bom = "net.kyori:adventure-bom:4.16.0" +adventure-bom = "net.kyori:adventure-bom:4.17.0" adventure-facet = "net.kyori:adventure-platform-facet:4.3.2" asm = "org.ow2.asm:asm:9.6" auto-service = "com.google.auto.service:auto-service:1.0.1" From 6073f698e28ec29eeb82e3d1bfa06493955d8a36 Mon Sep 17 00:00:00 2001 From: Gero Date: Fri, 12 Jul 2024 11:16:42 +0200 Subject: [PATCH 19/29] Add PlayerConfigurationEvent and PlayerEnteredConfigurationEvent (#1371) * Configuring the player (i.e. sending resource packs) should now be done in the new PlayerConfigurationEvent. * The new PlayerEnteredConfigurationEvent is called when a player acknowledged the switch to configuration state. * The PlayerEnterConfigurationEvent is no longer called twice. It is now called when the backed wants to reconfigure the player. * The PlayerFinishConfigurationEvent should no longer be used to configure the player (i.e. sending resource packs). This is because since 1.20.5 the backend server can't send keep alive packets between switching state anymore and the connection will thus time out. --- .../PlayerConfigurationEvent.java | 26 +++++++ .../PlayerEnterConfigurationEvent.java | 16 +++-- .../PlayerEnteredConfigurationEvent.java | 27 ++++++++ .../PlayerFinishConfigurationEvent.java | 11 +-- .../PlayerFinishedConfigurationEvent.java | 4 +- .../backend/LoginSessionHandler.java | 16 ++--- .../connection/client/AuthSessionHandler.java | 31 ++++----- .../client/ClientConfigSessionHandler.java | 68 ++++++++++--------- .../client/ClientPlaySessionHandler.java | 17 +---- .../connection/client/ConnectedPlayer.java | 52 +++++++++++--- 10 files changed, 173 insertions(+), 95 deletions(-) create mode 100644 api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerConfigurationEvent.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnteredConfigurationEvent.java diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerConfigurationEvent.java new file mode 100644 index 00000000..6e042af1 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerConfigurationEvent.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 a player entered the configuration state and can be configured by Velocity. + *

Velocity will wait for this event before continuing/ending the configuration state.

+ * + * @param player The player who can be configured. + * @param server The server that is currently configuring the player. + * @since 3.3.0 + * @sinceMinecraft 1.20.2 + */ +@AwaitingEvent +public record PlayerConfigurationEvent(@NotNull Player player, ServerConnection server) { +} 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 index 3b108c6a..05d6c2af 100644 --- 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 @@ -7,21 +7,23 @@ package com.velocitypowered.api.event.player.configuration; -import com.velocitypowered.api.network.ProtocolState; +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 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}.

+ * This event is executed when a player is about to enter the configuration state. + * It is not called for the initial configuration of a player after login. + *

Velocity will wait for this event before asking the client to enter configuration state. + * However due to backend server being unable to keep the connection alive during state changes, + * Velocity will only wait for a maximum of 5 seconds.

* - * @param player The player that has entered the configuration phase. - * @param server The server that will now (re-)configure the player. + * @param player The player who is about to enter configuration state. + * @param server The server that wants to reconfigure the player. * @since 3.3.0 * @sinceMinecraft 1.20.2 */ +@AwaitingEvent public record PlayerEnterConfigurationEvent(@NotNull Player player, ServerConnection server) { } diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnteredConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnteredConfigurationEvent.java new file mode 100644 index 00000000..c1677706 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnteredConfigurationEvent.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 has entered the configuration state. + *

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 who has entered the configuration state. + * @param server The server that will now (re-)configure the player. + * @since 3.3.0 + * @sinceMinecraft 1.20.2 + */ +public record PlayerEnteredConfigurationEvent(@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 index f6249b89..50df5a8a 100644 --- 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 @@ -13,11 +13,14 @@ 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.

+ * This event is executed when a player is about to finish the configuration state. + *

Velocity will wait for this event before asking the client to finish the configuration state. + * However due to backend server being unable to keep the connection alive during state changes, + * Velocity will only wait for a maximum of 5 seconds. If you need to hold a player in configuration + * state, use the {@link PlayerConfigurationEvent}.

* - * @param player The player who is about to complete the configuration phase. - * @param server The server that is currently (re-)configuring the player. + * @param player The player who is about to finish the configuration phase. + * @param server The server that has (re-)configured the player. * @since 3.3.0 * @sinceMinecraft 1.20.2 */ 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 index 09e76104..517f119c 100644 --- 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 @@ -13,11 +13,11 @@ 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. + * This event is executed when a player has finished 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 player The player who has finished the configuration state. * @param server The server that has (re-)configured the player. * @since 3.3.0 * @sinceMinecraft 1.20.2 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 a2d2205e..0fdb5fa2 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 @@ -19,6 +19,7 @@ package com.velocitypowered.proxy.connection.backend; import com.velocitypowered.api.event.player.CookieRequestEvent; import com.velocitypowered.api.event.player.ServerLoginPluginMessageEvent; +import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.velocitypowered.proxy.VelocityServer; @@ -142,10 +143,8 @@ public class LoginSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(ServerLoginSuccessPacket packet) { - if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN - && !informationForwarded) { - resultFuture.complete(ConnectionRequestResults.forDisconnect(MODERN_IP_FORWARDING_FAILURE, - serverConn.getServer())); + if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && !informationForwarded) { + resultFuture.complete(ConnectionRequestResults.forDisconnect(MODERN_IP_FORWARDING_FAILURE, serverConn.getServer())); serverConn.disconnect(); return true; } @@ -156,12 +155,10 @@ public class LoginSessionHandler implements MinecraftSessionHandler { // Move into the PLAY phase. MinecraftConnection smc = serverConn.ensureConnected(); if (smc.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) { - smc.setActiveSessionHandler(StateRegistry.PLAY, - new TransitionSessionHandler(server, serverConn, resultFuture)); + smc.setActiveSessionHandler(StateRegistry.PLAY, new TransitionSessionHandler(server, serverConn, resultFuture)); } else { smc.write(new LoginAcknowledgedPacket()); - smc.setActiveSessionHandler(StateRegistry.CONFIG, - new ConfigSessionHandler(server, serverConn, resultFuture)); + smc.setActiveSessionHandler(StateRegistry.CONFIG, new ConfigSessionHandler(server, serverConn, resultFuture)); ConnectedPlayer player = serverConn.getPlayer(); if (player.getClientSettingsPacket() != null) { smc.write(player.getClientSettingsPacket()); @@ -169,6 +166,9 @@ public class LoginSessionHandler implements MinecraftSessionHandler { if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler clientPlaySessionHandler) { smc.setAutoReading(false); clientPlaySessionHandler.doSwitch().thenAcceptAsync((unused) -> smc.setAutoReading(true), smc.eventLoop()); + } else { + // Initial login - the player is already in configuration state. + server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConn)); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java index 77290584..ac02bcf7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java @@ -178,14 +178,14 @@ public class AuthSessionHandler implements MinecraftSessionHandler { inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_data")); } else { loginState = State.ACKNOWLEDGED; - mcConnection.setActiveSessionHandler(StateRegistry.CONFIG, - new ClientConfigSessionHandler(server, connectedPlayer)); + mcConnection.setActiveSessionHandler(StateRegistry.CONFIG, new ClientConfigSessionHandler(server, connectedPlayer)); - server.getEventManager().fire(new PostLoginEvent(connectedPlayer)) - .thenCompose((ignored) -> connectToInitialServer(connectedPlayer)).exceptionally((ex) -> { - logger.error("Exception while connecting {} to initial server", connectedPlayer, ex); - return null; - }); + server.getEventManager().fire(new PostLoginEvent(connectedPlayer)).thenCompose(ignored -> { + return connectToInitialServer(connectedPlayer); + }).exceptionally((ex) -> { + logger.error("Exception while connecting {} to initial server", connectedPlayer, ex); + return null; + }); } return true; } @@ -224,8 +224,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler { player.disconnect0(reason.get(), true); } else { if (!server.registerConnection(player)) { - player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"), - true); + player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"), true); return; } @@ -238,13 +237,13 @@ public class AuthSessionHandler implements MinecraftSessionHandler { loginState = State.SUCCESS_SENT; if (inbound.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) { loginState = State.ACKNOWLEDGED; - mcConnection.setActiveSessionHandler(StateRegistry.PLAY, - new InitialConnectSessionHandler(player, server)); - server.getEventManager().fire(new PostLoginEvent(player)) - .thenCompose((ignored) -> connectToInitialServer(player)).exceptionally((ex) -> { - logger.error("Exception while connecting {} to initial server", player, ex); - return null; - }); + mcConnection.setActiveSessionHandler(StateRegistry.PLAY, new InitialConnectSessionHandler(player, server)); + server.getEventManager().fire(new PostLoginEvent(player)).thenCompose((ignored) -> { + return connectToInitialServer(player); + }).exceptionally((ex) -> { + logger.error("Exception while connecting {} to initial server", player, ex); + return null; + }); } } }, mcConnection.eventLoop()).exceptionally((ex) -> { 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 3d955e90..7d232b09 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,7 @@ 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.PlayerConfigurationEvent; import com.velocitypowered.api.event.player.configuration.PlayerFinishConfigurationEvent; import com.velocitypowered.api.event.player.configuration.PlayerFinishedConfigurationEvent; import com.velocitypowered.proxy.VelocityServer; @@ -48,8 +49,6 @@ import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; /** * Handles the client config stage. @@ -61,6 +60,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { private final ConnectedPlayer player; private String brandChannel = null; + private CompletableFuture configurationFuture; private CompletableFuture configSwitchFuture; /** @@ -81,11 +81,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(final KeepAlivePacket packet) { - final VelocityServerConnection serverConnection = player.getConnectedServer(); - if (!this.sendKeepAliveToBackend(serverConnection, packet)) { - final VelocityServerConnection connectionInFlight = player.getConnectionInFlight(); - this.sendKeepAliveToBackend(connectionInFlight, packet); - } + player.forwardKeepAlive(packet); return true; } @@ -106,8 +102,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(FinishedUpdatePacket packet) { - player.getConnection() - .setActiveSessionHandler(StateRegistry.PLAY, new ClientPlaySessionHandler(server, player)); + player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, new ClientPlaySessionHandler(server, player)); configSwitchFuture.complete(null); return true; @@ -141,12 +136,14 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(KnownPacksPacket packet) { - if (player.getConnectionInFlight() != null) { - player.getConnectionInFlight().ensureConnected().write(packet); - return true; - } + callConfigurationEvent().thenRun(() -> { + player.getConnectionInFlightOrConnectedServer().ensureConnected().write(packet); + }).exceptionally(ex -> { + logger.error("Error forwarding known packs response to backend:", ex); + return null; + }); - return false; + return true; } @Override @@ -209,26 +206,25 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { @Override public void exception(Throwable throwable) { - player.disconnect( - Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED)); + player.disconnect(Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED)); } - private boolean sendKeepAliveToBackend( - final @Nullable VelocityServerConnection serverConnection, - final @NotNull KeepAlivePacket packet - ) { - if (serverConnection != null) { - final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); - if (sentTime != null) { - final MinecraftConnection smc = serverConnection.getConnection(); - if (smc != null) { - player.setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime)); - smc.write(packet); - return true; - } - } + /** + * Calls the {@link PlayerConfigurationEvent}. + * For 1.20.5+ backends this is done when the client responds to + * the known packs request. The response is delayed until the event + * has been called. + * For 1.20.2-1.20.4 servers this is done when the client acknowledges + * the end of the configuration. + * This is handled differently because for 1.20.5+ servers can't keep + * their connection alive between states and older servers don't have + * the known packs transaction. + */ + private CompletableFuture callConfigurationEvent() { + if (configurationFuture != null) { + return configurationFuture; } - return false; + return configurationFuture = server.getEventManager().fire(new PlayerConfigurationEvent(player, player.getConnectionInFlightOrConnectedServer())); } /** @@ -248,11 +244,17 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { smc.write(brandPacket); } - server.getEventManager().fire(new PlayerFinishConfigurationEvent(player, serverConn)).thenAcceptAsync(event -> { + callConfigurationEvent().thenCompose(v -> { + return server.getEventManager().fire(new PlayerFinishConfigurationEvent(player, serverConn)) + .completeOnTimeout(null, 5, TimeUnit.SECONDS); + }).thenRunAsync(() -> { player.getConnection().write(FinishedUpdatePacket.INSTANCE); player.getConnection().getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.PLAY); server.getEventManager().fireAndForget(new PlayerFinishedConfigurationEvent(player, serverConn)); - }, player.getConnection().eventLoop()); + }, player.getConnection().eventLoop()).exceptionally(ex -> { + logger.error("Error finishing configuration state:", ex); + return null; + }); 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 f2df78a7..fed61693 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,7 +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.event.player.configuration.PlayerEnteredConfigurationEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; @@ -86,7 +86,6 @@ import java.util.Queue; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.concurrent.TimeUnit; import net.kyori.adventure.key.Key; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -178,17 +177,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(KeepAlivePacket packet) { - final VelocityServerConnection serverConnection = player.getConnectedServer(); - if (serverConnection != null) { - final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); - if (sentTime != null) { - final MinecraftConnection smc = serverConnection.getConnection(); - if (smc != null) { - player.setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime)); - smc.write(packet); - } - } - } + player.forwardKeepAlive(packet); return true; } @@ -408,7 +397,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)); + server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConnection)); if (serverConnection != null) { MinecraftConnection smc = serverConnection.ensureConnected(); CompletableFuture.runAsync(() -> { 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 df6769ff..11f6b529 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 @@ -111,6 +111,7 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; import net.kyori.adventure.audience.MessageType; import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.identity.Identity; @@ -634,6 +635,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, return connectionInFlight; } + public VelocityServerConnection getConnectionInFlightOrConnectedServer() { + return connectionInFlight != null ? connectionInFlight : connectedServer; + } + public void resetInFlightConnection() { connectionInFlight = null; } @@ -1239,21 +1244,46 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } } + /** + * Forwards the keep alive packet to the backend server it belongs to. + * This is either the connection in flight or the connected server. + */ + public boolean forwardKeepAlive(final KeepAlivePacket packet) { + if (!this.sendKeepAliveToBackend(connectedServer, packet)) { + return this.sendKeepAliveToBackend(connectionInFlight, packet); + } + return false; + } + + private boolean sendKeepAliveToBackend(final @Nullable VelocityServerConnection serverConnection, final @NotNull KeepAlivePacket packet) { + if (serverConnection != null) { + final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); + if (sentTime != null) { + final MinecraftConnection smc = serverConnection.getConnection(); + if (smc != null) { + setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime)); + smc.write(packet); + return true; + } + } + } + return false; + } + /** * Switches the connection to the client into config state. */ public void switchToConfigState() { - CompletableFuture.runAsync(() -> { - connection.write(StartUpdatePacket.INSTANCE); - connection.getChannel().pipeline() - .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); - return null; - }); + server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, getConnectionInFlightOrConnectedServer())) + .completeOnTimeout(null, 5, TimeUnit.SECONDS).thenRunAsync(() -> { + connection.write(StartUpdatePacket.INSTANCE); + connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG); + // Make sure we don't send any play packets to the player after update start + connection.addPlayPacketQueueHandler(); + }, connection.eventLoop()).exceptionally((ex) -> { + logger.error("Error switching player connection to config state", ex); + return null; + }); } /** From d5e7fa5b84d2146dc7d7144714a893c959530fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n?= <44579213+Rubenicos@users.noreply.github.com> Date: Fri, 12 Jul 2024 04:02:00 -0600 Subject: [PATCH 20/29] Add ServerResourcePackRemoveEvent (#1379) * Add ServerResourcePackRemoveEvent * Add nonnull check * Fix indentation * Fix import code style --- .../player/ServerResourcePackRemoveEvent.java | 68 +++++++++++++++++++ .../backend/BackendPlaySessionHandler.java | 29 +++++--- .../backend/ConfigSessionHandler.java | 30 ++++++++ 3 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 api/src/main/java/com/velocitypowered/api/event/player/ServerResourcePackRemoveEvent.java diff --git a/api/src/main/java/com/velocitypowered/api/event/player/ServerResourcePackRemoveEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/ServerResourcePackRemoveEvent.java new file mode 100644 index 00000000..96d1bb8e --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/ServerResourcePackRemoveEvent.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2018-2023 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; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.event.ResultedEvent; +import com.velocitypowered.api.event.annotation.AwaitingEvent; +import com.velocitypowered.api.proxy.ServerConnection; +import java.util.UUID; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * This event is fired when the downstream server tries to remove a resource pack from player + * or clear all of them. The proxy will wait on this event to finish before forwarding the + * action to the user. If this event is denied, no resource packs will be removed from player. + */ +@AwaitingEvent +public class ServerResourcePackRemoveEvent implements ResultedEvent { + + private GenericResult result; + private final @MonotonicNonNull UUID packId; + private final ServerConnection serverConnection; + + /** + * Instantiates this event. + */ + public ServerResourcePackRemoveEvent(UUID packId, ServerConnection serverConnection) { + this.result = ResultedEvent.GenericResult.allowed(); + this.packId = packId; + this.serverConnection = serverConnection; + } + + /** + * Returns the id of the resource pack, if it's null all the resource packs + * from player will be cleared. + * + * @return the id + */ + @Nullable + public UUID getPackId() { + return packId; + } + + /** + * Returns the server that tries to remove a resource pack from player or clear all of them. + * + * @return the server connection + */ + public ServerConnection getServerConnection() { + return serverConnection; + } + + @Override + public GenericResult getResult() { + return this.result; + } + + @Override + public void setResult(GenericResult result) { + this.result = Preconditions.checkNotNull(result, "result"); + } +} 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 14989b1a..128d5c37 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 @@ -29,6 +29,7 @@ import com.velocitypowered.api.event.connection.PreTransferEvent; import com.velocitypowered.api.event.player.CookieRequestEvent; import com.velocitypowered.api.event.player.CookieStoreEvent; import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; +import com.velocitypowered.api.event.player.ServerResourcePackRemoveEvent; import com.velocitypowered.api.event.player.ServerResourcePackSendEvent; import com.velocitypowered.api.event.proxy.ProxyPingEvent; import com.velocitypowered.api.network.ProtocolVersion; @@ -258,14 +259,26 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(RemoveResourcePackPacket packet) { - final ConnectedPlayer player = serverConn.getPlayer(); - final ResourcePackHandler handler = player.resourcePackHandler(); - if (packet.getId() != null) { - handler.remove(packet.getId()); - } else { - handler.clearAppliedResourcePacks(); - } - playerConnection.write(packet); + final ServerResourcePackRemoveEvent event = new ServerResourcePackRemoveEvent( + packet.getId(), this.serverConn); + server.getEventManager().fire(event).thenAcceptAsync(serverResourcePackRemoveEvent -> { + if (playerConnection.isClosed()) { + return; + } + if (serverResourcePackRemoveEvent.getResult().isAllowed()) { + final ConnectedPlayer player = serverConn.getPlayer(); + final ResourcePackHandler handler = player.resourcePackHandler(); + if (packet.getId() != null) { + handler.remove(packet.getId()); + } else { + handler.clearAppliedResourcePacks(); + } + playerConnection.write(packet); + } + }, playerConnection.eventLoop()).exceptionally((ex) -> { + logger.error("Exception while handling resource pack remove for {}", playerConnection, ex); + return null; + }); return true; } 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 a19854eb..74f0576c 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 @@ -21,6 +21,7 @@ import com.velocitypowered.api.event.connection.PreTransferEvent; import com.velocitypowered.api.event.player.CookieRequestEvent; import com.velocitypowered.api.event.player.CookieStoreEvent; import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; +import com.velocitypowered.api.event.player.ServerResourcePackRemoveEvent; import com.velocitypowered.api.event.player.ServerResourcePackSendEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.player.ResourcePackInfo; @@ -30,6 +31,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.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; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; @@ -41,6 +43,7 @@ import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; +import com.velocitypowered.proxy.protocol.packet.RemoveResourcePackPacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequestPacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket; import com.velocitypowered.proxy.protocol.packet.TransferPacket; @@ -192,6 +195,33 @@ public class ConfigSessionHandler implements MinecraftSessionHandler { return true; } + @Override + public boolean handle(RemoveResourcePackPacket packet) { + final MinecraftConnection playerConnection = this.serverConn.getPlayer().getConnection(); + + final ServerResourcePackRemoveEvent event = new ServerResourcePackRemoveEvent( + packet.getId(), this.serverConn); + server.getEventManager().fire(event).thenAcceptAsync(serverResourcePackRemoveEvent -> { + if (playerConnection.isClosed()) { + return; + } + if (serverResourcePackRemoveEvent.getResult().isAllowed()) { + final ConnectedPlayer player = serverConn.getPlayer(); + final ResourcePackHandler handler = player.resourcePackHandler(); + if (packet.getId() != null) { + handler.remove(packet.getId()); + } else { + handler.clearAppliedResourcePacks(); + } + playerConnection.write(packet); + } + }, playerConnection.eventLoop()).exceptionally((ex) -> { + logger.error("Exception while handling resource pack remove for {}", playerConnection, ex); + return null; + }); + return true; + } + @Override public boolean handle(FinishedUpdatePacket packet) { final MinecraftConnection smc = serverConn.ensureConnected(); From ebc418f6ccadb8e8a3ce5f5679fe9b79c6724d56 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sat, 13 Jul 2024 21:50:27 -0400 Subject: [PATCH 21/29] Do not use classes from `io.netty.util.internal` In this case, we don't even need a concurrency-friendly MPSC queue, since this code only ever gets run in the event loop associated to the channel, so a simple `ArrayDeque` is enough for our needs. --- .../proxy/protocol/netty/PlayPacketQueueInboundHandler.java | 4 ++-- .../proxy/protocol/netty/PlayPacketQueueOutboundHandler.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 index fe553f76..1affc13b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueInboundHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueInboundHandler.java @@ -24,7 +24,7 @@ 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.ArrayDeque; import java.util.Queue; import org.jetbrains.annotations.NotNull; @@ -42,7 +42,7 @@ import org.jetbrains.annotations.NotNull; public class PlayPacketQueueInboundHandler extends ChannelDuplexHandler { private final StateRegistry.PacketRegistry.ProtocolRegistry registry; - private final Queue queue = PlatformDependent.newMpscQueue(); + private final Queue queue = new ArrayDeque<>(); /** * Provides registries for client & server bound packets. diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueOutboundHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueOutboundHandler.java index d5764ef6..c5727104 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueOutboundHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueOutboundHandler.java @@ -25,7 +25,7 @@ import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.util.ReferenceCountUtil; -import io.netty.util.internal.PlatformDependent; +import java.util.ArrayDeque; import java.util.Queue; import org.jetbrains.annotations.NotNull; @@ -43,7 +43,7 @@ import org.jetbrains.annotations.NotNull; public class PlayPacketQueueOutboundHandler extends ChannelDuplexHandler { private final StateRegistry.PacketRegistry.ProtocolRegistry registry; - private final Queue queue = PlatformDependent.newMpscQueue(); + private final Queue queue = new ArrayDeque<>(); /** * Provides registries for client & server bound packets. From d999ee20583445f96934e491da72e043ddf4c5af Mon Sep 17 00:00:00 2001 From: Gero Date: Sun, 14 Jul 2024 16:29:30 +0200 Subject: [PATCH 22/29] Fix PlayerConfigurationEvent only being called for initial configuration on 1.20.5+ --- .../proxy/connection/client/ClientConfigSessionHandler.java | 5 +++++ 1 file changed, 5 insertions(+) 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 7d232b09..7bb7bedf 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 @@ -79,6 +79,11 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler { configSwitchFuture = new CompletableFuture<>(); } + @Override + public void deactivated() { + configurationFuture = null; + } + @Override public boolean handle(final KeepAlivePacket packet) { player.forwardKeepAlive(packet); From a4476253abc4f4486385aa5dda4d8e6d77332f13 Mon Sep 17 00:00:00 2001 From: Jaime Costas <8948346+cijaaimee@users.noreply.github.com> Date: Fri, 19 Jul 2024 20:34:46 +0200 Subject: [PATCH 23/29] End bundle session on server switch (#1385) --- .../proxy/connection/client/ConnectedPlayer.java | 5 +++++ 1 file changed, 5 insertions(+) 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 11f6b529..2b22fc7b 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 @@ -69,6 +69,7 @@ import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.connection.util.VelocityInboundConnection; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; +import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; @@ -1276,6 +1277,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, public void switchToConfigState() { server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, getConnectionInFlightOrConnectedServer())) .completeOnTimeout(null, 5, TimeUnit.SECONDS).thenRunAsync(() -> { + if (bundleHandler.isInBundleSession()) { + bundleHandler.toggleBundleSession(); + connection.write(BundleDelimiterPacket.INSTANCE); + } connection.write(StartUpdatePacket.INSTANCE); connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG); // Make sure we don't send any play packets to the player after update start From 44b1e0c6f923bc45201be9010abab12cf16f7afa Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Mon, 22 Jul 2024 15:42:42 +0100 Subject: [PATCH 24/29] Print correct exception when hotspot dump fails --- .../velocitypowered/proxy/command/builtin/VelocityCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java index 7904a06a..79ac41e7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java @@ -379,7 +379,7 @@ public final class VelocityCommand { this.heapGenerator.invoke(hotspotMbean, file.toString(), true); } catch (Throwable e1) { // This should not occur - throw new RuntimeException(e); + throw new RuntimeException(e1); } src.sendMessage(Component.text("Heap dump saved to " + file, NamedTextColor.GREEN)); }; From 09f687e5d5f69747473c033b69cda711a33a20a0 Mon Sep 17 00:00:00 2001 From: Patryk Twardosz Date: Thu, 25 Jul 2024 01:13:21 +0200 Subject: [PATCH 25/29] feat: Add server registered/unregistered events (#1386) * feat: Add server registered/unregistered events * Annotate new API with `@Beta` * Migrate from classes to records * Add null checks * Fix code style indent * Add links in documentation * Fix docs indent --------- Co-authored-by: powercas_gamer --- .../proxy/server/ServerRegisteredEvent.java | 31 +++++++++++++++++++ .../proxy/server/ServerUnregisteredEvent.java | 31 +++++++++++++++++++ .../proxy/server/ServerMap.java | 10 ++++++ 3 files changed, 72 insertions(+) create mode 100644 api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerRegisteredEvent.java create mode 100644 api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerUnregisteredEvent.java diff --git a/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerRegisteredEvent.java b/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerRegisteredEvent.java new file mode 100644 index 00000000..754492a6 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerRegisteredEvent.java @@ -0,0 +1,31 @@ +/* + * 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.proxy.server; + +import com.google.common.annotations.Beta; +import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.proxy.server.ServerInfo; +import org.jetbrains.annotations.NotNull; + +/** + * This event is fired by the proxy after a backend server is registered to the server map. + * Currently, it may occur when a server is registered dynamically at runtime or when a server is + * replaced due to configuration reload. + * + * @see com.velocitypowered.api.proxy.ProxyServer#registerServer(ServerInfo) + * + * @param registeredServer A {@link RegisteredServer} that has been registered. + * @since 3.3.0 + */ +@Beta +public record ServerRegisteredEvent(@NotNull RegisteredServer registeredServer) { + public ServerRegisteredEvent { + Preconditions.checkNotNull(registeredServer, "registeredServer"); + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerUnregisteredEvent.java b/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerUnregisteredEvent.java new file mode 100644 index 00000000..36b4023b --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerUnregisteredEvent.java @@ -0,0 +1,31 @@ +/* + * 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.proxy.server; + +import com.google.common.annotations.Beta; +import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.proxy.server.ServerInfo; +import org.jetbrains.annotations.NotNull; + +/** + * This event is fired by the proxy after a backend server is unregistered from the server map. + * Currently, it may occur when a server is unregistered dynamically at runtime + * or when a server is replaced due to configuration reload. + * + * @see com.velocitypowered.api.proxy.ProxyServer#unregisterServer(ServerInfo) + * + * @param unregisteredServer A {@link RegisteredServer} that has been unregistered. + * @since 3.3.0 + */ +@Beta +public record ServerUnregisteredEvent(@NotNull RegisteredServer unregisteredServer) { + public ServerUnregisteredEvent { + Preconditions.checkNotNull(unregisteredServer, "unregisteredServer"); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/ServerMap.java b/proxy/src/main/java/com/velocitypowered/proxy/server/ServerMap.java index d3fc5431..267b7874 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/ServerMap.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/ServerMap.java @@ -19,6 +19,8 @@ package com.velocitypowered.proxy.server; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.velocitypowered.api.event.proxy.server.ServerRegisteredEvent; +import com.velocitypowered.api.event.proxy.server.ServerUnregisteredEvent; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; import com.velocitypowered.proxy.VelocityServer; @@ -84,6 +86,10 @@ public class ServerMap { throw new IllegalArgumentException( "Server with name " + serverInfo.getName() + " already registered"); } else if (existing == null) { + if (server != null) { + server.getEventManager().fireAndForget(new ServerRegisteredEvent(rs)); + } + return rs; } else { return existing; @@ -107,5 +113,9 @@ public class ServerMap { "Trying to remove server %s with differing information", serverInfo.getName()); Preconditions.checkState(servers.remove(lowerName, rs), "Server with name %s replaced whilst unregistering", serverInfo.getName()); + + if (server != null) { + server.getEventManager().fireAndForget(new ServerUnregisteredEvent(rs)); + } } } From f6a47ae8b08818273a39b47f3e2072b1e5e9e470 Mon Sep 17 00:00:00 2001 From: powercas_gamer Date: Thu, 25 Jul 2024 01:25:41 +0200 Subject: [PATCH 26/29] [ci skip] update docs url (#1393) --- .../proxy/connection/backend/LoginSessionHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0fdb5fa2..612e9c25 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 @@ -208,7 +208,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { The connection to the remote server was unexpectedly closed. This is usually because the remote server does not have \ BungeeCord IP forwarding correctly enabled. - See https://velocitypowered.com/wiki/users/forwarding/ for instructions \ + See https://docs.papermc.io/velocity/player-information-forwarding for instructions \ on how to configure player info forwarding correctly.""")); } else { resultFuture.completeExceptionally( From 51468530a95ce7336f7756e22747a3066886d326 Mon Sep 17 00:00:00 2001 From: powercas_gamer Date: Fri, 26 Jul 2024 06:53:22 +0200 Subject: [PATCH 27/29] [ci skip] add adventure-key and text-minimessage javadoc links (#1394) --- api/build.gradle.kts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/build.gradle.kts b/api/build.gradle.kts index e7dd2bf4..2e524db9 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -67,7 +67,9 @@ tasks { "https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/", "https://docs.oracle.com/en/java/javase/17/docs/api/", "https://jd.advntr.dev/api/${libs.adventure.bom.get().version}/", - "https://javadoc.io/doc/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/" + "https://jd.advntr.dev/text-minimessage/${libs.adventure.bom.get().version}/", + "https://jd.advntr.dev/key/${libs.adventure.bom.get().version}/", + "https://javadoc.io/doc/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/", ) o.tags( From dd7b8b4d8f963e49a9d99b17c7d6f016fb5054fc Mon Sep 17 00:00:00 2001 From: Timon Date: Thu, 8 Aug 2024 16:47:18 +0200 Subject: [PATCH 28/29] Update 1.21 protocol to include 1.21.1 release (#1402) --- .../java/com/velocitypowered/api/network/ProtocolVersion.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java index e968b025..8bb0c98b 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -87,7 +87,7 @@ public enum ProtocolVersion implements Ordered { MINECRAFT_1_20_2(764, "1.20.2"), MINECRAFT_1_20_3(765, "1.20.3", "1.20.4"), MINECRAFT_1_20_5(766, "1.20.5", "1.20.6"), - MINECRAFT_1_21(767, "1.21"); + MINECRAFT_1_21(767, "1.21", "1.21.1"); private static final int SNAPSHOT_BIT = 30; From 00ed2284ecb4fb03bbb97d2b6a62c61ab15cde1a Mon Sep 17 00:00:00 2001 From: Max Lee Date: Sat, 10 Aug 2024 20:12:19 +0100 Subject: [PATCH 29/29] Use the default Minecraft port instead of custom one (#1277) --- .../proxy/config/VelocityConfiguration.java | 8 ++++---- proxy/src/main/resources/default-velocity.toml | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java index 11c65d52..9c2e1e82 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -60,7 +60,7 @@ public class VelocityConfiguration implements ProxyConfig { private static final Logger logger = LogManager.getLogger(VelocityConfiguration.class); @Expose - private String bind = "0.0.0.0:25577"; + private String bind = "0.0.0.0:25565"; @Expose private String motd = "A Velocity Server"; @Expose @@ -503,7 +503,7 @@ public class VelocityConfiguration implements ProxyConfig { final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough", PingPassthroughMode.DISABLED); - final String bind = config.getOrElse("bind", "0.0.0.0:25577"); + final String bind = config.getOrElse("bind", "0.0.0.0:25565"); final int maxPlayers = config.getIntOrElse("show-max-players", 500); final boolean onlineMode = config.getOrElse("online-mode", true); final boolean forceKeyAuthentication = config.getOrElse("force-key-authentication", true); @@ -830,7 +830,7 @@ public class VelocityConfiguration implements ProxyConfig { @Expose private boolean queryEnabled = false; @Expose - private int queryPort = 25577; + private int queryPort = 25565; @Expose private String queryMap = "Velocity"; @Expose @@ -849,7 +849,7 @@ public class VelocityConfiguration implements ProxyConfig { private Query(CommentedConfig config) { if (config != null) { this.queryEnabled = config.getOrElse("enabled", false); - this.queryPort = config.getIntOrElse("port", 25577); + this.queryPort = config.getIntOrElse("port", 25565); this.queryMap = config.getOrElse("map", "Velocity"); this.showPlugins = config.getOrElse("show-plugins", false); } diff --git a/proxy/src/main/resources/default-velocity.toml b/proxy/src/main/resources/default-velocity.toml index 3f4f6318..e402305c 100644 --- a/proxy/src/main/resources/default-velocity.toml +++ b/proxy/src/main/resources/default-velocity.toml @@ -1,8 +1,8 @@ # Config version. Do not change this config-version = "2.7" -# What port should the proxy be bound to? By default, we'll bind to all addresses on port 25577. -bind = "0.0.0.0:25577" +# What port should the proxy be bound to? By default, we'll bind to all addresses on port 25565. +bind = "0.0.0.0:25565" # What should be the MOTD? This gets displayed when the player adds your server to # their server list. Only MiniMessage format is accepted. @@ -150,7 +150,7 @@ accepts-transfers = false enabled = false # If query is enabled, on what port should the query protocol listen on? -port = 25577 +port = 25565 # This is the map name that is reported to the query services. map = "Velocity"