From 6224adf70a2f029b3d54e99cdb62eb6fc7b6d4e2 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Fri, 5 Jul 2024 22:01:31 -0400 Subject: [PATCH 1/8] 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 2c6f752f7..367219c63 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 2/8] 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 367219c63..0af477c42 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 3/8] 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 ea28d2c97..b047d1868 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 4/8] [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 aa6778fca..e7dd2bf45 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 6cf90a791..257fc9e64 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 5/8] 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 a3d0523dc..510c4d62a 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 6/8] 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 000000000..6e042af1c --- /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 3b108c6a7..05d6c2af0 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 000000000..c16777066 --- /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 f6249b897..50df5a8ab 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 09e76104f..517f119cf 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 a2d2205ef..0fdb5fa25 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 77290584d..ac02bcf76 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 3d955e903..7d232b09f 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 f2df78a7d..fed61693f 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 df6769ffd..11f6b5298 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 7/8] 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 000000000..96d1bb8e5 --- /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 14989b1a3..128d5c370 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 a19854eb9..74f0576c1 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 8/8] 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 fe553f76a..1affc13bc 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 d5764ef6a..c57271040 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.