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 key = RegistryArgumentExtractor.getTypedKey(ctx, RegistryKey.ENCHANTMENT, "key"); - ctx.getSource().getSender().sendPlainMessage(key.toString()); + ctx.getSource().sendSuccess(Component.text(key.toString()), false); return Command.SINGLE_SUCCESS; }) ).build() @@ -65,7 +69,7 @@ private static void registerViaLifecycleEvents(final JavaPlugin plugin) { Commands.argument("pos", ArgumentTypes.finePosition(false)) .executes(ctx -> { final FinePositionResolver position = ctx.getArgument("pos", FinePositionResolver.class); - ctx.getSource().getSender().sendPlainMessage("Position: " + position.resolve(ctx.getSource())); + ctx.getSource().sendRichSuccess("Position: " + position.resolve(ctx.getSource()), false); return Command.SINGLE_SUCCESS; }) ).build() @@ -73,7 +77,7 @@ private static void registerViaLifecycleEvents(final JavaPlugin plugin) { // ensure plugin commands override commands.register(Commands.literal("tag") .executes(ctx -> { - ctx.getSource().getSender().sendPlainMessage("overriden command"); + ctx.getSource().sendRichSuccess("overriden command", false); return Command.SINGLE_SUCCESS; }) .build(), @@ -88,7 +92,7 @@ private static void registerViaLifecycleEvents(final JavaPlugin plugin) { .then(Commands.literal("sub_command") .requires(source -> source.getSender().hasPermission("testplugin.test")) .executes(ctx -> { - ctx.getSource().getSender().sendPlainMessage("root_command sub_command"); + ctx.getSource().sendRichSuccess("root_command sub_command", false); return Command.SINGLE_SUCCESS; })).build(), null, @@ -126,7 +130,7 @@ public void execute(@NotNull final CommandSourceStack commandSourceStack, final return true; }) .executes(ctx -> { - ctx.getSource().getExecutor().sendMessage("You are in lava!"); + ctx.getSource().sendRichReply("You are in lava!"); return Command.SINGLE_SUCCESS; })).build(), null, @@ -165,14 +169,14 @@ public static void registerViaBootstrap(final BootstrapContext context) { .then(Commands.literal("item") .then(Commands.argument("mat", MaterialArgumentType.item()) .executes(ctx -> { - ctx.getSource().getSender().sendPlainMessage(ctx.getArgument("mat", Material.class).name()); + ctx.getSource().sendSuccess(Component.translatable(ctx.getArgument("mat", Material.class)), false); return Command.SINGLE_SUCCESS; }) ) ).then(Commands.literal("block") .then(Commands.argument("mat", MaterialArgumentType.block()) .executes(ctx -> { - ctx.getSource().getSender().sendPlainMessage(ctx.getArgument("mat", Material.class).name()); + ctx.getSource().sendSuccess(Component.translatable(ctx.getArgument("mat", Material.class)), false); return Command.SINGLE_SUCCESS; }) ) @@ -181,6 +185,21 @@ public static void registerViaBootstrap(final BootstrapContext context) { null, Collections.emptyList() ); + + commands.register(Commands.literal("send_success") + .then(Commands.argument("allow_inform_admins", BoolArgumentType.bool()) + .then(Commands.argument("msg", StringArgumentType.greedyString()) + .executes(ctx -> { + ctx.getSource().sendRichSuccess( + StringArgumentType.getString(ctx, "msg"), + BoolArgumentType.getBool(ctx, "allow_inform_admins") + ); + return Command.SINGLE_SUCCESS; + }))) + .build(), + null, + Collections.emptyList() + ); }); lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS.newHandler(event -> { @@ -188,8 +207,8 @@ public static void registerViaBootstrap(final BootstrapContext context) { commands.register(Commands.literal("heya") .then(Commands.argument("range", ArgumentTypes.doubleRange()) .executes((ct) -> { - ct.getSource().getSender().sendPlainMessage(ct.getArgument("range", DoubleRangeProvider.class).range().toString()); - return 1; + ct.getSource().sendSuccess(Component.text(ct.getArgument("range", DoubleRangeProvider.class).range().toString()), false); + return Command.SINGLE_SUCCESS; }) ).build(), null,