Skip to content

Commit

Permalink
Initial implementation of FlagsArgument (and other changes)
Browse files Browse the repository at this point in the history
Changes:
- Implemented `FlagsArgument` (#483)
  - Moved var handles for `CommandNode` `children`,`literals`, and `arguments` to `CommandAPIHandler`
  - Added `FlagsArgumentCommon` FlagsArgumentRootNode` and `FlagsArgumentEndingNode` to handle the special node structure and parsing required
- Updated `CustomArgument`
  - All `AbstractArgument` builder methods are delegated to the base argument
  - Replaced `CommandAPIExecutor` parameter of `AbstractArgument#addArgumentNodes` to a `Function` to allow object that hold arguments to better control how those arguments are executed
- Added package `dev.jorel.commandapi.commandnodes` for class that extend `CommandNode` and related classes
- Tweaked some exceptions
  - `GreedyArgumentException`
    - Changed because the `FlagsArgument` is sometimes greedy - only greedy iff it has no terminal branches
    - Greedy arguments are now detected when `AbstractArgument#addArgumentNodes` returns an empty list
    - Tweaked the exception message
  - `DuplicateNodeNameException`
    - Changed because literal arguments can conflict with other nodes if they are listed
    - Now thrown when two listed arguments have the same node name
    - Added `UnnamedArgumentCommandNode` to make sure unlisted arguments do not conflict
    - Renamed `MultiLiteralCommandNode` to `NamedLiteralCommandNode` for use by listed `Literal` arguments
    - Tweaked the exception message

TODO:
- Clean up code
- Add tests
- Remove test commands in CommandAPIMain
- Add javadocs and documentation
- Hope Mojang/brigadier#144 is resolved, otherwise be annoyed :(
  • Loading branch information
willkroboth committed Apr 30, 2024
1 parent c0260f6 commit 13bf2c4
Show file tree
Hide file tree
Showing 26 changed files with 1,301 additions and 317 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import com.mojang.brigadier.tree.CommandNode;
import dev.jorel.commandapi.arguments.AbstractArgument;
import dev.jorel.commandapi.arguments.GreedyArgument;
import dev.jorel.commandapi.exceptions.GreedyArgumentException;
import dev.jorel.commandapi.exceptions.MissingCommandExecutorException;

import java.util.ArrayList;
Expand Down Expand Up @@ -125,55 +123,34 @@ public List<List<String>> getBranchesAsStrings() {
/**
* Builds the Brigadier {@link CommandNode} structure for this argument tree.
*
* @param previousNodes A List of {@link CommandNode}s to add this argument onto.
* @param previousArguments A List of CommandAPI arguments that came before this argument tree.
* @param previousNonLiteralArgumentNames A List of Strings containing the node names that came before this argument.
* @param <Source> The Brigadier Source object for running commands.
* @param previousNodes A List of {@link CommandNode}s to add this argument onto.
* @param previousArguments A List of CommandAPI arguments that came before this argument tree.
* @param previousArgumentNames A List of Strings containing the node names that came before this argument.
* @param <Source> The Brigadier Source object for running commands.
*/
public <Source> void buildBrigadierNode(
List<CommandNode<Source>> previousNodes,
List<Argument> previousArguments, List<String> previousNonLiteralArgumentNames
List<Argument> previousArguments, List<String> previousArgumentNames
) {
CommandAPIHandler<Argument, CommandSender, Source> handler = CommandAPIHandler.getInstance();

// Check preconditions
if (argument instanceof GreedyArgument && !arguments.isEmpty()) {
// Argument is followed by at least some arguments
throw new GreedyArgumentException(previousArguments, argument, getBranchesAsList());
}
if (!executor.hasAnyExecutors() && arguments.isEmpty()) {
// If we don't have any executors then no branches is bad because this path can't be run at all
throw new MissingCommandExecutorException(previousArguments, argument);
}

// Create node for this argument
previousNodes = argument.addArgumentNodes(previousNodes, previousArguments, previousNonLiteralArgumentNames, executor);
previousNodes = argument.addArgumentNodes(previousNodes, previousArguments, previousArgumentNames,
executor.hasAnyExecutors() ? args -> handler.generateBrigadierCommand(args, executor) : null);

// Add our branches as children to the node
for (AbstractArgumentTree<?, Argument, CommandSender> child : arguments) {
// We need a new list for each branch of the tree
List<Argument> newPreviousArguments = new ArrayList<>(previousArguments);
List<String> newPreviousArgumentNames = new ArrayList<>(previousNonLiteralArgumentNames);
List<String> newPreviousArgumentNames = new ArrayList<>(previousArgumentNames);

child.buildBrigadierNode(previousNodes, newPreviousArguments, newPreviousArgumentNames);
}
}

/**
* @return A list of paths that represent the possible branches of this argument tree as Argument objects.
*/
protected List<List<Argument>> getBranchesAsList() {
if (arguments.isEmpty()) return List.of(List.of());

List<List<Argument>> branchesList = new ArrayList<>();

for (AbstractArgumentTree<?, Argument, CommandSender> branch : arguments) {
for (List<Argument> subBranchList : branch.getBranchesAsList()) {
List<Argument> newBranchList = new ArrayList<>();
newBranchList.add(branch.argument);
newBranchList.addAll(subBranchList);
branchesList.add(newBranchList);
}
}

return branchesList;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@
*******************************************************************************/
package dev.jorel.commandapi;

import com.mojang.brigadier.Command;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;
import dev.jorel.commandapi.arguments.AbstractArgument;
import dev.jorel.commandapi.arguments.GreedyArgument;
import dev.jorel.commandapi.exceptions.GreedyArgumentException;
import dev.jorel.commandapi.exceptions.MissingCommandExecutorException;
import dev.jorel.commandapi.exceptions.OptionalArgumentException;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;

/**
* A builder used to create commands to be registered by the CommandAPI.
Expand Down Expand Up @@ -317,30 +317,19 @@ protected <Source> void createArgumentNodes(LiteralCommandNode<Source> rootNode)
previousArguments.add(commandNames);

// Add required arguments
Function<List<Argument>, Command<Source>> executorCreator = executor.hasAnyExecutors() ?
args -> handler.generateBrigadierCommand(args, executor) : null;
for (int i = 0; i < requiredArguments.size(); i++) {
Argument argument = requiredArguments.get(i);
previousNodes = argument.addArgumentNodes(previousNodes, previousArguments, previousArgumentNames,
// Only the last required argument is executable
i == requiredArguments.size() - 1 ? executor : null);
i == requiredArguments.size() - 1 ? executorCreator : null);
}

// Add optional arguments
for (Argument argument : optionalArguments) {
// All optional arguments are executable
previousNodes = argument.addArgumentNodes(previousNodes, previousArguments, previousArgumentNames, executor);
}

// Check greedy argument constraint
// We need to check it down here so that all the combined arguments are properly considered after unpacking
for (int i = 0; i < previousArguments.size() - 1 /* Minus one since we don't need to check last argument */; i++) {
Argument argument = previousArguments.get(i);
if (argument instanceof GreedyArgument) {
throw new GreedyArgumentException(
previousArguments.subList(0, i), // Arguments before this
argument,
List.of(previousArguments.subList(i + 1, previousArguments.size())) // Arguments after this
);
}
previousNodes = argument.addArgumentNodes(previousNodes, previousArguments, previousArgumentNames, executorCreator);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@ protected <Source> void createArgumentNodes(LiteralCommandNode<Source> rootNode)
for (AbstractArgumentTree<?, Argument, CommandSender> argument : arguments) {
// We need new previousArguments lists for each branch so they don't interfere
List<Argument> previousArguments = new ArrayList<>();
List<String> previousNonLiteralArgumentNames = new ArrayList<>();
List<String> previousArgumentNames = new ArrayList<>();
previousArguments.add(commandNames);

argument.buildBrigadierNode(List.of(rootNode), previousArguments, previousNonLiteralArgumentNames);
argument.buildBrigadierNode(List.of(rootNode), previousArguments, previousArgumentNames);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.tree.ArgumentCommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;
import dev.jorel.commandapi.arguments.*;
import dev.jorel.commandapi.commandsenders.AbstractCommandSender;
Expand All @@ -59,24 +60,35 @@
* @param <Source> The class for running Brigadier commands
*/
@RequireField(in = CommandContext.class, name = "arguments", ofType = Map.class)
@RequireField(in = CommandNode.class, name = "children", ofType = Map.class)
@RequireField(in = CommandNode.class, name = "literals", ofType = Map.class)
@RequireField(in = CommandNode.class, name = "arguments", ofType = Map.class)
public class CommandAPIHandler<Argument
/// @cond DOX
extends AbstractArgument<?, ?, Argument, CommandSender>
/// @endcond
, CommandSender, Source> {
// TODO: Need to ensure this can be safely "disposed of" when we're done (e.g. on reloads).
// I hiiiiiiighly doubt we're storing class caches of classes that can be unloaded at runtime,
// but this IS a generic class caching system and we don't want derpy memory leaks
private static final Map<ClassCache, Field> FIELDS;

private static final SafeVarHandle<CommandContext<?>, Map<String, ParsedArgument<?, ?>>> commandContextArguments;
// VarHandle seems incapable of setting final fields, so we have to use Field here
private static final Field commandNodeChildren;
private static final Field commandNodeLiterals;
private static final Field commandNodeArguments;

// Compute all var handles all in one go so we don't do this during main server
// runtime
// Compute all var handles all in one go so we don't do this during main server runtime
static {
FIELDS = new HashMap<>();

commandContextArguments = SafeVarHandle.ofOrNull(CommandContext.class, "arguments", "arguments", Map.class);
commandNodeChildren = CommandAPIHandler.getField(CommandNode.class, "children");
commandNodeLiterals = CommandAPIHandler.getField(CommandNode.class, "literals");
commandNodeArguments = CommandAPIHandler.getField(CommandNode.class, "arguments");
}

// TODO: Need to ensure this can be safely "disposed of" when we're done (e.g. on reloads).
// I hiiiiiiighly doubt we're storing class caches of classes that can be unloaded at runtime,
// but this IS a generic class caching system and we don't want derpy memory leaks
private static final Map<ClassCache, Field> FIELDS = new HashMap<>();

final CommandAPIPlatform<Argument, CommandSender, Source> platform;
final List<RegisteredCommand> registeredCommands; // Keep track of what has been registered for type checking
final Map<List<String>, Previewable<?, ?>> previewableArguments; // Arguments with previewable chat
Expand Down Expand Up @@ -141,7 +153,7 @@ public CommandAPIPlatform<Argument, CommandSender, Source> getPlatform() {
// SECTION: Creating commands //
////////////////////////////////

void registerCommand(ExecutableCommand<?, CommandSender> command, String namespace) {
public void registerCommand(ExecutableCommand<?, CommandSender> command, String namespace) {
platform.preCommandRegistration(command.getName());

List<RegisteredCommand> registeredCommandInformation = RegisteredCommand.fromExecutableCommand(command, namespace);
Expand Down Expand Up @@ -431,6 +443,10 @@ public Predicate<Source> generateBrigadierRequirements(CommandPermission permiss
};
}

////////////////////////////////
// SECTION: Brigadier Helpers //
////////////////////////////////

public void writeDispatcherToFile() {
File file = CommandAPI.getConfiguration().getDispatcherFile();
if (file != null) {
Expand All @@ -452,7 +468,7 @@ public void writeDispatcherToFile() {
}
}

LiteralCommandNode<Source> namespaceNode(LiteralCommandNode<Source> original, String namespace) {
public LiteralCommandNode<Source> namespaceNode(LiteralCommandNode<Source> original, String namespace) {
// Adapted from a section of `CraftServer#syncCommands`
LiteralCommandNode<Source> clone = new LiteralCommandNode<>(
namespace + ":" + original.getLiteral(),
Expand All @@ -469,6 +485,54 @@ LiteralCommandNode<Source> namespaceNode(LiteralCommandNode<Source> original, St
return clone;
}

public static <Source> Map<String, CommandNode<Source>> getCommandNodeChildren(CommandNode<Source> target) {
try {
return (Map<String, CommandNode<Source>>) commandNodeChildren.get(target);
} catch (IllegalAccessException e) {
throw new IllegalStateException("This shouldn't happen. The field should be accessible.", e);
}
}

public static <Source> void setCommandNodeChildren(CommandNode<Source> target, Map<String, CommandNode<Source>> children) {
try {
commandNodeChildren.set(target, children);
} catch (IllegalAccessException e) {
throw new IllegalStateException("This shouldn't happen. The field should be accessible.", e);
}
}

public static <Source> Map<String, LiteralCommandNode<Source>> getCommandNodeLiterals(CommandNode<Source> target) {
try {
return (Map<String, LiteralCommandNode<Source>>) commandNodeLiterals.get(target);
} catch (IllegalAccessException e) {
throw new IllegalStateException("This shouldn't happen. The field should be accessible.", e);
}
}

public static <Source> void setCommandNodeLiterals(CommandNode<Source> target, Map<String, LiteralCommandNode<Source>> literals) {
try {
commandNodeLiterals.set(target, literals);
} catch (IllegalAccessException e) {
throw new IllegalStateException("This shouldn't happen. The field should be accessible.", e);
}
}

public static <Source> Map<String, ArgumentCommandNode<Source, ?>> getCommandNodeArguments(CommandNode<Source> target) {
try {
return (Map<String, ArgumentCommandNode<Source, ?>>) commandNodeArguments.get(target);
} catch (IllegalAccessException e) {
throw new IllegalStateException("This shouldn't happen. The field should be accessible.", e);
}
}

public static <Source> void setCommandNodeArguments(CommandNode<Source> target, Map<String, ArgumentCommandNode<Source, ?>> arguments) {
try {
commandNodeArguments.set(target, arguments);
} catch (IllegalAccessException e) {
throw new IllegalStateException("This shouldn't happen. The field should be accessible.", e);
}
}

////////////////////////////////
// SECTION: Parsing arguments //
////////////////////////////////
Expand Down Expand Up @@ -522,7 +586,7 @@ public static <CommandSource> String getRawArgumentInput(CommandContext<CommandS
* @return an CommandArguments object which can be used in (sender, args) ->
* @throws CommandSyntaxException If an argument is improperly formatted and cannot be parsed
*/
CommandArguments argsToCommandArgs(CommandContext<Source> cmdCtx, List<Argument> args) throws CommandSyntaxException {
public CommandArguments argsToCommandArgs(CommandContext<Source> cmdCtx, List<Argument> args) throws CommandSyntaxException {
// Array for arguments for executor
List<Object> argList = new ArrayList<>();

Expand Down Expand Up @@ -564,7 +628,7 @@ CommandArguments argsToCommandArgs(CommandContext<Source> cmdCtx, List<Argument>
* @return the Argument's corresponding object
* @throws CommandSyntaxException when the input for the argument isn't formatted correctly
*/
Object parseArgument(CommandContext<Source> cmdCtx, String key, Argument value, CommandArguments previousArgs) throws CommandSyntaxException {
public Object parseArgument(CommandContext<Source> cmdCtx, String key, Argument value, CommandArguments previousArgs) throws CommandSyntaxException {
if (value.isListed()) {
return value.parseArgument(cmdCtx, key, previousArgs);
} else {
Expand Down
Loading

0 comments on commit 13bf2c4

Please sign in to comment.