diff --git a/paper-api/src/main/java/io/papermc/paper/command/brigadier/CommandSourceStack.java b/paper-api/src/main/java/io/papermc/paper/command/brigadier/CommandSourceStack.java index 6289970005e2..da840ef3e570 100644 --- a/paper-api/src/main/java/io/papermc/paper/command/brigadier/CommandSourceStack.java +++ b/paper-api/src/main/java/io/papermc/paper/command/brigadier/CommandSourceStack.java @@ -1,8 +1,14 @@ package io.papermc.paper.command.brigadier; +import com.google.common.base.Preconditions; import com.mojang.brigadier.RedirectModifier; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.tree.CommandNode; +import net.kyori.adventure.pointer.Pointered; +import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; +import org.bukkit.GameRules; import org.bukkit.Location; import org.bukkit.command.CommandSender; import org.bukkit.entity.Entity; @@ -81,4 +87,123 @@ public interface CommandSourceStack { * @see com.mojang.brigadier.builder.ArgumentBuilder#fork(CommandNode, RedirectModifier) */ CommandSourceStack withExecutor(Entity executor); + + /** + * Sends a system message to the {@link #getExecutor()} if it is a {@link Player}, + * otherwise sends a system message to the {@link #getSender()}. + * + * @param message the message to send + * @throws IllegalArgumentException if the message is null + */ + void sendReply(ComponentLike message); + + /** + * Sends a system message with the MiniMessage format to the {@link #getExecutor()} + * if it is a {@link Player}, otherwise sends it to the {@link #getSender()}. + * + *
See MiniMessage docs and + * MiniMessage Placeholders docs + * for more information on the format.
+ * + *This method provides the target of the system message as a {@link net.kyori.adventure.pointer.Pointered} + * to the MiniMessage {@link MiniMessage#deserialize(String, Pointered, TagResolver...)} method.
+ * + * @param message the MiniMessage message to send + * @param resolvers resolvers to use + * @throws IllegalArgumentException if the message is null + */ + default void sendRichReply(final String message, final TagResolver... resolvers) { + Preconditions.checkArgument(message != null, "message cannot be null."); + this.sendReply(MiniMessage.miniMessage().deserialize( + message, + this.getExecutor() instanceof final Player player ? player : this.getSender(), + resolvers + )); + } + + /** + * Sends a system message to the {@link #getSender()}, admins, and console indicating successful command execution + * according to vanilla semantics. + * + *This currently includes checking for environments with suppressed output, + * {@link GameRules#SEND_COMMAND_FEEDBACK}, and {@link GameRules#LOG_ADMIN_COMMANDS}.
+ * + * @param message the message to send + * @param allowInformingAdmins whether admins and console may be informed of this success + * @throws IllegalArgumentException if the message is null + */ + void sendSuccess(ComponentLike message, boolean allowInformingAdmins); + + /** + * Sends a system message to the {@link #getSender()}, admins, and console indicating successful command execution + * according to vanilla semantics. This method informs admins and the console of this success. + * + *This currently includes checking for environments with suppressed output, + * {@link GameRules#SEND_COMMAND_FEEDBACK}, and {@link GameRules#LOG_ADMIN_COMMANDS}.
+ * + * @param message the message to send + * @see #sendSuccess(ComponentLike, boolean) to disable if admins and console should be informed + * @throws IllegalArgumentException if the message is null + */ + default void sendSuccess(final ComponentLike message) { + this.sendSuccess(message, true); + } + + /** + * Sends a system message with the MiniMessage format to the {@link #getSender()}, admins, and console indicating + * successful command execution according to vanilla semantics. This method informs admins and the console of + * this success. + * + *This currently includes checking for environments with suppressed output, + * {@link GameRules#SEND_COMMAND_FEEDBACK}, and {@link GameRules#LOG_ADMIN_COMMANDS}.
+ * + *See MiniMessage docs and + * MiniMessage Placeholders docs + * for more information on the format.
+ * + *This method provides the {@link CommandSender} of this command as a {@link net.kyori.adventure.pointer.Pointered} + * to the MiniMessage {@link MiniMessage#deserialize(String, Pointered, TagResolver...)} method.
+ * + * @param message the MiniMessage message to send + * @param resolvers resolvers to use + * @see #sendSuccess(ComponentLike, boolean) to disable if admins and console should be informed + * @throws IllegalArgumentException if the message is null + */ + default void sendRichSuccess(final String message, final TagResolver... resolvers) { + Preconditions.checkArgument(message != null, "message cannot be null."); + this.sendSuccess(MiniMessage.miniMessage().deserialize(message, this.getSender(), resolvers), true); + } + + /** + * Sends a system message indicating a failed command execution to the {@link #getSender()}. + * Does not apply red styling to the message as vanilla does to allow for custom failure message styling. + * + *Respects vanilla semantics for accepting failure output and suppressed output environments.
+ * + * @param message the message to send + * @throws IllegalArgumentException if the message is null + */ + void sendFailure(ComponentLike message); + + /** + * Sends a system message with the MiniMessage format indicating a failed command execution to the{@link #getSender()}. + * Does not apply red styling to the message as vanilla does to allow for custom failure message styling. + * + *Respects vanilla semantics for accepting failure output and suppressed output environments.
+ * + *See MiniMessage docs and + * MiniMessage Placeholders docs + * for more information on the format.
+ * + *This method provides the {@link CommandSender} of this command as a {@link net.kyori.adventure.pointer.Pointered} + * to the MiniMessage {@link MiniMessage#deserialize(String, Pointered, TagResolver...)} method.
+ * + * @param message the MiniMessage message to send + * @param resolvers resolvers to use + * @throws IllegalArgumentException if the message is null + */ + default void sendRichFailure(final String message, final TagResolver... resolvers) { + Preconditions.checkArgument(message != null, "message cannot be null."); + this.sendFailure(MiniMessage.miniMessage().deserialize(message, this.getSender(), resolvers)); + } } diff --git a/paper-server/src/main/java/io/papermc/paper/command/brigadier/PaperCommandSourceStack.java b/paper-server/src/main/java/io/papermc/paper/command/brigadier/PaperCommandSourceStack.java index 174a5b6a8b3b..8f311a8081fa 100644 --- a/paper-server/src/main/java/io/papermc/paper/command/brigadier/PaperCommandSourceStack.java +++ b/paper-server/src/main/java/io/papermc/paper/command/brigadier/PaperCommandSourceStack.java @@ -2,6 +2,8 @@ import com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource; import com.google.common.base.Preconditions; +import io.papermc.paper.adventure.PaperAdventure; +import net.kyori.adventure.text.ComponentLike; import com.mojang.brigadier.exceptions.CommandSyntaxException; import net.minecraft.world.level.Level; import net.minecraft.world.phys.Vec2; @@ -49,6 +51,24 @@ default CommandSourceStack withExecutor(Entity executor) { return this.getHandle().withEntity(((CraftEntity) executor).getHandle()); } + @Override + default void sendReply(final ComponentLike message) { + Preconditions.checkArgument(message != null, "message cannot be null."); + this.getHandle().sendSystemMessage(PaperAdventure.asVanilla(message.asComponent())); + } + + @Override + default void sendSuccess(final ComponentLike message, final boolean allowInformingAdmins) { + Preconditions.checkArgument(message != null, "message cannot be null."); + this.getHandle().sendSuccess(() -> PaperAdventure.asVanilla(message.asComponent()), allowInformingAdmins); + } + + @Override + default void sendFailure(final ComponentLike message) { + Preconditions.checkArgument(message != null, "message cannot be null."); + this.getHandle().sendFailure(PaperAdventure.asVanilla(message.asComponent()), false); + } + @Override default Player getPlayerOrThrow() throws CommandSyntaxException { return this.getHandle().getPlayerOrException().getBukkitEntity(); diff --git a/test-plugin/src/main/java/io/papermc/testplugin/brigtests/Registration.java b/test-plugin/src/main/java/io/papermc/testplugin/brigtests/Registration.java index 01837b2088ae..ce93c61e68f3 100644 --- a/test-plugin/src/main/java/io/papermc/testplugin/brigtests/Registration.java +++ b/test-plugin/src/main/java/io/papermc/testplugin/brigtests/Registration.java @@ -1,6 +1,8 @@ package io.papermc.testplugin.brigtests; import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; import io.papermc.paper.command.brigadier.BasicCommand; import io.papermc.paper.command.brigadier.CommandSourceStack; import io.papermc.paper.command.brigadier.Commands; @@ -19,6 +21,8 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; import org.bukkit.Material; import org.bukkit.command.CommandSender; import org.bukkit.command.defaults.BukkitCommand; @@ -45,7 +49,7 @@ private static void registerViaLifecycleEvents(final JavaPlugin plugin) { .then( Commands.argument("name", ArgumentTypes.resource(RegistryKey.ENCHANTMENT)) .executes(ctx -> { - ctx.getSource().getSender().sendPlainMessage(ctx.getArgument("name", Enchantment.class).toString()); + ctx.getSource().sendSuccess(Component.translatable(ctx.getArgument("name", Enchantment.class)), false); return Command.SINGLE_SUCCESS; }) ).build() @@ -55,7 +59,7 @@ private static void registerViaLifecycleEvents(final JavaPlugin plugin) { Commands.argument("key", ArgumentTypes.resourceKey(RegistryKey.ENCHANTMENT)) .executes(ctx -> { final TypedKey