Add intermediary command executors.

This allows for command executors along the entire command chain, such that `/command`and `/command subcommand` may both be executed.
This commit is contained in:
Alexander Söderberg 2020-09-26 16:23:04 +02:00 committed by Alexander Söderberg
parent 64fa3430a9
commit 0d44a8c944
8 changed files with 160 additions and 23 deletions

View file

@ -3,4 +3,6 @@
# Make sure all submodules are initialized
git submodule update --remote
# Package all jars
./mvnw clean package
export MAVEN_OPTS="-XX:+TieredCompilation -XX:TieredStopAtLevel=1"
./mvnw -T1C clean package -DskipTests=true

View file

@ -226,6 +226,16 @@ public class Command<C> {
return this.arguments.get(argument).getDescription();
}
@Override
public final String toString() {
final StringBuilder stringBuilder = new StringBuilder();
for (final CommandArgument<C, ?> argument : this.getArguments()) {
stringBuilder.append(argument.getName()).append(' ');
}
final String build = stringBuilder.toString();
return build.substring(0, build.length() - 1);
}
/**
* Check whether or not the command is hidden
*
@ -274,7 +284,7 @@ public class Command<C> {
*/
@Nonnull
public Builder<C> meta(@Nonnull final String key, @Nonnull final String value) {
final CommandMeta commandMeta = SimpleCommandMeta.builder().with(this.commandMeta).build();
final CommandMeta commandMeta = SimpleCommandMeta.builder().with(this.commandMeta).with(key, value).build();
return new Builder<>(this.commandManager, commandMeta, this.senderType, this.commandArguments,
this.commandExecutionHandler, this.commandPermission);
}

View file

@ -56,6 +56,7 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@ -73,12 +74,13 @@ import java.util.function.Function;
@SuppressWarnings("unused")
public abstract class CommandManager<C> {
private final Map<Class<? extends Exception>, BiConsumer<C, ? extends Exception>> exceptionHandlers = Maps.newHashMap();
private final EnumSet<ManagerSettings> managerSettings = EnumSet.of(ManagerSettings.ENFORCE_INTERMEDIARY_PERMISSIONS);
private final CommandContextFactory<C> commandContextFactory = new StandardCommandContextFactory<>();
private final ServicePipeline servicePipeline = ServicePipeline.builder().build();
private final ParserRegistry<C> parserRegistry = new StandardParserRegistry<>();
private final Map<Class<? extends Exception>, BiConsumer<C, ? extends Exception>> exceptionHandlers = Maps.newHashMap();
private final Collection<Command<C>> commands = Lists.newLinkedList();
private final CommandExecutionCoordinator<C> commandExecutionCoordinator;
private final CommandTree<C> commandTree;
@ -552,4 +554,42 @@ public abstract class CommandManager<C> {
return new CommandHelpHandler<>(this);
}
/**
* Get a command manager setting
*
* @param setting Setting
* @return {@code true} if the setting is activated or {@code false} if it's not
*/
public boolean getSetting(@Nonnull final ManagerSettings setting) {
return this.managerSettings.contains(setting);
}
/**
* Set the setting
*
* @param setting Setting to set
* @param value Value
*/
public void setSetting(@Nonnull final ManagerSettings setting,
final boolean value) {
if (value) {
this.managerSettings.add(setting);
} else {
this.managerSettings.remove(setting);
}
}
/**
* Configurable command related settings
*/
public enum ManagerSettings {
/**
* Do not create a compound permission and do not look greedily
* for child permission values, if a preceding command in the tree path
* has a command handler attached
*/
ENFORCE_INTERMEDIARY_PERMISSIONS
}
}

View file

@ -190,8 +190,7 @@ public final class CommandTree<C> {
if (result.getParsedValue().isPresent()) {
parsedArguments.add(child.getValue());
return this.parseCommand(parsedArguments, commandContext, commandQueue, child);
} /*else if (result.getFailure().isPresent() && root.children.size() == 1) {
}*/
}
}
}
}
@ -201,7 +200,21 @@ public final class CommandTree<C> {
getChain(root).stream().map(Node::getValue).collect(Collectors.toList()),
stringOrEmpty(commandQueue.peek()));
}
/* We have already traversed the tree */
/* If we couldn't match a child, check if there's a command attached and execute it */
if (root.getValue() != null && root.getValue().getOwningCommand() != null) {
final Command<C> command = root.getValue().getOwningCommand();
if (!this.getCommandManager().hasPermission(commandContext.getSender(),
command.getCommandPermission())) {
throw new NoPermissionException(command.getCommandPermission(),
commandContext.getSender(),
this.getChain(root)
.stream()
.map(Node::getValue)
.collect(Collectors.toList()));
}
return Optional.of(root.getValue().getOwningCommand());
}
/* We know that there's no command and we also cannot match any of the children */
throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter()
.apply(parsedArguments, root),
commandContext.getSender(), this.getChain(root)
@ -235,6 +248,20 @@ public final class CommandTree<C> {
} else if (!child.getValue().isRequired()) {
return Optional.ofNullable(this.cast(child.getValue().getOwningCommand()));
} else if (child.isLeaf()) {
/* The child is not a leaf, but may have an intermediary executor, attempt to use it */
if (root.getValue() != null && root.getValue().getOwningCommand() != null) {
final Command<C> command = root.getValue().getOwningCommand();
if (!this.getCommandManager().hasPermission(commandContext.getSender(),
command.getCommandPermission())) {
throw new NoPermissionException(command.getCommandPermission(),
commandContext.getSender(),
this.getChain(root)
.stream()
.map(Node::getValue)
.collect(Collectors.toList()));
}
return Optional.of(command);
}
/* Not enough arguments */
throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter()
.apply(Objects.requireNonNull(
@ -245,13 +272,21 @@ public final class CommandTree<C> {
.map(Node::getValue)
.collect(Collectors.toList()));
} else {
/*
throw new NoSuchCommandException(commandContext.getSender(),
this.getChain(root)
.stream()
.map(Node::getValue)
.collect(Collectors.toList()),
"");*/
/* The child is not a leaf, but may have an intermediary executor, attempt to use it */
if (root.getValue() != null && root.getValue().getOwningCommand() != null) {
final Command<C> command = root.getValue().getOwningCommand();
if (!this.getCommandManager().hasPermission(commandContext.getSender(),
command.getCommandPermission())) {
throw new NoPermissionException(command.getCommandPermission(),
commandContext.getSender(),
this.getChain(root)
.stream()
.map(Node::getValue)
.collect(Collectors.toList()));
}
return Optional.of(command);
}
/* Child does not have a command and so we cannot proceed */
throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter()
.apply(parsedArguments, root),
commandContext.getSender(), this.getChain(root)
@ -357,8 +392,7 @@ public final class CommandTree<C> {
final ArgumentParseResult<?> result = child.getValue().getParser().parse(commandContext, commandQueue);
if (result.getParsedValue().isPresent()) {
return this.getSuggestions(commandContext, commandQueue, child);
} /* else if (result.getFailure().isPresent() && root.children.size() == 1) {
}*/
}
}
}
}
@ -407,6 +441,12 @@ public final class CommandTree<C> {
node = tempNode;
}
if (node.getValue() != null) {
if (node.getValue().getOwningCommand() != null) {
throw new IllegalStateException(String.format(
"Duplicate command chains detected. Node '%s' already has an owning command (%s)",
node.toString(), node.getValue().getOwningCommand().toString()
));
}
node.getValue().setOwningCommand(command);
}
// Verify the command structure every time we add a new command
@ -474,7 +514,6 @@ public final class CommandTree<C> {
// noinspection all
final CommandPermission commandPermission = node.getValue().getOwningCommand().getCommandPermission();
/* All leaves must necessarily have an owning command */
// noinspection all
node.nodeMeta.put("permission", commandPermission);
// Get chain and order it tail->head then skip the tail (leaf node)
List<Node<CommandArgument<C, ?>>> chain = this.getChain(node);
@ -483,12 +522,25 @@ public final class CommandTree<C> {
// Go through all nodes from the tail upwards until a collision occurs
for (final Node<CommandArgument<C, ?>> commandArgumentNode : chain) {
final CommandPermission existingPermission = (CommandPermission) commandArgumentNode.nodeMeta.get("permission");
CommandPermission permission;
if (existingPermission != null) {
commandArgumentNode.nodeMeta.put("permission",
OrPermission.of(Arrays.asList(commandPermission, existingPermission)));
permission = OrPermission.of(Arrays.asList(commandPermission, existingPermission));
} else {
commandArgumentNode.nodeMeta.put("permission", commandPermission);
permission = commandPermission;
}
/* Now also check if there's a command handler attached to an upper level node */
if (commandArgumentNode.getValue() != null && commandArgumentNode.getValue().getOwningCommand() != null) {
final Command<C> command = commandArgumentNode.getValue().getOwningCommand();
if (this.getCommandManager().getSetting(CommandManager.ManagerSettings.ENFORCE_INTERMEDIARY_PERMISSIONS)) {
permission = command.getCommandPermission();
} else {
permission = OrPermission.of(Arrays.asList(permission, command.getCommandPermission()));
}
}
commandArgumentNode.nodeMeta.put("permission", permission);
}
});
}

View file

@ -46,6 +46,8 @@ class CommandTreeTest {
@BeforeAll
static void newTree() {
manager = new TestCommandManager();
/* Build general test commands */
manager.command(manager.commandBuilder("test", SimpleCommandMeta.empty())
.literal("one").build())
.command(manager.commandBuilder("test", SimpleCommandMeta.empty())
@ -57,6 +59,8 @@ class CommandTreeTest {
.optional("num", EXPECTED_INPUT_NUMBER))
.build())
.command(manager.commandBuilder("req").withSenderType(SpecificCommandSender.class).build());
/* Build command to test command proxying */
final Command<TestCommandSender> toProxy = manager.commandBuilder("test")
.literal("unproxied")
.argument(StringArgument.required("string"))
@ -66,6 +70,17 @@ class CommandTreeTest {
.build();
manager.command(toProxy);
manager.command(manager.commandBuilder("proxy").proxies(toProxy).build());
/* Build command for testing intermediary and final executors */
manager.command(manager.commandBuilder("command")
.withPermission("command.inner")
.literal("inner")
.handler(c -> System.out.println("Using inner command"))
.build());
manager.command(manager.commandBuilder("command")
.withPermission("command.outer")
.handler(c -> System.out.println("Using outer command"))
.build());
}
@Test
@ -136,6 +151,12 @@ class CommandTreeTest {
manager.executeCommand(new TestCommandSender(), "proxy foo 10").join();
}
@Test
void testIntermediary() {
manager.executeCommand(new TestCommandSender(), "command inner").join();
manager.executeCommand(new TestCommandSender(), "command").join();
}
public static final class SpecificCommandSender extends TestCommandSender {
}

View file

@ -47,6 +47,7 @@ public class TestCommandManager extends CommandManager<TestCommandSender> {
@Override
public final boolean hasPermission(@Nonnull final TestCommandSender sender,
@Nonnull final String permission) {
System.out.printf("Testing permission: %s\n", permission);
return !permission.equalsIgnoreCase("no");
}

View file

@ -42,6 +42,7 @@ import com.intellectualsites.commands.bukkit.BukkitCommandManager;
import com.intellectualsites.commands.bukkit.BukkitCommandMetaBuilder;
import com.intellectualsites.commands.bukkit.CloudBukkitCapabilities;
import com.intellectualsites.commands.bukkit.parsers.WorldArgument;
import com.intellectualsites.commands.exceptions.InvalidSyntaxException;
import com.intellectualsites.commands.execution.AsynchronousCommandExecutionCoordinator;
import com.intellectualsites.commands.execution.CommandExecutionCoordinator;
import com.intellectualsites.commands.extra.confirmation.CommandConfirmationManager;
@ -92,7 +93,7 @@ public final class BukkitTest extends JavaPlugin {
mgr);
try {
((PaperCommandManager<CommandSender>) mgr).registerBrigadier();
mgr.registerBrigadier();
} catch (final Exception e) {
getLogger().warning("Failed to initialize Brigadier support: " + e.getMessage());
}
@ -116,7 +117,6 @@ public final class BukkitTest extends JavaPlugin {
BukkitCommandMetaBuilder.builder().withDescription(p.get(StandardParameters.DESCRIPTION,
"No description")).build());
annotationParser.parse(this);
//noinspection all
mgr.command(mgr.commandBuilder("gamemode", this.metaWithDescription("Your ugli"), "gajmöde")
.argument(EnumArgument.required(GameMode.class, "gamemode"))
@ -148,6 +148,9 @@ public final class BukkitTest extends JavaPlugin {
));
})
.build())
.command(mgr.commandBuilder("uuidtest")
.handler(c -> c.getSender().sendMessage("Hey yo dum, provide a UUID idiot. Thx!"))
.build())
.command(mgr.commandBuilder("uuidtest")
.argument(UUID.class, "uuid", builder -> builder
.asRequired()
@ -201,11 +204,14 @@ public final class BukkitTest extends JavaPlugin {
.handler(confirmationManager.createConfirmationExecutionHandler()).build())
.command(mgr.commandBuilder("cloud")
.literal("help")
.withPermission("cloud.help")
.argument(StringArgument.<CommandSender>newBuilder("query").greedy()
.asOptionalWithDefault("")
.build(), Description.of("Help Query"))
.handler(c -> minecraftHelp.queryCommands(c.<String>get("query").orElse(""),
c.getSender())).build());
mgr.registerExceptionHandler(InvalidSyntaxException.class, (c, e) -> e.printStackTrace());
} catch (final Exception e) {
e.printStackTrace();
}
@ -222,7 +228,7 @@ public final class BukkitTest extends JavaPlugin {
}
@Confirmation
@CommandMethod("cloud debug")
@CommandMethod(value = "cloud", permission = "cloud.debug")
private void doHelp() {
final Set<CloudBukkitCapabilities> capabilities = this.mgr.queryCapabilities();
Bukkit.broadcastMessage(ChatColor.GOLD + "" + ChatColor.BOLD + "Capabilities");

View file

@ -155,4 +155,9 @@ final class BukkitCommand<C> extends org.bukkit.command.Command implements Plugi
return this.cloudCommand.getCommandPermission().toString();
}
@Override
public String getUsage() {
return this.manager.getCommandSyntaxFormatter().apply(this.cloudCommand.getArguments(), null);
}
}