brigadier icon indicating copy to clipboard operation
brigadier copied to clipboard

[Bug] Merging of sibling nodes with same name is faulty

Open Marcono1234 opened this issue 7 years ago • 3 comments

It appears it is intended that sibling nodes with the same name exist (see this comment). However the way they are treated is in my opinion rather unintuitive.

The method CommandNode.addChild(CommandNode<S>) replaces the command of the existing node and adds the children of the new node to the existing one. This is, at least from my point of view, not the wanted behavior since

  • The old command is lost
  • Other attributes of the new node like the requirement are lost

It might be best to either remove this merging and allow siblings with the same name, or to prevent them when building.

Example code

CommandDispatcher<Object> dispatcher = new CommandDispatcher<>();
    
dispatcher.register(
    literal("foo")
    .then(
        argument("bar", word())
        .executes(c -> {
            System.out.println("string");
            return 1;
        })
    )
    .then(
        argument("bar", bool())
        .executes(c -> {
            System.out.println("bool");
            return 1;
        })
    )
);

dispatcher.execute("foo not_a_bool", null);

Marcono1234 avatar Oct 09 '18 17:10 Marcono1234

Names must be unique, so we can't allow multiple with the same name. It's used in a variety of places as a key, such as pathing in the tree, serialization, or simply getting values out of the context.

Merging is supported as a design principle (which I really must write down somewhere!). It must be supported to "enhance" a command after it has already been registered, to allow mods to easily extend commands in Minecraft (and I'm sure other applications/games will find that useful too).

That said, merging must be made smarter - I totally agree with this. We should probably do some sanity checking when a merge happens, and throw an exception if it's nonsense - if anything other than the children changed. For example in your code, the second .then(argument("bar"), bool()) should throw an exception as the very type of the argument has changed, and that should not be allowed.

Dinnerbone avatar Oct 14 '18 18:10 Dinnerbone

What do you think about having separate methods for this modification instead? For example ArgumentCommand could have the methods

  • addArgument(ArgumentType<T>) Adds the given argument type, which has to have the same type as the argument node. Argument types would then be stored in a list and the respective methods would go through the other arguments in this list if one fails.
  • setArguments(List<ArgumentType<T>>) Replaces all currently set argument types.

Methods like these might be less suprising than runtime exceptions which are thrown when the merge fails and they already define restrictions during compile time. Additionally this implementation might be less error prone since there are a lot of cases you have to consider when determining if a merge is valid.

Marcono1234 avatar Oct 14 '18 23:10 Marcono1234

A solution I can think of would be :

  • Allow merging literal arguments with the same name
  • Allow merging required arguments with the same name and same type
  • Prevent merging of required arguments of different types

Exemple :

// Case 1 :
dispatcher.register(
    literal("foo")
    .then(
        literal("bar")
        .executes(c -> {
            System.out.println("string");
            return 1;
        })
    )
    .then(
        literal("bar") // Same name, literal -> Merge
        .then(literal("baz"))
    )
);
// Case 2 :
dispatcher.register(
    literal("foo")
    .then(
        argument("bar", word())
        .executes(c -> {
            System.out.println("string");
            return 1;
        })
    )
    .then(
        argument("bar", word()) // Same name, same type -> Merge
        .then(literal("baz"))
    )
);
// Case 3 :
dispatcher.register(
    literal("foo")
    .then(
        argument("bar", word())
        .executes(c -> {
            System.out.println("string");
            return 1;
        })
    )
    .then(
        argument("bar", bool()) // Same name, different type -> Exception
        .executes(c -> {
            System.out.println("bool");
            return 1;
        })
    )
);

JesFot avatar Dec 01 '19 06:12 JesFot