diff --git a/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftExceptionHandler.java b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftExceptionHandler.java index 55119f31..7d9a825e 100644 --- a/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftExceptionHandler.java +++ b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftExceptionHandler.java @@ -36,6 +36,7 @@ import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; +import net.kyori.adventure.util.ComponentMessageThrowable; import org.checkerframework.checker.nullness.qual.NonNull; import java.io.PrintWriter; @@ -53,31 +54,29 @@ import java.util.function.Function; */ public final class MinecraftExceptionHandler { + private static final Component NULL = Component.text("null"); + /** * Default component builder for {@link InvalidSyntaxException} */ public static final Function DEFAULT_INVALID_SYNTAX_FUNCTION = - e -> Component.text() - .append(Component.text("Invalid command syntax. Correct command syntax is: ", NamedTextColor.RED)) + e -> Component.text("Invalid command syntax. Correct command syntax is: ", NamedTextColor.RED) .append(ComponentHelper.highlight( Component.text( String.format("/%s", ((InvalidSyntaxException) e).getCorrectSyntax()), NamedTextColor.GRAY ), NamedTextColor.WHITE - )) - .build(); + )); /** * Default component builder for {@link InvalidCommandSenderException} */ public static final Function DEFAULT_INVALID_SENDER_FUNCTION = - e -> Component.text() - .append(Component.text("Invalid command sender. You must be of type ", NamedTextColor.RED)) + e -> Component.text("Invalid command sender. You must be of type ", NamedTextColor.RED) .append(Component.text( ((InvalidCommandSenderException) e).getRequiredSender().getSimpleName(), NamedTextColor.GRAY - )) - .build(); + )); /** * Default component builder for {@link NoPermissionException} */ @@ -91,10 +90,8 @@ public final class MinecraftExceptionHandler { * Default component builder for {@link ArgumentParseException} */ public static final Function DEFAULT_ARGUMENT_PARSING_FUNCTION = - e -> Component.text() - .append(Component.text("Invalid command argument: ", NamedTextColor.RED)) - .append(Component.text(e.getCause().getMessage(), NamedTextColor.GRAY)) - .build(); + e -> Component.text("Invalid command argument: ", NamedTextColor.RED) + .append(getMessage(e.getCause()).colorIfAbsent(NamedTextColor.GRAY)); /** * Default component builder for {@link CommandExecutionException} * @@ -110,6 +107,7 @@ public final class MinecraftExceptionHandler { final String stackTrace = writer.toString().replaceAll("\t", " "); final HoverEvent hover = HoverEvent.showText( Component.text() + .append(getMessage(cause)) .append(Component.text(stackTrace)) .append(Component.newline()) .append(Component.text( @@ -120,10 +118,8 @@ public final class MinecraftExceptionHandler { ); final ClickEvent click = ClickEvent.copyToClipboard(stackTrace); return Component.text() - .append(Component.text( - "An internal error occurred while attempting to perform this command.", - NamedTextColor.RED - )) + .content("An internal error occurred while attempting to perform this command.") + .color(NamedTextColor.RED) .hoverEvent(hover) .clickEvent(click) .build(); @@ -251,7 +247,7 @@ public final class MinecraftExceptionHandler { final @NonNull CommandManager manager, final @NonNull Function<@NonNull C, @NonNull Audience> audienceMapper ) { - if (componentBuilders.containsKey(ExceptionType.INVALID_SYNTAX)) { + if (this.componentBuilders.containsKey(ExceptionType.INVALID_SYNTAX)) { manager.registerExceptionHandler( InvalidSyntaxException.class, (c, e) -> audienceMapper.apply(c).sendMessage( @@ -260,7 +256,7 @@ public final class MinecraftExceptionHandler { ) ); } - if (componentBuilders.containsKey(ExceptionType.INVALID_SENDER)) { + if (this.componentBuilders.containsKey(ExceptionType.INVALID_SENDER)) { manager.registerExceptionHandler( InvalidCommandSenderException.class, (c, e) -> audienceMapper.apply(c).sendMessage( @@ -269,7 +265,7 @@ public final class MinecraftExceptionHandler { ) ); } - if (componentBuilders.containsKey(ExceptionType.NO_PERMISSION)) { + if (this.componentBuilders.containsKey(ExceptionType.NO_PERMISSION)) { manager.registerExceptionHandler( NoPermissionException.class, (c, e) -> audienceMapper.apply(c).sendMessage( @@ -278,7 +274,7 @@ public final class MinecraftExceptionHandler { ) ); } - if (componentBuilders.containsKey(ExceptionType.ARGUMENT_PARSING)) { + if (this.componentBuilders.containsKey(ExceptionType.ARGUMENT_PARSING)) { manager.registerExceptionHandler( ArgumentParseException.class, (c, e) -> audienceMapper.apply(c).sendMessage( @@ -287,7 +283,7 @@ public final class MinecraftExceptionHandler { ) ); } - if (componentBuilders.containsKey(ExceptionType.COMMAND_EXECUTION)) { + if (this.componentBuilders.containsKey(ExceptionType.COMMAND_EXECUTION)) { manager.registerExceptionHandler( CommandExecutionException.class, (c, e) -> audienceMapper.apply(c).sendMessage( @@ -298,6 +294,10 @@ public final class MinecraftExceptionHandler { } } + private static Component getMessage(final Throwable throwable) { + final Component msg = ComponentMessageThrowable.getOrConvertMessage(throwable); + return msg == null ? NULL : msg; + } /** * Exception types diff --git a/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftExtrasMetaKeys.java b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftExtrasMetaKeys.java new file mode 100644 index 00000000..bf2bd5f5 --- /dev/null +++ b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftExtrasMetaKeys.java @@ -0,0 +1,60 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +package cloud.commandframework.minecraft.extras; + +import cloud.commandframework.meta.CommandMeta; +import net.kyori.adventure.text.Component; + +/** + * Extra command metadata for providing rich text. + * + * @since 1.4.0 + */ +public final class MinecraftExtrasMetaKeys { + + /** + * A component short description. + * + *

This will not set the plain-text description, but will be used in place of that meta key in help.

+ */ + public static final CommandMeta.Key DESCRIPTION = CommandMeta.Key.of( + Component.class, + "cloud:minecraft_extras/description" + ); + + /** + * A component long description. + * + *

This will not set the plain-text long description, but will be used in place of that meta key in help.

+ */ + public static final CommandMeta.Key LONG_DESCRIPTION = CommandMeta.Key.of( + Component.class, + "cloud:minecraft_extras/long_description" + ); + + private MinecraftExtrasMetaKeys() { + } + +} diff --git a/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftHelp.java b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftHelp.java index 76cb0707..95265a49 100644 --- a/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftHelp.java +++ b/cloud-minecraft/cloud-minecraft-extras/src/main/java/cloud/commandframework/minecraft/extras/MinecraftHelp.java @@ -43,6 +43,7 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; @@ -360,9 +361,16 @@ public final class MinecraftHelp { return header; }, (helpEntry, isLastOfPage) -> { - final Component description = helpEntry.getDescription().isEmpty() - ? this.messageProvider.provide(sender, MESSAGE_CLICK_TO_SHOW_HELP) - : this.descriptionDecorator.apply(helpEntry.getDescription()); + final Optional richDescription = + helpEntry.getCommand().getCommandMeta().get(MinecraftExtrasMetaKeys.DESCRIPTION); + final Component description; + if (richDescription.isPresent()) { + description = richDescription.get(); + } else if (helpEntry.getDescription().isEmpty()) { + description = this.messageProvider.provide(sender, MESSAGE_CLICK_TO_SHOW_HELP); + } else { + description = this.descriptionDecorator.apply(helpEntry.getDescription()); + } final boolean lastBranch = isLastOfPage || helpTopic.getEntries().indexOf(helpEntry) == helpTopic.getEntries().size() - 1; @@ -439,9 +447,21 @@ public final class MinecraftHelp { .append(text(": ", this.colors.primary)) .append(this.highlight(text("/" + command, this.colors.highlight))) ); - final Component topicDescription = helpTopic.getDescription().isEmpty() - ? this.messageProvider.provide(sender, MESSAGE_NO_DESCRIPTION) - : this.descriptionDecorator.apply(helpTopic.getDescription()); + /* Topics will use the long description if available, but fall back to the short description. */ + final Component richDescription = + helpTopic.getCommand().getCommandMeta().get(MinecraftExtrasMetaKeys.LONG_DESCRIPTION) + .orElse(helpTopic.getCommand().getCommandMeta().get(MinecraftExtrasMetaKeys.DESCRIPTION) + .orElse(null)); + + final Component topicDescription; + if (richDescription != null) { + topicDescription = richDescription; + } else if (helpTopic.getDescription().isEmpty()) { + topicDescription = this.messageProvider.provide(sender, MESSAGE_NO_DESCRIPTION); + } else { + topicDescription = this.descriptionDecorator.apply(helpTopic.getDescription()); + } + final boolean hasArguments = helpTopic.getCommand().getArguments().size() > 1; audience.sendMessage(text() .append(text(" "))