Open willkroboth opened 1 year ago
Ah, I found my workaround.
I didn't realize before that the CommandDispatcher nodes don't have to form a tree. A child can have multiple parents. So, my test example can work like so:
CommandDispatcher<Object> dispatcher = new CommandDispatcher<>();
CommandNode<Object> baseCommand = literal("command").build();
// Add first branch
CommandNode<Object> firstBranchTermination = argument("number", IntegerArgumentType.integer(10, 20)).build();
baseCommand.addChild(literal("high").then(firstBranchTermination).build());
// Redirect other branches to first branch
CommandNode<Object> otherBranchTermination = argument("number", IntegerArgumentType.integer(0, 10)).build();
baseCommand.addChild(literal("low").then(otherBranchTermination).build());
// Actually build final arguments
CommandNode<Object> finalArgument = argument("string", StringArgumentType.greedyString())
.executes(context -> {
int number = context.getArgument("number", Integer.class);
System.out.println("The given number is " + number);
String stringArgument = context.getArgument("string", String.class);
System.out.println("The string was given as " + stringArgument);
return 1;
})
.build();
// Add final arguments to both branches
firstBranchTermination.addChild(finalArgument);
otherBranchTermination.addChild(finalArgument);
// Register command
dispatcher.getRoot().addChild(baseCommand);
// Test cases
dispatcher.execute("command high 11 some text", new Object());
dispatcher.execute("command low 5 some text", new Object());
This method can also work with cycles. At least, suggestions and executing a CommandDispatcher
with nodes that cycle seem to work fine. For example, this setup:
LiteralCommandNode<Source> command = LiteralArgumentBuilder.<Source>literal("command").build();
ArgumentCommandNode<Source, String> flagName = RequiredArgumentBuilder
.<Source, String>argument("flag", StringArgumentType.word()).build();
ArgumentCommandNode<Source, Integer> flagValue = RequiredArgumentBuilder
.<Source, Integer>argument("value", IntegerArgumentType.integer(0, 255))
.executes(context -> {
// Use the flags...
}).build();
flagName.addChild(flagValue);
flagValue.addChild(flagName);
command.addChild(flagName);
dispatcher.getRoot().addChild(command);
// command looks like `/command <flag> <value> <flag> <value> ...`
In this command, the user can input any number of flag-value pairs. Executing the command is possible whenever they have typed in a value. Brigadier seems to handle this case fine, so nothing is lost from using cycling children rather than redirects.
However, as https://github.com/Mojang/brigadier/issues/105 mentions, Minecraft does not support cycles when it sends commands to the player in a packet. At least on a Paper server, trying to send a commands packet containing cycling children causes a stack overflow. While the format of the commands packet does not seem to prohibit a structure like this, the server code and I assume the Vanilla Minecraft client reject this setup. So, if you want to include argument cycles in your Minecraft command (and I do :P), you can't use redirects since the arguments disappear, and you can't use cycling children because the Vanilla client says no.
However, I have found a hacky workaround. The trick is that when Minecraft builds the Commands packet, it uses the map CommandNode.children
. However, when Brigadier parses a command, it uses the map CommandNode.arguments
. Usually, these maps are synced up, but the magic of reflection allows you to do (almost) whatever you want. This is what I did:
LiteralCommandNode<Source> command = LiteralArgumentBuilder.<Source>literal("command").build();
ArgumentCommandNode<Source, String> flagName = RequiredArgumentBuilder
.<Source, String>argument("flag", StringArgumentType.word()).build();
ArgumentCommandNode<Source, Integer> flagValue = RequiredArgumentBuilder
.<Source, Integer>argument("value", IntegerArgumentType.integer(0, 255))
// Use the flags...
})
.redirect(command)
.build();
ArgumentCommandNode<Source, Integer> flagValueCopy = flagValue.createBuilder().redirect(null).build();
flagValueCopy.addChild(flagName);
Map<String, CommandNode<Source>> children;
Map<String, ArgumentCommandNode<Source, ?>> arguments;
try {
Field commandNodeChildren = CommandNode.class.getDeclaredField("children");
commandNodeChildren.setAccessible(true);
children = (Map<String, CommandNode<Source>>) commandNodeChildren.get(flagName);
Field commandNodeArguments = CommandNode.class.getDeclaredField("arguments");
commandNodeArguments.setAccessible(true);
arguments = (Map<String, ArgumentCommandNode<Source, ?>>) commandNodeArguments.get(flagName);
} catch (ReflectiveOperationException exception) {
throw new RuntimeException(exception);
}
children.put(flagValue.getName(), flagValue);
arguments.put(flagValueCopy.getName(), flagValueCopy);
command.addChild(flagName);
platform.getBrigadierDispatcher().getRoot().addChild(command);
Based on the children
map, the command looks like: /command <name> <value> -> command
. So, when the server sends the command to a client, it gives them a redirect cycle. The client asks the server to execute commands, so it doesn't need to worry about commands disappearing after redirects, avoiding the issue I've presented here.
However, using reflection, I put a different node into the arguments
map. This way, the command looks like /command <name> <value> <name> <value>...
. So, when the server executes the command for the player, it uses a child cycle. That avoids the issue here where arguments disappear after redirects, and so a command can access all the arguments given (with just a bit more finagling that I've omitted).
So, it is possible to work around this issue. However, I don't think any solution involving reflection should be official :P. It would still be great to solve this issue via #142, or something else like that.
This is helpful for optional arguments
, it's strange that the brigadier command system refuse to pass the context by default.
This may well be the intended behavior, but I wanted to check because it feels like a bug. If this is intended, I was wondering what the workaround would be?
I encountered this problem when working generally with this sort of command structure:
I have some base command path, and I want to add a bunch of branches after that base command node. These branches should converge onto the same final arguments node structure. Abstractly, this is how I achieved that:
I setup
(base command) -> (first branch)
, then setup(base command) -> (branch) -> redirect to (firstBranch)
. Then, I add the final arguments onto the first branch. Since the other branches redirect to first branch, their parsing correctly continues on to the final arguments.It is important to note that the final arguments structure is built after the branches are built. That's why I redirect the other branches to the first branch. With that, I only need to add the final arguments to the first branch, and therefore I only need to keep track of this "representative" branch in my building code.
I only introduce this as context for the bug. If you think my real problem is that I'm building my command wrong, fair enough. I'm happy to go into more depth about my specific situation if the bug is actually intended behavior.
To show this problem, I set up this simple example:
Here's how that connects to my abstract structure before
There is a low branch that only accepts integers from 0 to 10, and a high branch that only accepts integers from 10 to 20. Both of these branches converge on greedy string, which has an executor that uses the given
"number"
and"string"
argument.So, the outcome I expect from running this code is that the two test cases at the end execute without failure
The first case that uses the high branch works as expected. However, the low branch case throws this exception:
When I use the redirected
low
branch, the"number"
argument is not present in theCommandContext
, hence the title of this issue. Even though the"number"
argument was parsed as5
, it is not available when the command executes. When a node redirects, the arguments defined before the redirected node disappear.I think the bug comes from this line in
CommandDispatcher
(The same problem likely occurs when commands fork due to similar code on line 248):https://github.com/Mojang/brigadier/blob/f20bede62a516a11a468d27d3f1adde2085762bc/src/main/java/com/mojang/brigadier/CommandDispatcher.java#L239
I don't know the reasoning behind the parsing and execution logic here, but I can comment on what I see happening here. I don't know why, but I can tell you how this bug happens.
When
"command low 5 some text"
is parsed, the resulting command context looks something like this:The original context parsed everything up to the redirected node. It maps the
"number"
argument to its value,5
. When parsing reached the redirect, it started a new child context to parse everything after the redirected node. This context maps the"string"
argument to its value,"some text"
. The child context also knows that it has a command to execute. Again, I don't know why, but this seems reasonable.Anyway, the line I linked to above is part of code responsible for using this
CommandContext
to actually execute a command. It looks through all the child contexts and handles all the redirects, especially if the context forks and the executor needs to be multiplied among many sources. The line I specifically linked to is responsible for figuring out the next context in this case with a child context that does not fork. Remember, that line is:So, the next context that will be evaluated is the child context, but copied to have the original context's source. So, now the context looks like this:
This context ends the chain, and it is executable, so the command is run using the current context. However, the current context only contains the
"string"
argument, not the"number"
argument. The"number"
argument only existed in the original context, and it was not copied to the child context. Hence, the"number"
argument does not exist while running the command, and I get theIllegalArgumentException
.The main reason why I think this is a bug is because it can be easily fixed. Instead of the current code that just copies the command source:
Using this line solves my issue:
Where
CommandContext#copyArguments
is defined like so:It seems likely to me that not copying the parent context's arguments is just a semantics error and not intended behavior.
Due to the lack of documentation for the
redirect
andfork
methods, I'm not sure what behavior to expect. It makes sense to me that prior arguments would be preserved after a redirect, so I assume this is a bug. Looking at the current uses of redirect and fork, I don't think my changes would cause different behavior. For example, Minecraft's/execute
command uses redirects to have a looping argument command structure. That means I could reuse the same arguments like so:Here, the
"targets"
argument is used twice, once as@a
and once as@e[type=!player,limit=1]
. I assume theCommandContext
in this situation would like the following:When the original context's fork is being evaluated,
"entities"
is@a
as expected. So, the redirect modifier should correctly select all the players as the next sources for the command. When merging the arguments for the newCommandContext
, the child context's arguments take precedence. So, the next context will look like this:Since the child context already had a value for
"entities"
, merging arguments from the parentCommandContext
doesn't do anything. Therefore, the redirect modifier correctly still processes@e[type=!player,limit=1]
as the next selector. Overall, merging the arguments doesn't affect old command structures, since if they expect an argument, it will always override the value possibly given to the parent context.So, I hope the fact that arguments are not copied to child contexts is a bug, because I think it would solve my problem. If it is a bug, I think it can be easily fixed without affecting old commands. It seems intuitive to me that arguments would be preserved after redirected nodes, but I can't find any documentation that suggests one way or the other.
If this is a bug, I can definitely make a PR for this. I'm not familiar with the code style of this repo yet, but I think I can figure it out with time. The changes shouldn't be too large; I mostly just need to figure out how to create a test for this.