Brigadier Command Support (#8235)
Adds the ability for plugins to register their own brigadier commands --------- Co-authored-by: Jake Potrebic <jake.m.potrebic@gmail.com> Co-authored-by: Jason Penilla <11360596+jpenilla@users.noreply.github.com> Co-authored-by: Bjarne Koll <lynxplay101@gmail.com>
This commit is contained in:
parent
447f9a1e16
commit
b98d20a8ac
30 changed files with 5195 additions and 515 deletions
|
@ -2,7 +2,6 @@ version = "1.0.0-SNAPSHOT"
|
|||
|
||||
dependencies {
|
||||
compileOnly(project(":paper-api"))
|
||||
compileOnly(project(":paper-mojangapi"))
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
|
|
|
@ -8,5 +8,8 @@ public final class TestPlugin extends JavaPlugin implements Listener {
|
|||
@Override
|
||||
public void onEnable() {
|
||||
this.getServer().getPluginManager().registerEvents(this, this);
|
||||
|
||||
// io.papermc.testplugin.brigtests.Registration.registerViaOnEnable(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ public class TestPluginBootstrap implements PluginBootstrap {
|
|||
|
||||
@Override
|
||||
public void bootstrap(@NotNull BootstrapContext context) {
|
||||
// io.papermc.testplugin.brigtests.Registration.registerViaBootstrap(context);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
package io.papermc.testplugin.brigtests;
|
||||
|
||||
import com.mojang.brigadier.Command;
|
||||
import io.papermc.paper.command.brigadier.BasicCommand;
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import io.papermc.paper.command.brigadier.Commands;
|
||||
import io.papermc.paper.command.brigadier.argument.ArgumentTypes;
|
||||
import io.papermc.paper.command.brigadier.argument.range.DoubleRangeProvider;
|
||||
import io.papermc.paper.plugin.bootstrap.BootstrapContext;
|
||||
import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager;
|
||||
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
|
||||
import io.papermc.testplugin.brigtests.example.ExampleAdminCommand;
|
||||
import io.papermc.testplugin.brigtests.example.MaterialArgumentType;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.defaults.BukkitCommand;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public final class Registration {
|
||||
|
||||
private Registration() {
|
||||
}
|
||||
|
||||
public static void registerViaOnEnable(final JavaPlugin plugin) {
|
||||
registerLegacyCommands(plugin);
|
||||
registerViaLifecycleEvents(plugin);
|
||||
}
|
||||
|
||||
private static void registerViaLifecycleEvents(final JavaPlugin plugin) {
|
||||
final LifecycleEventManager<Plugin> lifecycleManager = plugin.getLifecycleManager();
|
||||
lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS, event -> {
|
||||
final Commands commands = event.registrar();
|
||||
// ensure plugin commands override
|
||||
commands.register(Commands.literal("tag")
|
||||
.executes(ctx -> {
|
||||
ctx.getSource().getSender().sendPlainMessage("overriden command");
|
||||
return Command.SINGLE_SUCCESS;
|
||||
})
|
||||
.build(),
|
||||
null,
|
||||
Collections.emptyList()
|
||||
);
|
||||
});
|
||||
|
||||
lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS.newHandler(event -> {
|
||||
final Commands commands = event.registrar();
|
||||
commands.register(plugin.getPluginMeta(), Commands.literal("root_command")
|
||||
.then(Commands.literal("sub_command")
|
||||
.requires(source -> source.getSender().hasPermission("testplugin.test"))
|
||||
.executes(ctx -> {
|
||||
ctx.getSource().getSender().sendPlainMessage("root_command sub_command");
|
||||
return Command.SINGLE_SUCCESS;
|
||||
})).build(),
|
||||
null,
|
||||
Collections.emptyList()
|
||||
);
|
||||
|
||||
commands.register(plugin.getPluginMeta(), "example", "test", Collections.emptyList(), new BasicCommand() {
|
||||
@Override
|
||||
public void execute(@NotNull final CommandSourceStack commandSourceStack, final @NotNull String[] args) {
|
||||
System.out.println(Arrays.toString(args));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Collection<String> suggest(final @NotNull CommandSourceStack commandSourceStack, final @NotNull String[] args) {
|
||||
System.out.println(Arrays.toString(args));
|
||||
return List.of("apple", "banana");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
commands.register(plugin.getPluginMeta(), Commands.literal("water")
|
||||
.requires(source -> {
|
||||
System.out.println("isInWater check");
|
||||
return source.getExecutor().isInWater();
|
||||
})
|
||||
.executes(ctx -> {
|
||||
ctx.getSource().getExecutor().sendMessage("You are in water!");
|
||||
return Command.SINGLE_SUCCESS;
|
||||
}).then(Commands.literal("lava")
|
||||
.requires(source -> {
|
||||
System.out.println("isInLava check");
|
||||
if (source.getExecutor() != null) {
|
||||
return source.getExecutor().isInLava();
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.executes(ctx -> {
|
||||
ctx.getSource().getExecutor().sendMessage("You are in lava!");
|
||||
return Command.SINGLE_SUCCESS;
|
||||
})).build(),
|
||||
null,
|
||||
Collections.emptyList());
|
||||
|
||||
|
||||
ExampleAdminCommand.register(plugin, commands);
|
||||
}).priority(10));
|
||||
}
|
||||
|
||||
private static void registerLegacyCommands(final JavaPlugin plugin) {
|
||||
plugin.getServer().getCommandMap().register("fallback", new BukkitCommand("hi", "cool hi command", "<>", List.of("hialias")) {
|
||||
@Override
|
||||
public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) {
|
||||
sender.sendMessage("hi");
|
||||
return true;
|
||||
}
|
||||
});
|
||||
plugin.getServer().getCommandMap().register("fallback", new BukkitCommand("cooler-command", "cool hi command", "<>", List.of("cooler-command-alias")) {
|
||||
@Override
|
||||
public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) {
|
||||
sender.sendMessage("hi");
|
||||
return true;
|
||||
}
|
||||
});
|
||||
plugin.getServer().getCommandMap().getKnownCommands().values().removeIf((command) -> {
|
||||
return command.getName().equals("hi");
|
||||
});
|
||||
}
|
||||
|
||||
public static void registerViaBootstrap(final BootstrapContext context) {
|
||||
final LifecycleEventManager<BootstrapContext> lifecycleManager = context.getLifecycleManager();
|
||||
lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS, event -> {
|
||||
final Commands commands = event.registrar();
|
||||
commands.register(Commands.literal("material")
|
||||
.then(Commands.literal("item")
|
||||
.then(Commands.argument("mat", MaterialArgumentType.item())
|
||||
.executes(ctx -> {
|
||||
ctx.getSource().getSender().sendPlainMessage(ctx.getArgument("mat", Material.class).name());
|
||||
return Command.SINGLE_SUCCESS;
|
||||
})
|
||||
)
|
||||
).then(Commands.literal("block")
|
||||
.then(Commands.argument("mat", MaterialArgumentType.block())
|
||||
.executes(ctx -> {
|
||||
ctx.getSource().getSender().sendPlainMessage(ctx.getArgument("mat", Material.class).name());
|
||||
return Command.SINGLE_SUCCESS;
|
||||
})
|
||||
)
|
||||
)
|
||||
.build(),
|
||||
null,
|
||||
Collections.emptyList()
|
||||
);
|
||||
});
|
||||
|
||||
lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS.newHandler(event -> {
|
||||
final Commands commands = event.registrar();
|
||||
commands.register(Commands.literal("heya")
|
||||
.then(Commands.argument("range", ArgumentTypes.doubleRange())
|
||||
.executes((ct) -> {
|
||||
ct.getSource().getSender().sendPlainMessage(ct.getArgument("range", DoubleRangeProvider.class).range().toString());
|
||||
return 1;
|
||||
})
|
||||
).build(),
|
||||
null,
|
||||
Collections.emptyList()
|
||||
);
|
||||
}).priority(10));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package io.papermc.testplugin.brigtests.example;
|
||||
|
||||
import com.mojang.brigadier.ImmutableStringReader;
|
||||
import com.mojang.brigadier.Message;
|
||||
import com.mojang.brigadier.exceptions.CommandExceptionType;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import io.papermc.paper.command.brigadier.MessageComponentSerializer;
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
public class ComponentCommandExceptionType implements CommandExceptionType {
|
||||
|
||||
private final Message message;
|
||||
|
||||
public ComponentCommandExceptionType(final Component message) {
|
||||
this.message = MessageComponentSerializer.message().serialize(message);
|
||||
}
|
||||
|
||||
public CommandSyntaxException create() {
|
||||
return new CommandSyntaxException(this, this.message);
|
||||
}
|
||||
|
||||
public CommandSyntaxException createWithContext(final ImmutableStringReader reader) {
|
||||
return new CommandSyntaxException(this, this.message, reader.getString(), reader.getCursor());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package io.papermc.testplugin.brigtests.example;
|
||||
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import io.papermc.paper.command.brigadier.Commands;
|
||||
import io.papermc.paper.command.brigadier.argument.SignedMessageResolver;
|
||||
import io.papermc.paper.command.brigadier.argument.ArgumentTypes;
|
||||
import io.papermc.paper.command.brigadier.argument.resolvers.BlockPositionResolver;
|
||||
import io.papermc.paper.command.brigadier.argument.resolvers.selector.PlayerSelectorArgumentResolver;
|
||||
import io.papermc.paper.math.BlockPosition;
|
||||
import io.papermc.testplugin.TestPlugin;
|
||||
import net.kyori.adventure.chat.ChatType;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockState;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ExampleAdminCommand {
|
||||
|
||||
public static void register(JavaPlugin plugin, Commands commands) {
|
||||
final LiteralArgumentBuilder<CommandSourceStack> adminBuilder = Commands.literal("admin")
|
||||
.executes((ct) -> {
|
||||
ct.getSource().getSender().sendPlainMessage("root admin");
|
||||
return 1;
|
||||
})
|
||||
.then(
|
||||
Commands.literal("tp")
|
||||
.then(
|
||||
Commands.argument("player", ArgumentTypes.player()).executes((source) -> {
|
||||
CommandSourceStack sourceStack = source.getSource();
|
||||
Player resolved = source.getArgument("player", PlayerSelectorArgumentResolver.class).resolve(sourceStack).get(0);
|
||||
|
||||
if (resolved == source.getSource().getExecutor()) {
|
||||
source.getSource().getExecutor().sendMessage(Component.text("Can't teleport to self!"));
|
||||
return 0;
|
||||
}
|
||||
Entity entity = source.getSource().getExecutor();
|
||||
if (entity != null) {
|
||||
entity.teleport(resolved);
|
||||
}
|
||||
|
||||
return 1;
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(
|
||||
Commands.literal("tp-self")
|
||||
.executes((cmd) -> {
|
||||
if (cmd.getSource().getSender() instanceof Player player) {
|
||||
player.teleport(cmd.getSource().getLocation());
|
||||
}
|
||||
|
||||
return com.mojang.brigadier.Command.SINGLE_SUCCESS;
|
||||
})
|
||||
)
|
||||
.then(
|
||||
Commands.literal("broadcast")
|
||||
.then(
|
||||
Commands.argument("message", ArgumentTypes.component()).executes((source) -> {
|
||||
Component message = source.getArgument("message", Component.class);
|
||||
Bukkit.broadcast(message);
|
||||
return 1;
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(
|
||||
Commands.literal("ice_cream").then(
|
||||
Commands.argument("type", new IceCreamTypeArgument()).executes((context) -> {
|
||||
IceCreamType argumentResponse = context.getArgument("type", IceCreamType.class); // Gets the raw argument
|
||||
context.getSource().getSender().sendMessage(Component.text("You like: " + argumentResponse));
|
||||
return 1;
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(
|
||||
Commands.literal("execute")
|
||||
.redirect(commands.getDispatcher().getRoot().getChild("execute"))
|
||||
)
|
||||
.then(
|
||||
Commands.literal("signed_message").then(
|
||||
Commands.argument("msg", ArgumentTypes.signedMessage()).executes((context) -> {
|
||||
SignedMessageResolver argumentResponse = context.getArgument("msg", SignedMessageResolver.class); // Gets the raw argument
|
||||
|
||||
// This is a better way of getting signed messages, includes the concept of "disguised" messages.
|
||||
argumentResponse.resolveSignedMessage("msg", context)
|
||||
.thenAccept((signedMsg) -> {
|
||||
context.getSource().getSender().sendMessage(signedMsg, ChatType.SAY_COMMAND.bind(Component.text("STATIC")));
|
||||
});
|
||||
|
||||
return 1;
|
||||
})
|
||||
)
|
||||
)
|
||||
.then(
|
||||
Commands.literal("setblock").then(
|
||||
Commands.argument("block", ArgumentTypes.blockState())
|
||||
.then(Commands.argument("pos", ArgumentTypes.blockPosition())
|
||||
.executes((context) -> {
|
||||
CommandSourceStack sourceStack = context.getSource();
|
||||
BlockPosition position = context.getArgument("pos", BlockPositionResolver.class).resolve(sourceStack);
|
||||
BlockState state = context.getArgument("block", BlockState.class);
|
||||
|
||||
// TODO: better block state api here? :thinking:
|
||||
Block block = context.getSource().getLocation().getWorld().getBlockAt(position.blockX(), position.blockY(), position.blockZ());
|
||||
block.setType(state.getType());
|
||||
block.setBlockData(state.getBlockData());
|
||||
|
||||
return 1;
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
commands.register(plugin.getPluginMeta(), adminBuilder.build(), "Cool command showcasing what you can do!", List.of("alias_for_admin_that_you_shouldnt_use", "a"));
|
||||
|
||||
|
||||
Bukkit.getCommandMap().register(
|
||||
"legacy",
|
||||
new Command("legacy_command") {
|
||||
@Override
|
||||
public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException {
|
||||
return List.of(String.join(" ", args));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Bukkit.getCommandMap().register(
|
||||
"legacy",
|
||||
new Command("legacy_fail") {
|
||||
@Override
|
||||
public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException {
|
||||
return List.of(String.join(" ", args));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package io.papermc.testplugin.brigtests.example;
|
||||
|
||||
public enum IceCreamType {
|
||||
VANILLA,
|
||||
CHOCOLATE,
|
||||
BLUE_MOON,
|
||||
STRAWBERRY,
|
||||
WHOLE_MILK
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package io.papermc.testplugin.brigtests.example;
|
||||
|
||||
import com.mojang.brigadier.Message;
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import io.papermc.paper.command.brigadier.MessageComponentSerializer;
|
||||
import io.papermc.paper.command.brigadier.argument.CustomArgumentType;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class IceCreamTypeArgument implements CustomArgumentType.Converted<IceCreamType, String> {
|
||||
|
||||
@Override
|
||||
public @NotNull IceCreamType convert(String nativeType) throws CommandSyntaxException {
|
||||
try {
|
||||
return IceCreamType.valueOf(nativeType.toUpperCase());
|
||||
} catch (Exception e) {
|
||||
Message message = MessageComponentSerializer.message().serialize(Component.text("Invalid species %s!".formatted(nativeType), NamedTextColor.RED));
|
||||
|
||||
throw new CommandSyntaxException(new SimpleCommandExceptionType(message), message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ArgumentType<String> getNativeType() {
|
||||
return StringArgumentType.word();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S> CompletableFuture<Suggestions> listSuggestions(CommandContext<S> context, SuggestionsBuilder builder) {
|
||||
for (IceCreamType species : IceCreamType.values()) {
|
||||
builder.suggest(species.name(), MessageComponentSerializer.message().serialize(Component.text("COOL! TOOLTIP!", NamedTextColor.GREEN)));
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(
|
||||
builder.build()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package io.papermc.testplugin.brigtests.example;
|
||||
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import io.papermc.paper.command.brigadier.argument.CustomArgumentType;
|
||||
import io.papermc.paper.command.brigadier.argument.ArgumentTypes;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
import org.bukkit.Keyed;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Registry;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import static net.kyori.adventure.text.Component.translatable;
|
||||
|
||||
public class MaterialArgumentType implements CustomArgumentType.Converted<Material, NamespacedKey> {
|
||||
|
||||
private static final ComponentCommandExceptionType ERROR_INVALID = new ComponentCommandExceptionType(translatable("argument.id.invalid"));
|
||||
|
||||
private final Predicate<Material> check;
|
||||
|
||||
private MaterialArgumentType(Predicate<Material> check) {
|
||||
this.check = check;
|
||||
}
|
||||
|
||||
public static MaterialArgumentType item() {
|
||||
return new MaterialArgumentType(Material::isItem);
|
||||
}
|
||||
|
||||
public static MaterialArgumentType block() {
|
||||
return new MaterialArgumentType(Material::isBlock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Material convert(final @NotNull NamespacedKey nativeType) throws CommandSyntaxException {
|
||||
final Material material = Registry.MATERIAL.get(nativeType);
|
||||
if (material == null) {
|
||||
throw ERROR_INVALID.create();
|
||||
}
|
||||
if (!this.check.test(material)) {
|
||||
throw ERROR_INVALID.create();
|
||||
}
|
||||
return material;
|
||||
}
|
||||
|
||||
static boolean matchesSubStr(String remaining, String candidate) {
|
||||
for(int i = 0; !candidate.startsWith(remaining, i); ++i) {
|
||||
i = candidate.indexOf('_', i);
|
||||
if (i < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ArgumentType<NamespacedKey> getNativeType() {
|
||||
return ArgumentTypes.namespacedKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull <S> CompletableFuture<Suggestions> listSuggestions(final @NotNull CommandContext<S> context, final @NotNull SuggestionsBuilder builder) {
|
||||
final Stream<Material> stream = StreamSupport.stream(Registry.MATERIAL.spliterator(), false);
|
||||
final String remaining = builder.getRemaining();
|
||||
boolean containsColon = remaining.indexOf(':') > -1;
|
||||
stream.filter(this.check)
|
||||
.map(Keyed::key)
|
||||
.forEach(key -> {
|
||||
final String keyAsString = key.asString();
|
||||
if (containsColon) {
|
||||
if (matchesSubStr(remaining, keyAsString)) {
|
||||
builder.suggest(keyAsString);
|
||||
}
|
||||
} else if (matchesSubStr(remaining, key.namespace()) || "minecraft".equals(key.namespace()) && matchesSubStr(remaining, key.value())) {
|
||||
builder.suggest(keyAsString);
|
||||
}
|
||||
});
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue