Skip to content

Brigadier API : supports to redirect to the RootCommandNode #11647

@marcbal

Description

@marcbal

Is your feature request related to a problem?

I have a Brigadier command that has similar functionality to /execute, i.e. to conditionally run commands based on plugin data. Since I moved from using lucko's commodore to using the Lifecycle API to register Brigadier commands, my /execute clone is not able to register anymore when it contains a redirection to the root command node:

[LifecycleEventRunner] Could not run 'commands' lifecycle event handler from PandacubePaper vdev
java.lang.IllegalArgumentException: Unknown command node passed. Don't know how to unwrap this.
at io.papermc.paper.command.brigadier.ApiMirrorRootNode.convertFromPureBrigNode(ApiMirrorRootNode.java:144)~
at io.papermc.paper.command.brigadier.ApiMirrorRootNode.unwrapNode(ApiMirrorRootNode.java:93)~
at io.papermc.paper.command.brigadier.ApiMirrorRootNode.simpleUnwrap(ApiMirrorRootNode.java:249)~
at io.papermc.paper.command.brigadier.ApiMirrorRootNode.convertFromPureBrigNode(ApiMirrorRootNode.java:106)~
at io.papermc.paper.command.brigadier.ApiMirrorRootNode.unwrapNode(ApiMirrorRootNode.java:93)~
at io.papermc.paper.command.brigadier.ApiMirrorRootNode.convertFromPureBrigNode(ApiMirrorRootNode.java:155)~
at io.papermc.paper.command.brigadier.ApiMirrorRootNode.unwrapNode(ApiMirrorRootNode.java:93)~
at io.papermc.paper.command.brigadier.ApiMirrorRootNode.addChild(ApiMirrorRootNode.java:205)~
at io.papermc.paper.command.brigadier.PaperCommands.registerIntoDispatcher(PaperCommands.java:156)~
at io.papermc.paper.command.brigadier.PaperCommands.registerWithFlags(PaperCommands.java:102)~
at io.papermc.paper.command.brigadier.PaperCommands.register(PaperCommands.java:90)~
at io.papermc.paper.command.brigadier.PaperCommands.register(PaperCommands.java:85)~
at PandacubePaper-1668.jar/fr.pandacube.lib.paper.commands.PaperBrigadierCommand.lambda$register$6(PaperBrigadierCommand.java:169)~
at io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.lambda$callEvent$2(LifecycleEventRunner.java:68)~
at io.papermc.paper.plugin.lifecycle.event.types.PrioritizableLifecycleEventType.forEachHandler(PrioritizableLifecycleEventType.java:53)~
at io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.callEvent(LifecycleEventRunner.java:63)~
at io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.callReloadableRegistrarEvent(LifecycleEventRunner.java:107)~
at NMS.MinecraftServer.loadWorld0(MinecraftServer.java:678)~
at NMS.MinecraftServer.loadLevel(MinecraftServer.java:437)~
at NMS.dedicated.DedicatedServer.initServer(DedicatedServer.java:323)~
at NMS.MinecraftServer.runServer(MinecraftServer.java:1136)~
at NMS.MinecraftServer.lambda$spin$0(MinecraftServer.java:323)~
at java.base/java.lang.Thread.run(Thread.java:1583)~

It seems like the node unwrapper does not support encountering the RootCommandNode instance in the tree, since it’s not a subclass for LiteralCommandNode or ArgumentCommandNode:

+ private @NotNull CommandNode<CommandSourceStack> convertFromPureBrigNode(final CommandNode<CommandSourceStack> pureNode) {
+ /*
+ Logic for converting a node.
+ */
+ final CommandNode<CommandSourceStack> converted;
+ if (pureNode instanceof final LiteralCommandNode<CommandSourceStack> node) {
+ /*
+ Remap the literal node, we only have to account
+ for the redirect in this case.
+ */
+ converted = this.simpleUnwrap(node);
+ } else if (pureNode instanceof final ArgumentCommandNode<CommandSourceStack, ?> pureArgumentNode) {
+ final ArgumentType<?> pureArgumentType = pureArgumentNode.getType();
+ /*
+ Check to see if this argument type is a wrapped type, if so we know that
+ we can unwrap the node to get an NMS type.
+ */
+ if (pureArgumentType instanceof final CustomArgumentType<?, ?> customArgumentType) {
+ final SuggestionProvider<?> suggestionProvider;
+ try {
+ final Method listSuggestions = customArgumentType.getClass().getMethod("listSuggestions", CommandContext.class, SuggestionsBuilder.class);
+ if (listSuggestions.getDeclaringClass() != CustomArgumentType.class) {
+ suggestionProvider = customArgumentType::listSuggestions;
+ } else {
+ suggestionProvider = null;
+ }
+ } catch (final NoSuchMethodException ex) {
+ throw new IllegalStateException("Could not determine if the custom argument type " + customArgumentType + " overrides listSuggestions", ex);
+ }
+
+ converted = this.unwrapArgumentWrapper(pureArgumentNode, customArgumentType, customArgumentType.getNativeType(), suggestionProvider);
+ } else if (pureArgumentType instanceof final VanillaArgumentProviderImpl.NativeWrapperArgumentType<?,?> nativeWrapperArgumentType) {
+ converted = this.unwrapArgumentWrapper(pureArgumentNode, nativeWrapperArgumentType, nativeWrapperArgumentType, null); // "null" for suggestion provider so it uses the argument type's suggestion provider
+
+ /*
+ If it's not a wrapped type, it either has to be a primitive or an already
+ defined NMS type.
+ This method allows us to check if this is recognized by vanilla.
+ */
+ } else if (ArgumentTypeInfos.isClassRecognized(pureArgumentType.getClass())) {
+ // Allow any type of argument, as long as it's recognized by the client (but in most cases, this should be API only types)
+ // Previously we only allowed whitelisted types.
+ converted = this.simpleUnwrap(pureArgumentNode);
+ } else {
+ // Unknown argument type was passed
+ throw new IllegalArgumentException("Custom unknown argument type was passed, should be wrapped inside an CustomArgumentType.");
+ }
+ } else {
+ throw new IllegalArgumentException("Unknown command node passed. Don't know how to unwrap this.");
+ }
+

Describe the solution you'd like.

Being able to do this :

getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {
    event.registrar().register(literal("myexecute")
            .then(literal("run")
                    .redirect(event.registrar().getDispatcher().getRoot())
            )
            .build()
    );
});

Then run the following command in chat or command block: /myexecute run tp @p 100 70 100

Describe alternatives you've considered.

  • Take the conditionally run command into a StringArgument and run it through Bukkit.dispatchCommand() if conditions are met
  • Use reflection to add the feature myself:
getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {
    CommandDispatcher<CommandSourceStack> dispatcher = event.registrar().getDispatcher();
    RootCommandNode<CommandSourceStack> wrappedRoot = dispatcher.getRoot();
    ReflectClass<?> apiMirrorRootNodeClass = Reflect.ofClassOfInstance(wrappedRoot);
    try {
        RootCommandNode<?> unwrappedRoot = ((CommandDispatcher<?>) apiMirrorRootNodeClass.method("getDispatcher").invoke(wrappedRoot)).getRoot();

        Reflect.ofClass(CommandNode.class).field("unwrappedCached").setValue(wrappedRoot, unwrappedRoot);
        Reflect.ofClass(CommandNode.class).field("wrappedCached").setValue(unwrappedRoot, wrappedRoot);

    } catch (InvocationTargetException|IllegalAccessException|NoSuchMethodException|NoSuchFieldException e) {
        Log.severe("Unable to trick the Paper/Brigadier unwrapper to properly handle commands redirecting to root command node.", e);
    }
});

Other

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    scope: apistatus: acceptedDisputed bug is accepted as valid or Feature accepted as desired to be added.
    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions