Expand ability to use Adventure Components in exceptions and metadata (#200)

* cloud-minecraft: Support preserving component error messages

Any exception thrown by an argument parser that is a
ComponentMessageThrowable will now have its component message directly
queried.

* minecraft-extras: Add Component-based description handling for help

* Apply review comments
This commit is contained in:
zml 2021-01-09 23:32:57 -08:00 committed by Alexander Söderberg
parent 12f5c71504
commit a6f8159410
3 changed files with 107 additions and 27 deletions

View file

@ -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<C> {
private static final Component NULL = Component.text("null");
/**
* Default component builder for {@link InvalidSyntaxException}
*/
public static final Function<Exception, Component> 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<Exception, Component> 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<C> {
* Default component builder for {@link ArgumentParseException}
*/
public static final Function<Exception, Component> 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<C> {
final String stackTrace = writer.toString().replaceAll("\t", " ");
final HoverEvent<Component> 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<C> {
);
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<C> {
final @NonNull CommandManager<C> 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<C> {
)
);
}
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<C> {
)
);
}
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<C> {
)
);
}
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<C> {
)
);
}
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<C> {
}
}
private static Component getMessage(final Throwable throwable) {
final Component msg = ComponentMessageThrowable.getOrConvertMessage(throwable);
return msg == null ? NULL : msg;
}
/**
* Exception types

View file

@ -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.
*
* <p>This will not set the plain-text description, but will be used in place of that meta key in help.</p>
*/
public static final CommandMeta.Key<Component> DESCRIPTION = CommandMeta.Key.of(
Component.class,
"cloud:minecraft_extras/description"
);
/**
* A component long description.
*
* <p>This will not set the plain-text long description, but will be used in place of that meta key in help.</p>
*/
public static final CommandMeta.Key<Component> LONG_DESCRIPTION = CommandMeta.Key.of(
Component.class,
"cloud:minecraft_extras/long_description"
);
private MinecraftExtrasMetaKeys() {
}
}

View file

@ -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<C> {
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<Component> 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<C> {
.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(" "))