Feature: Add API to allow native Brigadier node merging behavior when registering BrigadierCommand
#1045
Labels
type: feature
New feature or request
BrigadierCommand
#1045
Summary
This issue comes from a discussion on the Velocity discord, though hopefully all the ideas from over there will be present here.
When
LiteralArgumentBuilder
nodes that have the same name are registered to a BrigadierCommandDispatcher
, they are merged together. However, when you wrap those nodes in Velocity'sBrigadierCommand
and register them usingCommandManager
, one node overrides the other, so only one is executable.BrigadierCommand
s overriding each other when they have the same name is currently intended behavior. However, it would be nice if there was an API method that made it possible to use theCommandDispatcher
logic where the commands are merged together.Also, on a bit of a side note, it would probably be useful if Velocity added this behavior to their documentation. Coming from a Brigadier standpoint, I expected the commands to be merged, so I was very confused when that didn't happen. That confusion could be avoided if The Command API page mentioned when a command is registered, previous commands with the same name are removed.
How exactly is Brigadier and Velocity's behavior different?
Click to expand
When Brigadier registers nodes that have the same name, it merges them together. On the other hand, Velocity will unregister the first node and replace it with the second. To see this in action, consider this Brigadier code (no Velocity other than
CommandSource
):This code defines two
LiteralArgumentBuilder
nodes --literal1
andliteral2
-- which represent the commands/test
and/test <argument>
respectively. These nodes are both registered to a BrigadierCommandDispatcher
using theCommandDispatcher#register
method. After running this code, theCommandDispatcher
's structure looks like this:What is important to notice is that both the
test
child and theargument
child are marked"executable": true
. Even though the definitions of these two paths were registered separately,CommandDispatcher#register
merged them together so that both paths were executable.Now, consider this code using Velocity's
BrigadierCommand
andCommandManager
:This code uses the same
LiteralArgumentBuilder
nodes, it just wraps them inBrigadierCommand
, and usesCommandManager#register
to register them to Velocity'sCommandDispatcher
. However, this code does not produce the same structure as before. Here is the structure that results from this code:Here, only the
argument
child is executable, andtest
is not executable. You can see this in-game. Only the/test <argument>
command works, not/test
.This is the intended behavior. When Velocity registers a
LiteralCommandNode
, it uses this code:Compare that to
CommandDispatcher
:Before Velocity registers the
/test <argument>
node, it usesremoveChildByName
to get rid of any nodes with thetest
name, hence why/test
becomes unexecutable. In contrast,CommandDispatcher
immediately usesaddChild
, which merges/test <argument>
with the old/test
node. So, it is not a bug that these behaviors are different. That is simply how the methods work.Why do I want to register separate
LiteralArgumentBuilder
nodes that have the same name?Click to expand
The previous section explained why the following Velocity code results with only the
/test <argument>
command executable, even though a/test
command is also registered.This is intended behavior, and there is a perfectly reasonable alternative method that makes both
/test
and/test <argument>
work. Here's that code:Instead of registering the
/test
and/test <argument>
commands separately, Brigadier allows you to merge them together into a singleLiteralArgumentBuilder
. Velocity can then register this as a singleBrigadierCommand
, and there is no problem registering these two command paths.So, a good question is, "Who would ever register separate LiteralArgumentBuilder nodes that have the same name?" Since I'm creating this issue, the answer is obviously me :). I'm currently working on porting a Spigot plugin called CommandAPI to Velocity. The CommandAPI allows developers to create and register Brigadier commands. For example, the
/test
and/test <argument>
commands could be created like so:The CommandAPI processes these
CommandAPICommand
objects and turns them intoLiteralArgumentBuilder
nodes. Note that two command paths are declared separately, hence why I want to register separateLiteralArgumentBuilder
nodes that have the same name.Alternate solutions (Velocity doesn't change)
Click to expand
This section describes alternate solutions to my problem where Velocity keeps the same behavior.
First, my problem. Once the
CommandAPICommand
objects are turned intoLiteralArgumentBuilder
nodes, theregisterCommandNode
method is called. Here is what that originally looked like:This doesn't work when two nodes with the same name are passed, since when registering a
BrigadierCommand
with the same name, Velocity will unregister old nodes with that name, as discussed.One solution I thought of looks like this:
A separate
CommandDispatcher mergeDispatcher
keeps track of all the nodes registered by theCommandAPI
. If there is ever a node with the same name, theregister
method ofmergeDispatcher
is easily accessible to get the merging behavior. I can then take the result of merging the nodes together, and give that toBrigadierCommand
instead. Even when Velocity unregisters the old/test
command, I have the merged version, so/test
and/test <argument>
are registered correctly.One downside of this method is that it keeps track of a duplicate
CommandDispatcher
structure for every command registered with the CommandAPI. This seems unnecessary when you could just merge the nodes in Velocity'sCommandDispatcher
. That's the second alternative solution:I want to merge the command directly into Velocity's
CommandDispatcher
, so just call theregister
method on Velocity'sCommandDispatcher
. The problem with this one is that you need reflection to access Velocity'sCommandDispatcher
. This is the solution I'm currently using, but it would be nice if reflection wasn't needed.Alternate solutions (Velocity does change) (Why this is a feature request)
Click to expand
I think it would be best if something like my original solution worked out:
Of course, as discussed, it doesn't currently work, so Velocity would need to change somehow.
My first proposal is to never remove the old nodes when registering new commands. That would change this code to be this:
Here, the call to
removeChildByName
is gone, avoiding the issue where the/test
command is unregistered before the/test <argument>
command is registered.There is some concern that this would cause problems when registering a
SimpleCommand
orRawCommand
. I'm not certain of the consequences, but I think it would be okay.SimpleCommand
andRawCommand
both have a single greedy argument. In my experience, when Brigadier registers two nodes in the same spot that accept the same input, the node that is registered first always gets precedence. So, in this scenario, I think registering, for instance, twoRawCommand
with the same name would mean only the first version would be runnable. This is reversed to the current behavior (where the second version would be runnable), but since it isn't documented I don't expect anyone depends on it. Again, I'm not certain this is what actually would happen, but I think it's worth looking into.If there are problems with
RawCommand
andSimpleCommand
, an alternative would be to provide an API method that allows this behavior only forBrigadierCommand
. Maybe aCommandManager#register(BrigadierCommand command, boolean merge)
method that could be used like so:The API method can really be anything IMO. I would appreciate any way to be able to use the native Brigadier merging behavior, and I think it would be a good addition to Velocity's API.
The text was updated successfully, but these errors were encountered: