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); 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/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/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" 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/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; 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(); 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 60c381b64..9c67b6a8b 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; @@ -144,8 +145,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { public boolean handle(ServerLoginSuccessPacket packet) { if (server.getConfiguration().getServerPlayerInfoForwardingMode(serverConn.getServerInfo().getName()) == PlayerInfoForwarding.MODERN && !informationForwarded) { - resultFuture.complete(ConnectionRequestResults.forDisconnect(MODERN_IP_FORWARDING_FAILURE, - serverConn.getServer())); + resultFuture.complete(ConnectionRequestResults.forDisconnect(MODERN_IP_FORWARDING_FAILURE, serverConn.getServer())); serverConn.disconnect(); return true; } @@ -156,12 +156,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 +167,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 91ab92022..9ed11540a 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; + }); } /** 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..0af477c42 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; @@ -86,35 +86,48 @@ 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())); - Set loadedPluginsById = new HashSet<>(); + Map loadedCandidates = new HashMap<>(); Map pluginContainers = new LinkedHashMap<>(); // Now load the plugins pluginLoad: for (PluginDescription candidate : sortedPlugins) { // 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 +138,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); } 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.