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 057b8a2398..32423e94d5 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -28,6 +28,7 @@ import java.util.Locale; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.function.UnaryOperator; import net.kyori.adventure.dialog.DialogLike; import net.kyori.adventure.identity.Identified; @@ -532,4 +533,19 @@ default void closeDialog() { * @sinceMinecraft 1.21 */ void setServerLinks(@NotNull List links); + + /** + * Rebuilds and resends this player's command list, combining the commands offered by the + * player's current server with the proxy's own commands. + * + *

This is useful when the set of available commands has changed and the client's command + * list needs to be refreshed, for example after a command is registered or unregistered at + * runtime. The {@link com.velocitypowered.api.event.command.PlayerAvailableCommandsEvent} is + * fired before the updated list is sent, giving listeners a final chance to modify it. + * + *

This method is non-blocking; the command list is sent asynchronously. + * + * @return a future that completes once the command list has been sent + */ + CompletableFuture sendAvailableCommands(); } \ No newline at end of file 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 dfe230e318..b5a928eac8 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 @@ -21,9 +21,6 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; -import com.mojang.brigadier.tree.RootCommandNode; -import com.velocitypowered.api.command.CommandSource; -import com.velocitypowered.api.event.command.PlayerAvailableCommandsEvent; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.connection.PreTransferEvent; import com.velocitypowered.api.event.player.CookieRequestEvent; @@ -36,7 +33,6 @@ import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.VelocityServer; -import com.velocitypowered.proxy.command.CommandGraphInjector; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; @@ -358,26 +354,8 @@ public boolean handle(RemovePlayerInfoPacket packet) { @Override public boolean handle(AvailableCommandsPacket commands) { - RootCommandNode rootNode = commands.getRootNode(); - if (server.getConfiguration().isAnnounceProxyCommands()) { - // Inject commands from the proxy. - final CommandGraphInjector injector = server.getCommandManager().getInjector(); - injector.inject(rootNode, serverConn.getPlayer()); - - // In 1.21.6 a confirmation prompt was added when executing a command via `run_command` click - // action if the command is unknown. To prevent this prompt we have to send the command. - if (this.playerConnection.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_6)) { - rootNode.removeChildByName("velocity:callback"); - } - } - - server.getEventManager().fire( - new PlayerAvailableCommandsEvent(serverConn.getPlayer(), rootNode)) - .thenAcceptAsync(event -> playerConnection.write(commands), playerConnection.eventLoop()) - .exceptionally((ex) -> { - logger.error("Exception while handling available commands for {}", playerConnection, ex); - return null; - }); + serverConn.setBackendCommandsNode(commands.getRootNode()); + serverConn.getPlayer().sendAvailableCommands(serverConn); return true; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java index c40a7aaac6..ff7e34329e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java @@ -22,6 +22,8 @@ import static java.util.Objects.requireNonNull; import com.google.common.base.Preconditions; +import com.mojang.brigadier.tree.RootCommandNode; +import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.network.HandshakeIntent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.ServerConnection; @@ -72,6 +74,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN; private final Map pendingPings = new HashMap<>(); private @MonotonicNonNull Integer entityId; + private volatile @Nullable RootCommandNode backendCommandsNode; /** * Initializes a new server connection. @@ -372,4 +375,12 @@ public void setConnectionPhase(BackendConnectionPhase connectionPhase) { public boolean hasCompletedJoin() { return hasCompletedJoin; } + + public @Nullable RootCommandNode getBackendCommandsNode() { + return backendCommandsNode; + } + + public void setBackendCommandsNode(@Nullable RootCommandNode backendCommandsNode) { + this.backendCommandsNode = backendCommandsNode; + } } 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 47d59a7186..9c92ee074c 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 @@ -24,6 +24,10 @@ import com.google.common.base.Preconditions; import com.google.gson.JsonObject; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.RootCommandNode; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.event.command.PlayerAvailableCommandsEvent; import com.velocitypowered.api.event.connection.DisconnectEvent; import com.velocitypowered.api.event.connection.DisconnectEvent.LoginStatus; import com.velocitypowered.api.event.connection.PreTransferEvent; @@ -59,6 +63,7 @@ import com.velocitypowered.api.util.ServerLink; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation; +import com.velocitypowered.proxy.command.CommandGraphInjector; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; @@ -71,6 +76,7 @@ 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.AvailableCommandsPacket; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; @@ -685,6 +691,52 @@ public void resetInFlightConnection() { connectionInFlight = null; } + @Override + public CompletableFuture sendAvailableCommands() { + return sendAvailableCommands(this.connectedServer); + } + + /** + * Rebuilds and resends this player's command list, combining the commands offered by the given + * {@link VelocityServerConnection}. This may be {@code null}, to not inject any backend server commands. + * + * @param conn the server connection to grab the {@link VelocityServerConnection#getBackendCommandsNode()} from, + * or {@code null} to not inject any backend server commands + * @return a future that completes after the {@link AvailableCommandsPacket} packet has been sent to the player + */ + public CompletableFuture sendAvailableCommands(@Nullable VelocityServerConnection conn) { + RootCommandNode workingNode = new RootCommandNode<>(); + if (conn != null) { + RootCommandNode backendNode = conn.getBackendCommandsNode(); + if (backendNode != null) { + for (CommandNode child : backendNode.getChildren()) { + workingNode.addChild(child); + } + } + } + + if (server.getConfiguration().isAnnounceProxyCommands()) { + // Inject commands from the proxy. + CommandGraphInjector injector = server.getCommandManager().getInjector(); + injector.inject(workingNode, this); + + // In 1.21.6 a confirmation prompt was added when executing a command via `run_command` click + // action if the command is unknown. To prevent this prompt we have to send the command. + if (this.connection.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_6)) { + workingNode.removeChildByName("velocity:callback"); + } + } + + AvailableCommandsPacket packet = new AvailableCommandsPacket(workingNode); + return server.getEventManager() + .fire(new PlayerAvailableCommandsEvent(this, workingNode)) + .thenAcceptAsync(event -> connection.write(packet), connection.eventLoop()) + .exceptionally(ex -> { + logger.error("Exception while sending available commands to {}", this, ex); + return null; + }); + } + /** * Handles unexpected disconnects. * diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommandsPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommandsPacket.java index 6653d7633c..3aea7ba3b6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommandsPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/AvailableCommandsPacket.java @@ -70,6 +70,13 @@ public class AvailableCommandsPacket implements MinecraftPacket { private @MonotonicNonNull RootCommandNode rootNode; + public AvailableCommandsPacket() { + } + + public AvailableCommandsPacket(RootCommandNode rootNode) { + this.rootNode = rootNode; + } + /** * Returns the root node. *