From 2888bf161a77adcfab9135f3fb8badc773174893 Mon Sep 17 00:00:00 2001 From: Wouter Gritter Date: Wed, 29 Apr 2026 16:33:43 +0200 Subject: [PATCH 1/2] Only add/inject Velocity's `velocity:callback` command to the AvailableCommandsPacket when needed (when one or more click callback has been registered through Adventure's API). Velocity does not register any click-callbacks itself; callbacks will only be registered if plugins make use of the feature (i.e. if no plugins use this feature, we never send this command to the player, this avoids clutter on command tabcompletion) --- .../velocitypowered/proxy/VelocityServer.java | 13 +++++++++ .../proxy/adventure/ClickCallbackManager.java | 29 +++++++++++++++++++ .../connection/client/ConnectedPlayer.java | 8 +++-- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index cc9331e6c6..fb6dd4beba 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -62,6 +62,7 @@ import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.ProxyVersion; import com.velocitypowered.api.util.ServerLink; +import com.velocitypowered.proxy.adventure.ClickCallbackManager; import com.velocitypowered.proxy.command.VelocityCommandManager; import com.velocitypowered.proxy.command.builtin.BuiltinCommandDefinition; import com.velocitypowered.proxy.command.builtin.CallbackCommand; @@ -438,6 +439,18 @@ void start() { registerCommands(); + // Re-send the available commands to all online players once a click-callback has been registered. + // Vanilla Velocity does not register any click-callbacks, only plugins may do so via the Adventure API. + // If no plugins are making use of this feature, we can omit the /velocity:callback (ClickCallbackManager#COMMAND_LABEL) + // from the available commands, as it only adds clutter to command completion suggestions. + // ConnectedPlayer#sendAvailableCommands will include this callback command in the command set if a click-listener + // has been registered at least once. + ClickCallbackManager.INSTANCE.setOnFirstRegistration(() -> { + for (ConnectedPlayer player : getAllPlayers()) { + player.sendAvailableCommands(); + } + }); + LOGGER.info("Loading localizations..."); translationRegistryManager.registerTranslations(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/adventure/ClickCallbackManager.java b/proxy/src/main/java/com/velocitypowered/proxy/adventure/ClickCallbackManager.java index 5d83da7c6c..ebbb3bcfdb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/adventure/ClickCallbackManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/adventure/ClickCallbackManager.java @@ -22,6 +22,7 @@ import com.github.benmanes.caffeine.cache.Expiry; import com.github.benmanes.caffeine.cache.Scheduler; import java.util.UUID; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.event.ClickCallback; @@ -39,6 +40,10 @@ public final class ClickCallbackManager { static final String COMMAND = "/" + COMMAND_LABEL + " "; + private final AtomicBoolean hadRegistrations = new AtomicBoolean(false); + + private Runnable onFirstRegistration = () -> {}; + private final Cache registrations = Caffeine.newBuilder() .expireAfter(new Expiry() { @Override @@ -68,6 +73,24 @@ public long expireAfterRead(@NotNull UUID key, @NotNull RegisteredCallback value private ClickCallbackManager() { } + /** + * Sets a listener that is invoked the first time a callback is registered. + * + * @param listener the listener to invoke on the first registration + */ + public void setOnFirstRegistration(Runnable listener) { + this.onFirstRegistration = listener; + } + + /** + * Returns whether any callback has ever been registered. + * + * @return {@code true} if at least one callback has been registered, {@code false} otherwise + */ + public boolean hasHadRegistrations() { + return hadRegistrations.get(); + } + /** * Run a callback. * @@ -97,6 +120,12 @@ public UUID register(ClickCallback callback, UUID id = UUID.randomUUID(); RegisteredCallback registration = new RegisteredCallback(options.lifetime(), options.uses(), callback); this.registrations.put(id, registration); + + boolean alreadyHadRegistrations = hadRegistrations.getAndSet(true); + if (!alreadyHadRegistrations) { + onFirstRegistration.run(); + } + return id; } } 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 43f77447a6..92e956ae28 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 @@ -889,9 +889,11 @@ public CompletableFuture sendAvailableCommands(@Nullable VelocityServerCon 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)) { + // Omit the click-callback command from the client's command tree unless: + // - the client is 1.21.6+ (needs it to suppress the unknown-command confirmation prompt), AND + // - there are actually pending callbacks to execute + if (this.connection.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_6) + || !ClickCallbackManager.INSTANCE.hasHadRegistrations()) { workingNode.removeChildByName(ClickCallbackManager.COMMAND_LABEL); } } From ec8a601a671d4abae10d3baaac07579646fa9cbe Mon Sep 17 00:00:00 2001 From: Wouter Gritter Date: Tue, 12 May 2026 16:49:58 +0200 Subject: [PATCH 2/2] Minor fixes --- .../com/velocitypowered/proxy/VelocityServer.java | 5 ++++- .../proxy/adventure/ClickCallbackManager.java | 12 ++++++++++-- .../proxy/connection/client/ConnectedPlayer.java | 3 ++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index fb6dd4beba..f5f04fde19 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -86,6 +86,7 @@ import com.velocitypowered.proxy.plugin.loader.VelocityPluginDescription; import com.velocitypowered.proxy.plugin.virtual.VelocityVirtualPlugin; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.util.FaviconSerializer; import com.velocitypowered.proxy.protocol.util.GameProfileSerializer; import com.velocitypowered.proxy.scheduler.VelocityScheduler; @@ -447,7 +448,9 @@ void start() { // has been registered at least once. ClickCallbackManager.INSTANCE.setOnFirstRegistration(() -> { for (ConnectedPlayer player : getAllPlayers()) { - player.sendAvailableCommands(); + if (player.getConnection().getState() == StateRegistry.PLAY) { + player.sendAvailableCommands(); + } } }); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/adventure/ClickCallbackManager.java b/proxy/src/main/java/com/velocitypowered/proxy/adventure/ClickCallbackManager.java index ebbb3bcfdb..2866b44b0f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/adventure/ClickCallbackManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/adventure/ClickCallbackManager.java @@ -27,6 +27,7 @@ import net.kyori.adventure.audience.Audience; import net.kyori.adventure.text.event.ClickCallback; import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.jetbrains.annotations.NotNull; /** @@ -42,7 +43,7 @@ public final class ClickCallbackManager { private final AtomicBoolean hadRegistrations = new AtomicBoolean(false); - private Runnable onFirstRegistration = () -> {}; + private volatile @MonotonicNonNull Runnable onFirstRegistration; private final Cache registrations = Caffeine.newBuilder() .expireAfter(new Expiry() { @@ -77,8 +78,12 @@ private ClickCallbackManager() { * Sets a listener that is invoked the first time a callback is registered. * * @param listener the listener to invoke on the first registration + * @throws IllegalStateException if a listener has already been set */ public void setOnFirstRegistration(Runnable listener) { + if (this.onFirstRegistration != null) { + throw new IllegalStateException("A first-registration listener has already been set"); + } this.onFirstRegistration = listener; } @@ -123,7 +128,10 @@ public UUID register(ClickCallback callback, boolean alreadyHadRegistrations = hadRegistrations.getAndSet(true); if (!alreadyHadRegistrations) { - onFirstRegistration.run(); + Runnable listener = this.onFirstRegistration; + if (listener != null) { + listener.run(); + } } return id; 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 92e956ae28..82d2718174 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 @@ -891,7 +891,8 @@ public CompletableFuture sendAvailableCommands(@Nullable VelocityServerCon // Omit the click-callback command from the client's command tree unless: // - the client is 1.21.6+ (needs it to suppress the unknown-command confirmation prompt), AND - // - there are actually pending callbacks to execute + // - at least one callback has been registered since proxy startup (i.e. some plugin is + // using the click-callback feature). if (this.connection.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_6) || !ClickCallbackManager.INSTANCE.hasHadRegistrations()) { workingNode.removeChildByName(ClickCallbackManager.COMMAND_LABEL);