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 81a7b8297..ce3452e40 100644 --- a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java +++ b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java @@ -35,10 +35,21 @@ public interface CommandManager { /** * Attempts to execute a command from the specified {@code cmdLine}. + * CommandExecuteEvent will not called * * @param source the command's source * @param cmdLine the command to run * @return true if the command was found and executed, false if it was not */ boolean execute(CommandSource source, String cmdLine); + + /** + * Attempts to execute a command from the specified {@code cmdLine}. + * + * @param source the command's source + * @param cmdLine the command to run + * @param callEvent will CommandExecuteEvent called or not + * @return true if the command was found and executed, false if it was not + */ + boolean execute(CommandSource source, String cmdLine, boolean callEvent); } diff --git a/api/src/main/java/com/velocitypowered/api/event/command/CommandExecuteEvent.java b/api/src/main/java/com/velocitypowered/api/event/command/CommandExecuteEvent.java new file mode 100644 index 000000000..f92ed6892 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/command/CommandExecuteEvent.java @@ -0,0 +1,143 @@ +package com.velocitypowered.api.event.command; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.event.ResultedEvent; +import com.velocitypowered.api.event.command.CommandExecuteEvent.CommandResult; +import java.util.Optional; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * This event is fired when a player types in a chat message. + */ +public final class CommandExecuteEvent implements ResultedEvent { + + private final CommandSource commandSource; + private final String command; + private CommandResult result; + + /** + * Constructs a PlayerChatEvent. + * @param commandSource the source executing the command + * @param command the command being executed without first slash + */ + public CommandExecuteEvent(CommandSource commandSource, String command) { + this.commandSource = Preconditions.checkNotNull(commandSource, "commandSource"); + this.command = Preconditions.checkNotNull(command, "command"); + this.result = CommandResult.allowed(); + } + + public CommandSource getCommandSource() { + return commandSource; + } + + /** + * Gets the original command being executed without first slash. + * @return the original command being executed + */ + public String getCommand() { + return command; + } + + @Override + public CommandResult getResult() { + return result; + } + + @Override + public void setResult(CommandResult result) { + this.result = Preconditions.checkNotNull(result, "result"); + } + + @Override + public String toString() { + return "PlayerCommmandEvent{" + + "commandSource=" + commandSource + + ", command=" + command + + ", result=" + result + + '}'; + } + + /** + * Represents the result of the {@link CommandExecuteEvent}. + */ + public static final class CommandResult implements Result { + + private static final CommandResult ALLOWED = new CommandResult(true, false,null); + private static final CommandResult DENIED = new CommandResult(false, false,null); + private static final CommandResult FORWARD_TO_SERVER = new CommandResult(false, true, null); + + private @Nullable String command; + private final boolean status; + private final boolean forward; + + private CommandResult(boolean status, boolean forward, @Nullable String command) { + this.status = status; + this.forward = forward; + this.command = command; + } + + public Optional getCommand() { + return Optional.ofNullable(command); + } + + public boolean isForwardToServer() { + return forward; + } + + @Override + public boolean isAllowed() { + return status; + } + + @Override + public String toString() { + return status ? "allowed" : "denied"; + } + + /** + * Allows the command to be sent, without modification. + * @return the allowed result + */ + public static CommandResult allowed() { + return ALLOWED; + } + + /** + * Prevents the command from being executed. + * @return the denied result + */ + public static CommandResult denied() { + return DENIED; + } + + /** + * Prevents the command from being executed, but forward command to server. + * @return the forward result + */ + public static CommandResult forwardToServer() { + return FORWARD_TO_SERVER; + } + + /** + * Prevents the command from being executed on proxy, but forward command to server. + * @param newCommand the command without first slash to use instead + * @return a result with a new command being forwarded to server + */ + public static CommandResult forwardToServer(@NonNull String newCommand) { + Preconditions.checkNotNull(newCommand, "newCommand"); + return new CommandResult(false, true, newCommand); + } + + /** + * Allows the command to be executed, but silently replaced old command with another. + * @param newCommand the command to use instead without first slash + * @return a result with a new command + */ + public static CommandResult command(@NonNull String newCommand) { + Preconditions.checkNotNull(newCommand, "newCommand"); + return new CommandResult(true, false, newCommand); + } + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/player/PlayerChatEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/PlayerChatEvent.java index 6b8b7bba1..ef77c96a4 100644 --- a/api/src/main/java/com/velocitypowered/api/event/player/PlayerChatEvent.java +++ b/api/src/main/java/com/velocitypowered/api/event/player/PlayerChatEvent.java @@ -70,6 +70,10 @@ public final class PlayerChatEvent implements ResultedEvent getMessage() { + return Optional.ofNullable(message); + } + @Override public boolean isAllowed() { return status; @@ -96,10 +100,6 @@ public final class PlayerChatEvent implements ResultedEvent getMessage() { - return Optional.ofNullable(message); - } - /** * Allows the message to be sent, but silently replaced with another. * @param message the message to use instead @@ -110,6 +110,4 @@ public final class PlayerChatEvent implements ResultedEvent commands = new HashMap<>(); + private final EventManager eventManager; + + public VelocityCommandManager(EventManager eventManager) { + this.eventManager = eventManager; + } @Override @Deprecated @@ -47,11 +57,39 @@ public class VelocityCommandManager implements CommandManager { this.commands.remove(alias.toLowerCase(Locale.ENGLISH)); } + /** + * Calls CommandExecuteEvent. + * @param source the command's source + * @param cmd the command + * @return CompletableFuture of event + */ + public CompletableFuture callCommandEvent(CommandSource source, String cmd) { + Preconditions.checkNotNull(source, "invoker"); + Preconditions.checkNotNull(cmd, "cmd"); + return eventManager.fire(new CommandExecuteEvent(source, cmd)); + } + @Override public boolean execute(CommandSource source, String cmdLine) { Preconditions.checkNotNull(source, "invoker"); Preconditions.checkNotNull(cmdLine, "cmdLine"); + return execute(source, cmdLine, false); + } + + @Override + public boolean execute(CommandSource source, String cmdLine, boolean callEvent) { + Preconditions.checkNotNull(source, "invoker"); + Preconditions.checkNotNull(cmdLine, "cmdLine"); + + if (callEvent) { + CommandExecuteEvent event = callCommandEvent(source, cmdLine).join(); + CommandResult commandResult = event.getResult(); + if (commandResult.isForwardToServer() || !commandResult.isAllowed()) { + return false; + } + cmdLine = commandResult.getCommand().orElse(event.getCommand()); + } String alias = cmdLine; String args = ""; int firstSpace = cmdLine.indexOf(' '); 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 35d149c24..f1bbaf1c8 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 @@ -4,6 +4,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.constructChannelsPacket; +import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; @@ -123,17 +124,32 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { String msg = packet.getMessage(); if (msg.startsWith("/")) { - try { - if (!server.getCommandManager().execute(player, msg.substring(1))) { - return false; - } - } catch (Exception e) { - logger.info("Exception occurred while running command for {}", player.getUsername(), - e); - player.sendMessage( - TextComponent.of("An error occurred while running this command.", TextColor.RED)); - return true; - } + + server.getCommandManager().callCommandEvent(player, msg.substring(1)) + .thenAcceptAsync(event -> { + CommandExecuteEvent.CommandResult commandResult = event.getResult(); + if (commandResult.isAllowed()) { + Optional eventCommand = event.getResult().getCommand(); + String command = eventCommand.orElse(event.getCommand()); + + if (commandResult.isForwardToServer()) { + smc.write(Chat.createServerbound(command)); + return; + } + + try { + if (!server.getCommandManager().execute(player, command)) { + smc.write(Chat.createServerbound(command)); + } + } catch (Exception e) { + logger.info("Exception occurred while running command for {}", player.getUsername(), + e); + player.sendMessage( + TextComponent.of("An error occurred while running this command.", + TextColor.RED)); + } + } + }); } else { PlayerChatEvent event = new PlayerChatEvent(player, msg); server.getEventManager().fire(event) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java index 4b9d38aee..0123db3e1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java @@ -91,7 +91,7 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons @Override protected void runCommand(String command) { try { - if (!this.server.getCommandManager().execute(this, command)) { + if (!this.server.getCommandManager().execute(this, command, true)) { sendMessage(TextComponent.of("Command not found.", TextColor.RED)); } } catch (Exception e) {