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 # Make sure all submodules are initialized
git submodule update --remote git submodule update --remote
# Package all jars # 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(); 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 * Check whether or not the command is hidden
* *
@ -274,7 +284,7 @@ public class Command<C> {
*/ */
@Nonnull @Nonnull
public Builder<C> meta(@Nonnull final String key, @Nonnull final String value) { 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, return new Builder<>(this.commandManager, commandMeta, this.senderType, this.commandArguments,
this.commandExecutionHandler, this.commandPermission); this.commandExecutionHandler, this.commandPermission);
} }

View file

@ -56,6 +56,7 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -73,12 +74,13 @@ import java.util.function.Function;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public abstract class CommandManager<C> { 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 CommandContextFactory<C> commandContextFactory = new StandardCommandContextFactory<>();
private final ServicePipeline servicePipeline = ServicePipeline.builder().build(); private final ServicePipeline servicePipeline = ServicePipeline.builder().build();
private final ParserRegistry<C> parserRegistry = new StandardParserRegistry<>(); 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 Collection<Command<C>> commands = Lists.newLinkedList();
private final CommandExecutionCoordinator<C> commandExecutionCoordinator; private final CommandExecutionCoordinator<C> commandExecutionCoordinator;
private final CommandTree<C> commandTree; private final CommandTree<C> commandTree;
@ -552,4 +554,42 @@ public abstract class CommandManager<C> {
return new CommandHelpHandler<>(this); 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()) { if (result.getParsedValue().isPresent()) {
parsedArguments.add(child.getValue()); parsedArguments.add(child.getValue());
return this.parseCommand(parsedArguments, commandContext, commandQueue, child); 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()), getChain(root).stream().map(Node::getValue).collect(Collectors.toList()),
stringOrEmpty(commandQueue.peek())); 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() throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter()
.apply(parsedArguments, root), .apply(parsedArguments, root),
commandContext.getSender(), this.getChain(root) commandContext.getSender(), this.getChain(root)
@ -235,6 +248,20 @@ public final class CommandTree<C> {
} else if (!child.getValue().isRequired()) { } else if (!child.getValue().isRequired()) {
return Optional.ofNullable(this.cast(child.getValue().getOwningCommand())); return Optional.ofNullable(this.cast(child.getValue().getOwningCommand()));
} else if (child.isLeaf()) { } 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 */ /* Not enough arguments */
throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter() throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter()
.apply(Objects.requireNonNull( .apply(Objects.requireNonNull(
@ -245,13 +272,21 @@ public final class CommandTree<C> {
.map(Node::getValue) .map(Node::getValue)
.collect(Collectors.toList())); .collect(Collectors.toList()));
} else { } else {
/* /* The child is not a leaf, but may have an intermediary executor, attempt to use it */
throw new NoSuchCommandException(commandContext.getSender(), 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) this.getChain(root)
.stream() .stream()
.map(Node::getValue) .map(Node::getValue)
.collect(Collectors.toList()), .collect(Collectors.toList()));
"");*/ }
return Optional.of(command);
}
/* Child does not have a command and so we cannot proceed */
throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter() throw new InvalidSyntaxException(this.commandManager.getCommandSyntaxFormatter()
.apply(parsedArguments, root), .apply(parsedArguments, root),
commandContext.getSender(), this.getChain(root) commandContext.getSender(), this.getChain(root)
@ -357,8 +392,7 @@ public final class CommandTree<C> {
final ArgumentParseResult<?> result = child.getValue().getParser().parse(commandContext, commandQueue); final ArgumentParseResult<?> result = child.getValue().getParser().parse(commandContext, commandQueue);
if (result.getParsedValue().isPresent()) { if (result.getParsedValue().isPresent()) {
return this.getSuggestions(commandContext, commandQueue, child); 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; node = tempNode;
} }
if (node.getValue() != null) { 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); node.getValue().setOwningCommand(command);
} }
// Verify the command structure every time we add a new command // Verify the command structure every time we add a new command
@ -474,7 +514,6 @@ public final class CommandTree<C> {
// noinspection all // noinspection all
final CommandPermission commandPermission = node.getValue().getOwningCommand().getCommandPermission(); final CommandPermission commandPermission = node.getValue().getOwningCommand().getCommandPermission();
/* All leaves must necessarily have an owning command */ /* All leaves must necessarily have an owning command */
// noinspection all
node.nodeMeta.put("permission", commandPermission); node.nodeMeta.put("permission", commandPermission);
// Get chain and order it tail->head then skip the tail (leaf node) // Get chain and order it tail->head then skip the tail (leaf node)
List<Node<CommandArgument<C, ?>>> chain = this.getChain(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 // Go through all nodes from the tail upwards until a collision occurs
for (final Node<CommandArgument<C, ?>> commandArgumentNode : chain) { for (final Node<CommandArgument<C, ?>> commandArgumentNode : chain) {
final CommandPermission existingPermission = (CommandPermission) commandArgumentNode.nodeMeta.get("permission"); final CommandPermission existingPermission = (CommandPermission) commandArgumentNode.nodeMeta.get("permission");
CommandPermission permission;
if (existingPermission != null) { if (existingPermission != null) {
commandArgumentNode.nodeMeta.put("permission", permission = OrPermission.of(Arrays.asList(commandPermission, existingPermission));
OrPermission.of(Arrays.asList(commandPermission, existingPermission)));
} else { } 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 @BeforeAll
static void newTree() { static void newTree() {
manager = new TestCommandManager(); manager = new TestCommandManager();
/* Build general test commands */
manager.command(manager.commandBuilder("test", SimpleCommandMeta.empty()) manager.command(manager.commandBuilder("test", SimpleCommandMeta.empty())
.literal("one").build()) .literal("one").build())
.command(manager.commandBuilder("test", SimpleCommandMeta.empty()) .command(manager.commandBuilder("test", SimpleCommandMeta.empty())
@ -57,6 +59,8 @@ class CommandTreeTest {
.optional("num", EXPECTED_INPUT_NUMBER)) .optional("num", EXPECTED_INPUT_NUMBER))
.build()) .build())
.command(manager.commandBuilder("req").withSenderType(SpecificCommandSender.class).build()); .command(manager.commandBuilder("req").withSenderType(SpecificCommandSender.class).build());
/* Build command to test command proxying */
final Command<TestCommandSender> toProxy = manager.commandBuilder("test") final Command<TestCommandSender> toProxy = manager.commandBuilder("test")
.literal("unproxied") .literal("unproxied")
.argument(StringArgument.required("string")) .argument(StringArgument.required("string"))
@ -66,6 +70,17 @@ class CommandTreeTest {
.build(); .build();
manager.command(toProxy); manager.command(toProxy);
manager.command(manager.commandBuilder("proxy").proxies(toProxy).build()); 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 @Test
@ -136,6 +151,12 @@ class CommandTreeTest {
manager.executeCommand(new TestCommandSender(), "proxy foo 10").join(); 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 { public static final class SpecificCommandSender extends TestCommandSender {
} }

View file

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