From b9b11665b9a3926bdbf45986e6e0f736ca0d01cd Mon Sep 17 00:00:00 2001 From: Riley Park Date: Mon, 22 Jan 2024 15:01:49 -0800 Subject: [PATCH] Refactor built-in commands to use Brigadier (#1208) * Refactor /velocity command to use Brigadier * Added default usage message and code cleanup * Reimplemented callback subcommand * Fixed execution of the callback subcommand when all permissions of the main command subcommands are denied * Migrated callback subcommand to its own command * Migrated server command * GList, Send and Server command cleanup --------- Co-authored-by: Adrian --- .../velocitypowered/proxy/VelocityServer.java | 6 +- .../proxy/adventure/ClickCallbackManager.java | 2 +- .../command/builtin/CallbackCommand.java | 58 +++ .../proxy/command/builtin/GlistCommand.java | 63 +-- .../proxy/command/builtin/SendCommand.java | 80 ++-- .../proxy/command/builtin/ServerCommand.java | 147 ++++--- .../command/builtin/VelocityCommand.java | 360 ++++++------------ .../backend/BackendPlaySessionHandler.java | 1 + 8 files changed, 325 insertions(+), 392 deletions(-) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/command/builtin/CallbackCommand.java diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 93216131c..5db97cbaa 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -37,6 +37,7 @@ import com.velocitypowered.api.util.Favicon; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.ProxyVersion; import com.velocitypowered.proxy.command.VelocityCommandManager; +import com.velocitypowered.proxy.command.builtin.CallbackCommand; import com.velocitypowered.proxy.command.builtin.GlistCommand; import com.velocitypowered.proxy.command.builtin.SendCommand; import com.velocitypowered.proxy.command.builtin.ServerCommand; @@ -223,8 +224,9 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { cm.logChannelInformation(); // Initialize commands first - commandManager.register("velocity", new VelocityCommand(this)); - commandManager.register("server", new ServerCommand(this)); + commandManager.register(VelocityCommand.create(this)); + commandManager.register(CallbackCommand.create()); + commandManager.register(ServerCommand.create(this)); commandManager.register("shutdown", ShutdownCommand.command(this), "end", "stop"); new GlistCommand(this).register(); 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 f8a27d1fd..de63b3553 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/adventure/ClickCallbackManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/adventure/ClickCallbackManager.java @@ -33,7 +33,7 @@ import org.checkerframework.checker.index.qual.NonNegative; public class ClickCallbackManager { public static final ClickCallbackManager INSTANCE = new ClickCallbackManager(); - static final String COMMAND = "/velocity callback "; + static final String COMMAND = "/velocity:callback "; private final Cache registrations = Caffeine.newBuilder() .expireAfter(new Expiry() { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/CallbackCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/CallbackCommand.java new file mode 100644 index 000000000..e85ec873f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/CallbackCommand.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.command.builtin; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.velocitypowered.api.command.BrigadierCommand; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.proxy.adventure.ClickCallbackManager; +import java.util.UUID; + +/** + * Callback Command. + */ +public final class CallbackCommand implements Command { + + @SuppressWarnings("checkstyle:MissingJavadocMethod") + public static BrigadierCommand create() { + final LiteralCommandNode node = BrigadierCommand + .literalArgumentBuilder("velocity:callback") + .then(BrigadierCommand.requiredArgumentBuilder("id", StringArgumentType.word()) + .executes(new CallbackCommand())) + .build(); + + return new BrigadierCommand(node); + } + + @Override + public int run(final CommandContext context) { + final String providedId = StringArgumentType.getString(context, "id"); + final UUID id; + try { + id = UUID.fromString(providedId); + } catch (final IllegalArgumentException ignored) { + return Command.SINGLE_SUCCESS; + } + + ClickCallbackManager.INSTANCE.runCallback(context.getSource(), id); + return Command.SINGLE_SUCCESS; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/GlistCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/GlistCommand.java index 0bfc0f074..2cf8c92d2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/GlistCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/GlistCommand.java @@ -20,12 +20,11 @@ package com.velocitypowered.proxy.command.builtin; import static com.mojang.brigadier.arguments.StringArgumentType.getString; import com.google.common.collect.ImmutableList; +import com.mojang.brigadier.Command; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; -import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.tree.ArgumentCommandNode; -import com.mojang.brigadier.tree.LiteralCommandNode; import com.velocitypowered.api.command.BrigadierCommand; import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.permission.Tristate; @@ -34,7 +33,6 @@ import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.server.RegisteredServer; import java.util.List; import java.util.Optional; -import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TranslatableComponent; @@ -57,20 +55,19 @@ public class GlistCommand { * Registers this command. */ public void register() { - LiteralCommandNode totalNode = LiteralArgumentBuilder - .literal("glist") + final LiteralArgumentBuilder rootNode = BrigadierCommand + .literalArgumentBuilder("glist") .requires(source -> source.getPermissionValue("velocity.command.glist") == Tristate.TRUE) - .executes(this::totalCount) - .build(); - ArgumentCommandNode serverNode = RequiredArgumentBuilder - .argument(SERVER_ARG, StringArgumentType.string()) + .executes(this::totalCount); + final ArgumentCommandNode serverNode = BrigadierCommand + .requiredArgumentBuilder(SERVER_ARG, StringArgumentType.string()) .suggests((context, builder) -> { - String argument = context.getArguments().containsKey(SERVER_ARG) + final String argument = context.getArguments().containsKey(SERVER_ARG) ? context.getArgument(SERVER_ARG, String.class) : ""; for (RegisteredServer server : server.getAllServers()) { - String serverName = server.getServerInfo().getName(); + final String serverName = server.getServerInfo().getName(); if (serverName.regionMatches(true, 0, argument, 0, argument.length())) { builder.suggest(serverName); } @@ -82,14 +79,14 @@ public class GlistCommand { }) .executes(this::serverCount) .build(); - totalNode.addChild(serverNode); - server.getCommandManager().register(new BrigadierCommand(totalNode)); + rootNode.then(serverNode); + server.getCommandManager().register(new BrigadierCommand(rootNode)); } private int totalCount(final CommandContext context) { final CommandSource source = context.getSource(); sendTotalProxyCount(source); - source.sendMessage(Identity.nil(), + source.sendMessage( Component.translatable("velocity.command.glist-view-all", NamedTextColor.YELLOW)); return 1; } @@ -98,38 +95,42 @@ public class GlistCommand { final CommandSource source = context.getSource(); final String serverName = getString(context, SERVER_ARG); if (serverName.equalsIgnoreCase("all")) { - for (RegisteredServer server : BuiltinCommandUtil.sortedServerList(server)) { + for (final RegisteredServer server : BuiltinCommandUtil.sortedServerList(server)) { sendServerPlayers(source, server, true); } sendTotalProxyCount(source); } else { - Optional registeredServer = server.getServer(serverName); - if (!registeredServer.isPresent()) { - source.sendMessage(Identity.nil(), - CommandMessages.SERVER_DOES_NOT_EXIST.args(Component.text(serverName))); + final Optional registeredServer = server.getServer(serverName); + if (registeredServer.isEmpty()) { + source.sendMessage( + CommandMessages.SERVER_DOES_NOT_EXIST + .arguments(Component.text(serverName))); return -1; } sendServerPlayers(source, registeredServer.get(), false); } - return 1; + return Command.SINGLE_SUCCESS; } private void sendTotalProxyCount(CommandSource target) { - int online = server.getPlayerCount(); - TranslatableComponent msg = online == 1 - ? Component.translatable("velocity.command.glist-player-singular") - : Component.translatable("velocity.command.glist-player-plural"); - target.sendMessage(msg.color(NamedTextColor.YELLOW) - .args(Component.text(Integer.toString(online), NamedTextColor.GREEN))); + final int online = server.getPlayerCount(); + final TranslatableComponent.Builder msg = Component.translatable() + .key(online == 1 + ? "velocity.command.glist-player-singular" + : "velocity.command.glist-player-plural" + ).color(NamedTextColor.YELLOW) + .arguments(Component.text(Integer.toString(online), NamedTextColor.GREEN)); + target.sendMessage(msg.build()); } - private void sendServerPlayers(CommandSource target, RegisteredServer server, boolean fromAll) { - List onServer = ImmutableList.copyOf(server.getPlayersConnected()); + private void sendServerPlayers(final CommandSource target, + final RegisteredServer server, final boolean fromAll) { + final List onServer = ImmutableList.copyOf(server.getPlayersConnected()); if (onServer.isEmpty() && fromAll) { return; } - TextComponent.Builder builder = Component.text() + final TextComponent.Builder builder = Component.text() .append(Component.text("[" + server.getServerInfo().getName() + "] ", NamedTextColor.DARK_AQUA)) .append(Component.text("(" + onServer.size() + ")", NamedTextColor.GRAY)) @@ -137,7 +138,7 @@ public class GlistCommand { .resetStyle(); for (int i = 0; i < onServer.size(); i++) { - Player player = onServer.get(i); + final Player player = onServer.get(i); builder.append(Component.text(player.getUsername())); if (i + 1 < onServer.size()) { @@ -145,6 +146,6 @@ public class GlistCommand { } } - target.sendMessage(Identity.nil(), builder.build()); + target.sendMessage(builder.build()); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/SendCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/SendCommand.java index a75a86412..626131daa 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/SendCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/SendCommand.java @@ -17,12 +17,12 @@ package com.velocitypowered.proxy.command.builtin; +import com.mojang.brigadier.Command; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.tree.ArgumentCommandNode; -import com.mojang.brigadier.tree.LiteralCommandNode; import com.velocitypowered.api.command.BrigadierCommand; import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.permission.Tristate; @@ -51,20 +51,19 @@ public class SendCommand { * Registers this command. */ public void register() { - LiteralCommandNode totalNode = LiteralArgumentBuilder - .literal("send") + final LiteralArgumentBuilder rootNode = BrigadierCommand + .literalArgumentBuilder("send") .requires(source -> source.getPermissionValue("velocity.command.send") == Tristate.TRUE) - .executes(this::usage) - .build(); - ArgumentCommandNode playerNode = RequiredArgumentBuilder - .argument("player", StringArgumentType.word()) + .executes(this::usage); + final RequiredArgumentBuilder playerNode = BrigadierCommand + .requiredArgumentBuilder(PLAYER_ARG, StringArgumentType.word()) .suggests((context, builder) -> { - String argument = context.getArguments().containsKey(PLAYER_ARG) + final String argument = context.getArguments().containsKey(PLAYER_ARG) ? context.getArgument(PLAYER_ARG, String.class) : ""; - for (Player player : server.getAllPlayers()) { - String playerName = player.getUsername(); + for (final Player player : server.getAllPlayers()) { + final String playerName = player.getUsername(); if (playerName.regionMatches(true, 0, argument, 0, argument.length())) { builder.suggest(playerName); } @@ -78,16 +77,15 @@ public class SendCommand { } return builder.buildFuture(); }) - .executes(this::usage) - .build(); - ArgumentCommandNode serverNode = RequiredArgumentBuilder - .argument("server", StringArgumentType.word()) + .executes(this::usage); + final ArgumentCommandNode serverNode = BrigadierCommand + .requiredArgumentBuilder(SERVER_ARG, StringArgumentType.word()) .suggests((context, builder) -> { - String argument = context.getArguments().containsKey(SERVER_ARG) + final String argument = context.getArguments().containsKey(SERVER_ARG) ? context.getArgument(SERVER_ARG, String.class) : ""; - for (RegisteredServer server : server.getAllServers()) { - String serverName = server.getServerInfo().getName(); + for (final RegisteredServer server : server.getAllServers()) { + final String serverName = server.getServerInfo().getName(); if (serverName.regionMatches(true, 0, argument, 0, argument.length())) { builder.suggest(server.getServerInfo().getName()); } @@ -96,66 +94,68 @@ public class SendCommand { }) .executes(this::send) .build(); - totalNode.addChild(playerNode); - playerNode.addChild(serverNode); - server.getCommandManager().register(new BrigadierCommand(totalNode)); + playerNode.then(serverNode); + rootNode.then(playerNode.build()); + server.getCommandManager().register(new BrigadierCommand(rootNode.build())); } - private int usage(CommandContext context) { + private int usage(final CommandContext context) { context.getSource().sendMessage( Component.translatable("velocity.command.send-usage", NamedTextColor.YELLOW) ); - return 1; + return Command.SINGLE_SUCCESS; } - private int send(CommandContext context) { - String serverName = context.getArgument(SERVER_ARG, String.class); - String player = context.getArgument(PLAYER_ARG, String.class); + private int send(final CommandContext context) { + final String serverName = context.getArgument(SERVER_ARG, String.class); + final String player = context.getArgument(PLAYER_ARG, String.class); - Optional maybeServer = server.getServer(serverName); + final Optional maybeServer = server.getServer(serverName); if (maybeServer.isEmpty()) { context.getSource().sendMessage( - CommandMessages.SERVER_DOES_NOT_EXIST.args(Component.text(serverName)) + CommandMessages.SERVER_DOES_NOT_EXIST.arguments(Component.text(serverName)) ); return 0; } - if (server.getPlayer(player).isEmpty() + final RegisteredServer targetServer = maybeServer.get(); + + final Optional maybePlayer = server.getPlayer(player); + if (maybePlayer.isEmpty() && !Objects.equals(player, "all") && !Objects.equals(player, "current")) { context.getSource().sendMessage( - CommandMessages.PLAYER_NOT_FOUND.args(Component.text(player)) + CommandMessages.PLAYER_NOT_FOUND.arguments(Component.text(player)) ); return 0; } if (Objects.equals(player, "all")) { - for (Player p : server.getAllPlayers()) { - p.createConnectionRequest(server.getServer(serverName).get()).fireAndForget(); + for (final Player p : server.getAllPlayers()) { + p.createConnectionRequest(targetServer).fireAndForget(); } - return 1; + return Command.SINGLE_SUCCESS; } if (Objects.equals(player, "current")) { - if (!(context.getSource() instanceof Player)) { + if (!(context.getSource() instanceof Player source)) { context.getSource().sendMessage(CommandMessages.PLAYERS_ONLY); return 0; } - Player source = (Player) context.getSource(); - Optional connectedServer = source.getCurrentServer(); + final Optional connectedServer = source.getCurrentServer(); if (connectedServer.isPresent()) { - for (Player p : connectedServer.get().getServer().getPlayersConnected()) { + for (final Player p : connectedServer.get().getServer().getPlayersConnected()) { p.createConnectionRequest(maybeServer.get()).fireAndForget(); } - return 1; + return Command.SINGLE_SUCCESS; } return 0; } - server.getPlayer(player).get().createConnectionRequest( - maybeServer.get()).fireAndForget(); - return 1; + // The player at this point must be present + maybePlayer.orElseThrow().createConnectionRequest(targetServer).fireAndForget(); + return Command.SINGLE_SUCCESS; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ServerCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ServerCommand.java index 1064f0794..4ec40070f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ServerCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ServerCommand.java @@ -19,9 +19,11 @@ package com.velocitypowered.proxy.command.builtin; import static net.kyori.adventure.text.event.HoverEvent.showText; -import com.google.common.collect.ImmutableList; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.velocitypowered.api.command.BrigadierCommand; import com.velocitypowered.api.command.CommandSource; -import com.velocitypowered.api.command.SimpleCommand; import com.velocitypowered.api.permission.Tristate; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; @@ -30,8 +32,6 @@ import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TranslatableComponent; @@ -41,50 +41,65 @@ import net.kyori.adventure.text.format.NamedTextColor; /** * Implements Velocity's {@code /server} command. */ -public class ServerCommand implements SimpleCommand { - +public final class ServerCommand { + private static final String SERVER_ARG = "server"; public static final int MAX_SERVERS_TO_LIST = 50; - private final ProxyServer server; - public ServerCommand(ProxyServer server) { - this.server = server; + @SuppressWarnings("checkstyle:MissingJavadocMethod") + public static BrigadierCommand create(final ProxyServer server) { + final LiteralCommandNode node = BrigadierCommand + .literalArgumentBuilder("server") + .requires(src -> src instanceof Player + && src.getPermissionValue("velocity.command.server") != Tristate.FALSE) + .executes(ctx -> { + final Player player = (Player) ctx.getSource(); + outputServerInformation(player, server); + return Command.SINGLE_SUCCESS; + }) + .then(BrigadierCommand.requiredArgumentBuilder(SERVER_ARG, StringArgumentType.word()) + .suggests((ctx, builder) -> { + final String argument = ctx.getArguments().containsKey(SERVER_ARG) + ? StringArgumentType.getString(ctx, SERVER_ARG) + : ""; + for (final RegisteredServer sv : server.getAllServers()) { + final String serverName = sv.getServerInfo().getName(); + if (serverName.regionMatches(true, 0, argument, 0, argument.length())) { + builder.suggest(serverName); + } + } + return builder.buildFuture(); + }) + .executes(ctx -> { + final Player player = (Player) ctx.getSource(); + // Trying to connect to a server. + final String serverName = StringArgumentType.getString(ctx, SERVER_ARG); + final Optional toConnect = server.getServer(serverName); + if (toConnect.isEmpty()) { + player.sendMessage(CommandMessages.SERVER_DOES_NOT_EXIST + .arguments(Component.text(serverName))); + return -1; + } + + player.createConnectionRequest(toConnect.get()).fireAndForget(); + return Command.SINGLE_SUCCESS; + }) + ).build(); + + return new BrigadierCommand(node); } - @Override - public void execute(final SimpleCommand.Invocation invocation) { - final CommandSource source = invocation.source(); - final String[] args = invocation.arguments(); - - if (!(source instanceof Player)) { - source.sendMessage(CommandMessages.PLAYERS_ONLY); - return; - } - - Player player = (Player) source; - if (args.length == 1) { - // Trying to connect to a server. - String serverName = args[0]; - Optional toConnect = server.getServer(serverName); - if (toConnect.isEmpty()) { - player.sendMessage(CommandMessages.SERVER_DOES_NOT_EXIST.args(Component.text(serverName))); - return; - } - - player.createConnectionRequest(toConnect.get()).fireAndForget(); - } else { - outputServerInformation(player); - } - } - - private void outputServerInformation(Player executor) { - String currentServer = executor.getCurrentServer().map(ServerConnection::getServerInfo) - .map(ServerInfo::getName).orElse(""); + private static void outputServerInformation(final Player executor, + final ProxyServer server) { + final String currentServer = executor.getCurrentServer() + .map(ServerConnection::getServerInfo) + .map(ServerInfo::getName) + .orElse(""); executor.sendMessage(Component.translatable( "velocity.command.server-current-server", NamedTextColor.YELLOW, Component.text(currentServer))); - List servers = BuiltinCommandUtil.sortedServerList(server); + final List servers = BuiltinCommandUtil.sortedServerList(server); if (servers.size() > MAX_SERVERS_TO_LIST) { executor.sendMessage(Component.translatable( "velocity.command.server-too-many", NamedTextColor.RED)); @@ -92,12 +107,12 @@ public class ServerCommand implements SimpleCommand { } // Assemble the list of servers as components - TextComponent.Builder serverListBuilder = Component.text() + final TextComponent.Builder serverListBuilder = Component.text() .append(Component.translatable("velocity.command.server-available", NamedTextColor.YELLOW)) - .append(Component.space()); + .appendSpace(); for (int i = 0; i < servers.size(); i++) { - RegisteredServer rs = servers.get(i); + final RegisteredServer rs = servers.get(i); serverListBuilder.append(formatServerComponent(currentServer, rs)); if (i != servers.size() - 1) { serverListBuilder.append(Component.text(", ", NamedTextColor.GRAY)); @@ -107,22 +122,22 @@ public class ServerCommand implements SimpleCommand { executor.sendMessage(serverListBuilder.build()); } - private TextComponent formatServerComponent(String currentPlayerServer, RegisteredServer server) { - ServerInfo serverInfo = server.getServerInfo(); - TextComponent serverTextComponent = Component.text(serverInfo.getName()); + private static TextComponent formatServerComponent(final String currentPlayerServer, + final RegisteredServer server) { + final ServerInfo serverInfo = server.getServerInfo(); + final TextComponent.Builder serverTextComponent = Component.text() + .content(serverInfo.getName()); - int connectedPlayers = server.getPlayersConnected().size(); - TranslatableComponent playersTextComponent; + final int connectedPlayers = server.getPlayersConnected().size(); + final TranslatableComponent.Builder playersTextComponent = Component.translatable(); if (connectedPlayers == 1) { - playersTextComponent = Component.translatable( - "velocity.command.server-tooltip-player-online"); + playersTextComponent.key("velocity.command.server-tooltip-player-online"); } else { - playersTextComponent = Component.translatable( - "velocity.command.server-tooltip-players-online"); + playersTextComponent.key("velocity.command.server-tooltip-players-online"); } - playersTextComponent = playersTextComponent.args(Component.text(connectedPlayers)); + playersTextComponent.arguments(Component.text(connectedPlayers)); if (serverInfo.getName().equals(currentPlayerServer)) { - serverTextComponent = serverTextComponent.color(NamedTextColor.GREEN) + serverTextComponent.color(NamedTextColor.GREEN) .hoverEvent( showText( Component.translatable("velocity.command.server-tooltip-current-server") @@ -130,7 +145,7 @@ public class ServerCommand implements SimpleCommand { .append(playersTextComponent)) ); } else { - serverTextComponent = serverTextComponent.color(NamedTextColor.GRAY) + serverTextComponent.color(NamedTextColor.GRAY) .clickEvent(ClickEvent.runCommand("/server " + serverInfo.getName())) .hoverEvent( showText( @@ -139,28 +154,6 @@ public class ServerCommand implements SimpleCommand { .append(playersTextComponent)) ); } - return serverTextComponent; - } - - @Override - public List suggest(final SimpleCommand.Invocation invocation) { - final String[] currentArgs = invocation.arguments(); - Stream possibilities = server.getAllServers().stream() - .map(rs -> rs.getServerInfo().getName()); - - if (currentArgs.length == 0) { - return possibilities.collect(Collectors.toList()); - } else if (currentArgs.length == 1) { - return possibilities - .filter(name -> name.regionMatches(true, 0, currentArgs[0], 0, currentArgs[0].length())) - .collect(Collectors.toList()); - } else { - return ImmutableList.of(); - } - } - - @Override - public boolean hasPermission(final SimpleCommand.Invocation invocation) { - return invocation.source().getPermissionValue("velocity.command.server") != Tristate.FALSE; + return serverTextComponent.build(); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java index 839537e89..bd240804c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java @@ -17,13 +17,16 @@ package com.velocitypowered.proxy.command.builtin; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.velocitypowered.api.command.BrigadierCommand; import com.velocitypowered.api.command.CommandSource; -import com.velocitypowered.api.command.SimpleCommand; import com.velocitypowered.api.permission.Tristate; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginDescription; @@ -31,7 +34,6 @@ import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.ProxyVersion; import com.velocitypowered.proxy.VelocityServer; -import com.velocitypowered.proxy.adventure.ClickCallbackManager; import com.velocitypowered.proxy.util.InformationUtils; import java.io.BufferedWriter; import java.io.IOException; @@ -44,14 +46,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.text.SimpleDateFormat; -import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.UUID; +import java.util.Set; import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.stream.Collectors; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; @@ -63,129 +63,71 @@ import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.checkerframework.checker.nullness.qual.NonNull; /** * Implements the {@code /velocity} command and friends. */ -public class VelocityCommand implements SimpleCommand { +public final class VelocityCommand { + private static final String USAGE = "/velocity <%s>"; - private interface SubCommand { - - void execute(final CommandSource source, final String @NonNull [] args); - - default List suggest(final CommandSource source, final String @NonNull [] currentArgs) { - return ImmutableList.of(); - } - - boolean hasPermission(final CommandSource source, final String @NonNull [] args); - } - - private final Map commands; - - /** - * Initializes the command object for /velocity. - * - * @param server the Velocity server - */ - public VelocityCommand(VelocityServer server) { - this.commands = ImmutableMap.builder() - .put("version", new Info(server)) - .put("plugins", new Plugins(server)) - .put("reload", new Reload(server)) - .put("dump", new Dump(server)) - .put("heap", new Heap()) - .put("callback", new Callback()) + @SuppressWarnings("checkstyle:MissingJavadocMethod") + public static BrigadierCommand create(final VelocityServer server) { + final LiteralCommandNode dump = BrigadierCommand.literalArgumentBuilder("dump") + .requires(source -> source.getPermissionValue("velocity.command.plugins") == Tristate.TRUE) + .executes(new Dump(server)) .build(); + final LiteralCommandNode heap = BrigadierCommand.literalArgumentBuilder("heap") + .requires(source -> source.getPermissionValue("velocity.command.heap") == Tristate.TRUE) + .executes(new Heap()) + .build(); + final LiteralCommandNode info = BrigadierCommand.literalArgumentBuilder("info") + .requires(source -> source.getPermissionValue("velocity.command.info") != Tristate.FALSE) + .executes(new Info(server)) + .build(); + final LiteralCommandNode plugins = BrigadierCommand + .literalArgumentBuilder("plugins") + .requires(source -> source.getPermissionValue("velocity.command.plugins") == Tristate.TRUE) + .executes(new Plugins(server)) + .build(); + final LiteralCommandNode reload = BrigadierCommand + .literalArgumentBuilder("reload") + .requires(source -> source.getPermissionValue("velocity.command.reload") == Tristate.TRUE) + .executes(new Reload(server)) + .build(); + + final List> commands = List + .of(dump, heap, info, plugins, reload); + return new BrigadierCommand( + commands.stream() + .reduce( + BrigadierCommand.literalArgumentBuilder("velocity") + .executes(ctx -> { + final CommandSource source = ctx.getSource(); + final String availableCommands = commands.stream() + .filter(e -> e.getRequirement().test(source)) + .map(LiteralCommandNode::getName) + .collect(Collectors.joining("|")); + final String commandText = USAGE.formatted(availableCommands); + source.sendMessage(Component.text(commandText, NamedTextColor.RED)); + return Command.SINGLE_SUCCESS; + }) + .requires(commands.stream() + .map(CommandNode::getRequirement) + .reduce(Predicate::or) + .orElseThrow()), + ArgumentBuilder::then, + ArgumentBuilder::then + ) + ); } - private void usage(CommandSource source) { - String availableCommands = commands.entrySet().stream() - .filter(e -> e.getValue().hasPermission(source, new String[0])) - .map(Map.Entry::getKey) - .collect(Collectors.joining("|")); - String commandText = "/velocity <" + availableCommands + ">"; - source.sendMessage(Component.text(commandText, NamedTextColor.RED)); - } - - @Override - public void execute(final SimpleCommand.Invocation invocation) { - final CommandSource source = invocation.source(); - final String[] args = invocation.arguments(); - - if (args.length == 0) { - usage(source); - return; - } - - SubCommand command = commands.get(args[0].toLowerCase(Locale.US)); - if (command == null) { - usage(source); - return; - } - @SuppressWarnings("nullness") - String[] actualArgs = Arrays.copyOfRange(args, 1, args.length); - command.execute(source, actualArgs); - } - - @Override - public List suggest(final SimpleCommand.Invocation invocation) { - final CommandSource source = invocation.source(); - final String[] currentArgs = invocation.arguments(); - - if (currentArgs.length == 0) { - return commands.entrySet().stream() - .filter(e -> e.getValue().hasPermission(source, new String[0])) - .map(Map.Entry::getKey) - .collect(ImmutableList.toImmutableList()); - } - - if (currentArgs.length == 1) { - return commands.entrySet().stream() - .filter(e -> e.getKey().regionMatches(true, 0, currentArgs[0], 0, - currentArgs[0].length())) - .filter(e -> e.getValue().hasPermission(source, new String[0])) - .map(Map.Entry::getKey) - .collect(ImmutableList.toImmutableList()); - } - - SubCommand command = commands.get(currentArgs[0].toLowerCase(Locale.US)); - if (command == null) { - return ImmutableList.of(); - } - @SuppressWarnings("nullness") - String[] actualArgs = Arrays.copyOfRange(currentArgs, 1, currentArgs.length); - return command.suggest(source, actualArgs); - } - - @Override - public boolean hasPermission(final SimpleCommand.Invocation invocation) { - final CommandSource source = invocation.source(); - final String[] args = invocation.arguments(); - - if (args.length == 0) { - return commands.values().stream().anyMatch(e -> e.hasPermission(source, args)); - } - SubCommand command = commands.get(args[0].toLowerCase(Locale.US)); - if (command == null) { - return true; - } - @SuppressWarnings("nullness") - String[] actualArgs = Arrays.copyOfRange(args, 1, args.length); - return command.hasPermission(source, actualArgs); - } - - private static class Reload implements SubCommand { + private record Reload(VelocityServer server) implements Command { private static final Logger logger = LogManager.getLogger(Reload.class); - private final VelocityServer server; - - private Reload(VelocityServer server) { - this.server = server; - } @Override - public void execute(CommandSource source, String @NonNull [] args) { + public int run(final CommandContext context) { + final CommandSource source = context.getSource(); try { if (server.reloadConfiguration()) { source.sendMessage(Component.translatable("velocity.command.reload-success", @@ -199,38 +141,28 @@ public class VelocityCommand implements SimpleCommand { source.sendMessage(Component.translatable("velocity.command.reload-failure", NamedTextColor.RED)); } - } - - @Override - public boolean hasPermission(final CommandSource source, final String @NonNull [] args) { - return source.getPermissionValue("velocity.command.reload") == Tristate.TRUE; + return Command.SINGLE_SUCCESS; } } - private static class Info implements SubCommand { + private record Info(ProxyServer server) implements Command { - private static final TextColor VELOCITY_COLOR = TextColor.fromHexString("#09add3"); - private final ProxyServer server; - - private Info(ProxyServer server) { - this.server = server; - } + private static final TextColor VELOCITY_COLOR = TextColor.color(0x09add3); @Override - public void execute(CommandSource source, String @NonNull [] args) { - if (args.length != 0) { - source.sendMessage(Component.text("/velocity version", NamedTextColor.RED)); - return; - } + public int run(final CommandContext context) { + final CommandSource source = context.getSource(); + final ProxyVersion version = server.getVersion(); - ProxyVersion version = server.getVersion(); - - Component velocity = Component.text().content(version.getName() + " ") + final Component velocity = Component.text() + .content(version.getName() + " ") .decoration(TextDecoration.BOLD, true) .color(VELOCITY_COLOR) - .append(Component.text(version.getVersion()).decoration(TextDecoration.BOLD, false)) + .append(Component.text() + .content(version.getVersion()) + .decoration(TextDecoration.BOLD, false)) .build(); - Component copyright = Component + final Component copyright = Component .translatable("velocity.command.version-copyright", Component.text(version.getVendor()), Component.text(version.getName())); @@ -238,14 +170,16 @@ public class VelocityCommand implements SimpleCommand { source.sendMessage(copyright); if (version.getName().equals("Velocity")) { - TextComponent embellishment = Component.text() - .append(Component.text().content("velocitypowered.com") + final TextComponent embellishment = Component.text() + .append(Component.text() + .content("velocitypowered.com") .color(NamedTextColor.GREEN) .clickEvent( ClickEvent.openUrl("https://velocitypowered.com")) .build()) .append(Component.text(" - ")) - .append(Component.text().content("GitHub") + .append(Component.text() + .content("GitHub") .color(NamedTextColor.GREEN) .decoration(TextDecoration.UNDERLINED, true) .clickEvent(ClickEvent.openUrl( @@ -254,59 +188,48 @@ public class VelocityCommand implements SimpleCommand { .build(); source.sendMessage(embellishment); } - } - - @Override - public boolean hasPermission(final CommandSource source, final String @NonNull [] args) { - return source.getPermissionValue("velocity.command.info") != Tristate.FALSE; + return Command.SINGLE_SUCCESS; } } - private static class Plugins implements SubCommand { - - private final ProxyServer server; - - private Plugins(ProxyServer server) { - this.server = server; - } + private record Plugins(ProxyServer server) implements Command { @Override - public void execute(CommandSource source, String @NonNull [] args) { - if (args.length != 0) { - source.sendMessage(Component.text("/velocity plugins", NamedTextColor.RED)); - return; - } + public int run(final CommandContext context) { + final CommandSource source = context.getSource(); - List plugins = ImmutableList.copyOf(server.getPluginManager().getPlugins()); - int pluginCount = plugins.size(); + final List plugins = List.copyOf(server.getPluginManager().getPlugins()); + final int pluginCount = plugins.size(); if (pluginCount == 0) { source.sendMessage(Component.translatable("velocity.command.no-plugins", NamedTextColor.YELLOW)); - return; + return Command.SINGLE_SUCCESS; } - TextComponent.Builder listBuilder = Component.text(); + final TextComponent.Builder listBuilder = Component.text(); for (int i = 0; i < pluginCount; i++) { - PluginContainer plugin = plugins.get(i); + final PluginContainer plugin = plugins.get(i); listBuilder.append(componentForPlugin(plugin.getDescription())); if (i + 1 < pluginCount) { listBuilder.append(Component.text(", ")); } } - TranslatableComponent.Builder output = Component.translatable() + final TranslatableComponent output = Component.translatable() .key("velocity.command.plugins-list") .color(NamedTextColor.YELLOW) - .args(listBuilder.build()); + .arguments(listBuilder.build()) + .build(); source.sendMessage(output); + return Command.SINGLE_SUCCESS; } private TextComponent componentForPlugin(PluginDescription description) { - String pluginInfo = description.getName().orElse(description.getId()) + final String pluginInfo = description.getName().orElse(description.getId()) + description.getVersion().map(v -> " " + v).orElse(""); - TextComponent.Builder hoverText = Component.text().content(pluginInfo); + final TextComponent.Builder hoverText = Component.text().content(pluginInfo); description.getUrl().ifPresent(url -> { hoverText.append(Component.newline()); @@ -333,61 +256,51 @@ public class VelocityCommand implements SimpleCommand { hoverText.append(Component.text(pdesc)); }); - return Component.text(description.getId(), NamedTextColor.GRAY) - .hoverEvent(HoverEvent.showText(hoverText.build())); - } - - @Override - public boolean hasPermission(final CommandSource source, final String @NonNull [] args) { - return source.getPermissionValue("velocity.command.plugins") == Tristate.TRUE; + return Component.text() + .content(description.getId()) + .color(NamedTextColor.GRAY) + .hoverEvent(HoverEvent.showText(hoverText.build())) + .build(); } } - private static class Dump implements SubCommand { - + private record Dump(ProxyServer server) implements Command { private static final Logger logger = LogManager.getLogger(Dump.class); - private final ProxyServer server; - private Dump(ProxyServer server) { - this.server = server; - } @Override - public void execute(CommandSource source, String @NonNull [] args) { - if (args.length != 0) { - source.sendMessage(Component.text("/velocity dump", NamedTextColor.RED)); - return; - } + public int run(final CommandContext context) { + final CommandSource source = context.getSource(); - Collection allServers = ImmutableSet.copyOf(server.getAllServers()); - JsonObject servers = new JsonObject(); - for (RegisteredServer iter : allServers) { + final Collection allServers = Set.copyOf(server.getAllServers()); + final JsonObject servers = new JsonObject(); + for (final RegisteredServer iter : allServers) { servers.add(iter.getServerInfo().getName(), InformationUtils.collectServerInfo(iter)); } - JsonArray connectOrder = new JsonArray(); - List attemptedConnectionOrder = ImmutableList.copyOf( + final JsonArray connectOrder = new JsonArray(); + final List attemptedConnectionOrder = List.copyOf( server.getConfiguration().getAttemptConnectionOrder()); - for (String s : attemptedConnectionOrder) { + for (final String s : attemptedConnectionOrder) { connectOrder.add(s); } - JsonObject proxyConfig = InformationUtils.collectProxyConfig(server.getConfiguration()); + final JsonObject proxyConfig = InformationUtils.collectProxyConfig(server.getConfiguration()); proxyConfig.add("servers", servers); proxyConfig.add("connectOrder", connectOrder); proxyConfig.add("forcedHosts", InformationUtils.collectForcedHosts(server.getConfiguration())); - JsonObject dump = new JsonObject(); + final JsonObject dump = new JsonObject(); dump.add("versionInfo", InformationUtils.collectProxyInfo(server.getVersion())); dump.add("platform", InformationUtils.collectEnvironmentInfo()); dump.add("config", proxyConfig); dump.add("plugins", InformationUtils.collectPluginInfo(server)); - Path dumpPath = Path.of("velocity-dump-" + final Path dumpPath = Path.of("velocity-dump-" + new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date()) + ".json"); - try (BufferedWriter bw = Files.newBufferedWriter( + try (final BufferedWriter bw = Files.newBufferedWriter( dumpPath, StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW)) { bw.write(InformationUtils.toHumanReadableString(dump)); @@ -397,32 +310,29 @@ public class VelocityCommand implements SimpleCommand { NamedTextColor.GREEN)); } catch (IOException e) { logger.error("Failed to complete dump command, " - + "the executor was interrupted: " + e.getMessage()); - e.printStackTrace(); + + "the executor was interrupted: " + e.getMessage(), e); source.sendMessage(Component.text( "We could not save the anonymized dump. Check the console for more details.", NamedTextColor.RED) ); } - } - - @Override - public boolean hasPermission(final CommandSource source, final String @NonNull [] args) { - return source.getPermissionValue("velocity.command.plugins") == Tristate.TRUE; + return Command.SINGLE_SUCCESS; } } /** * Heap SubCommand. */ - public static class Heap implements SubCommand { + public static final class Heap implements Command { private static final Logger logger = LogManager.getLogger(Heap.class); private MethodHandle heapGenerator; private Consumer heapConsumer; private final Path dir = Path.of("./dumps"); @Override - public void execute(CommandSource source, String @NonNull [] args) { + public int run(final CommandContext context) throws CommandSyntaxException { + final CommandSource source = context.getSource(); + try { if (Files.notExists(dir)) { Files.createDirectories(dir); @@ -480,39 +390,7 @@ public class VelocityCommand implements SimpleCommand { NamedTextColor.RED)); logger.error("Could not write heap", t); } - } - - @Override - public boolean hasPermission(CommandSource source, String @NonNull [] args) { - return source.getPermissionValue("velocity.command.heap") == Tristate.TRUE; - } - - } - - /** - * Callback SubCommand. - */ - public static class Callback implements SubCommand { - - @Override - public void execute(final CommandSource source, final String @NonNull [] args) { - if (args.length != 1) { - return; - } - - final UUID id; - try { - id = UUID.fromString(args[0]); - } catch (final IllegalArgumentException ignored) { - return; - } - - ClickCallbackManager.INSTANCE.runCallback(source, id); - } - - @Override - public boolean hasPermission(final CommandSource source, final String @NonNull [] args) { - return true; + return Command.SINGLE_SUCCESS; } } } 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 4c98ebaeb..0b224536a 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 @@ -297,6 +297,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { // Inject commands from the proxy. final CommandGraphInjector injector = server.getCommandManager().getInjector(); injector.inject(rootNode, serverConn.getPlayer()); + rootNode.removeChildByName("velocity:callback"); } server.getEventManager().fire(