Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions api/src/main/java/com/velocitypowered/api/proxy/Player.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -532,4 +533,19 @@ default void closeDialog() {
* @sinceMinecraft 1.21
*/
void setServerLinks(@NotNull List<ServerLink> 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.
*
* <p>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.
*
* <p>This method is non-blocking; the command list is sent asynchronously.
*
* @return a future that completes once the command list has been sent
*/
CompletableFuture<Void> sendAvailableCommands();

@WouterGritter WouterGritter May 26, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too sure if we need to return a CompletableFuture here. This doesn't get returned anywhere else in the Player interface, but at the same time, when this is completed it does guarantee that all PlayerAvailableCommandsEvents have been fired and the packet is on its way to the player.

Could this be useful for plugins? Or should we just return void and rely on plugins listening for this even to figure out that the packet is (about to be) on it's way, as a plugin calling this method, is most likely also manipulating this event?

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -358,26 +354,8 @@ public boolean handle(RemovePlayerInfoPacket packet) {

@Override
public boolean handle(AvailableCommandsPacket commands) {
RootCommandNode<CommandSource> rootNode = commands.getRootNode();
if (server.getConfiguration().isAnnounceProxyCommands()) {
// Inject commands from the proxy.
final CommandGraphInjector<CommandSource> 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -72,6 +74,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
private final Map<Long, Long> pendingPings = new HashMap<>();
private @MonotonicNonNull Integer entityId;
private volatile @Nullable RootCommandNode<CommandSource> backendCommandsNode;

/**
* Initializes a new server connection.
Expand Down Expand Up @@ -372,4 +375,12 @@ public void setConnectionPhase(BackendConnectionPhase connectionPhase) {
public boolean hasCompletedJoin() {
return hasCompletedJoin;
}

public @Nullable RootCommandNode<CommandSource> getBackendCommandsNode() {
return backendCommandsNode;
}

public void setBackendCommandsNode(@Nullable RootCommandNode<CommandSource> backendCommandsNode) {
this.backendCommandsNode = backendCommandsNode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -685,6 +691,52 @@ public void resetInFlightConnection() {
connectionInFlight = null;
}

@Override
public CompletableFuture<Void> 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<Void> sendAvailableCommands(@Nullable VelocityServerConnection conn) {
RootCommandNode<CommandSource> workingNode = new RootCommandNode<>();
if (conn != null) {
RootCommandNode<CommandSource> backendNode = conn.getBackendCommandsNode();
if (backendNode != null) {
for (CommandNode<CommandSource> child : backendNode.getChildren()) {
workingNode.addChild(child);
}
}
}

if (server.getConfiguration().isAnnounceProxyCommands()) {
// Inject commands from the proxy.
CommandGraphInjector<CommandSource> 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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ public class AvailableCommandsPacket implements MinecraftPacket {

private @MonotonicNonNull RootCommandNode<CommandSource> rootNode;

public AvailableCommandsPacket() {
}

public AvailableCommandsPacket(RootCommandNode<CommandSource> rootNode) {
this.rootNode = rootNode;
}

/**
* Returns the root node.
*
Expand Down