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 <git@lynxplay.dev>
This commit is contained in:
Owen1212055 2024-05-11 16:30:30 -04:00
parent 8a5ea7e5cf
commit 567cc3f4b3
27 changed files with 5176 additions and 496 deletions

View file

@ -1,36 +0,0 @@
plugins {
`java-library`
`maven-publish`
}
java {
withSourcesJar()
withJavadocJar()
}
dependencies {
implementation(project(":paper-api"))
api("com.mojang:brigadier:1.0.18")
compileOnly("it.unimi.dsi:fastutil:8.5.6")
compileOnly("org.jetbrains:annotations:23.0.0")
testImplementation("junit:junit:4.13.2")
testImplementation("org.hamcrest:hamcrest-library:1.3")
testImplementation("org.ow2.asm:asm-tree:9.7")
}
configure<PublishingExtension> {
publications.create<MavenPublication>("maven") {
from(components["java"])
}
}
val scanJar = tasks.register("scanJarForBadCalls", io.papermc.paperweight.tasks.ScanJarForBadCalls::class) {
badAnnotations.add("Lio/papermc/paper/annotation/DoNotUse;")
jarToScan.set(tasks.jar.flatMap { it.archiveFile })
classpath.from(configurations.compileClasspath)
}
tasks.check {
dependsOn(scanJar)
}

View file

@ -1,14 +0,0 @@
package com.destroystokyo.paper.brigadier;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import java.util.function.Predicate;
/**
* Brigadier {@link Command}, {@link SuggestionProvider}, and permission checker for Bukkit {@link Command}s.
*
* @param <S> command source type
*/
public interface BukkitBrigadierCommand <S extends BukkitBrigadierCommandSource> extends Command<S>, Predicate<S>, SuggestionProvider<S> {
}

View file

@ -1,21 +0,0 @@
package com.destroystokyo.paper.brigadier;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.jetbrains.annotations.Nullable;
public interface BukkitBrigadierCommandSource {
@Nullable
Entity getBukkitEntity();
@Nullable
World getBukkitWorld();
@Nullable
Location getBukkitLocation();
CommandSender getBukkitSender();
}

View file

@ -1,72 +0,0 @@
package com.destroystokyo.paper.event.brigadier;
import com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource;
import com.mojang.brigadier.tree.RootCommandNode;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
/**
* Fired any time a Brigadier RootCommandNode is generated for a player to inform the client of commands.
* You may manipulate this CommandNode to change what the client sees.
*
* <p>This event may fire on login, world change, and permission rebuilds, by plugin request, and potentially future means.</p>
*
* <p>This event will fire before {@link org.bukkit.event.player.PlayerCommandSendEvent}, so no filtering has been done by
* other plugins yet.</p>
*
* <p>WARNING: This event will potentially (and most likely) fire twice! Once for Async, and once again for Sync.
* It is important that you check event.isAsynchronous() and event.hasFiredAsync() to ensure you only act once.
* If for some reason we are unable to send this asynchronously in the future, only the sync method will fire.</p>
*
* <p>Your logic should look like this:
* {@code if (event.isAsynchronous() || !event.hasFiredAsync()) { // do stuff }}</p>
*
* <p>If your logic is not safe to run asynchronously, only react to the synchronous version.</p>
*
* <p>This is a draft/experimental API and is subject to change.</p>
*/
@ApiStatus.Experimental
public class AsyncPlayerSendCommandsEvent <S extends BukkitBrigadierCommandSource> extends PlayerEvent {
private static final HandlerList handlers = new HandlerList();
private final RootCommandNode<S> node;
private final boolean hasFiredAsync;
public AsyncPlayerSendCommandsEvent(Player player, RootCommandNode<S> node, boolean hasFiredAsync) {
super(player, !Bukkit.isPrimaryThread());
this.node = node;
this.hasFiredAsync = hasFiredAsync;
}
/**
* Gets the full Root Command Node being sent to the client, which is mutable.
*
* @return the root command node
*/
public RootCommandNode<S> getCommandNode() {
return node;
}
/**
* Gets if this event has already fired asynchronously.
*
* @return whether this event has already fired asynchronously
*/
public boolean hasFiredAsync() {
return hasFiredAsync;
}
@NotNull
public HandlerList getHandlers() {
return handlers;
}
@NotNull
public static HandlerList getHandlerList() {
return handlers;
}
}

View file

@ -1,84 +0,0 @@
package com.destroystokyo.paper.event.brigadier;
import com.mojang.brigadier.suggestion.Suggestions;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
/**
* Called when sending {@link Suggestions} to the client. Will be called asynchronously if a plugin
* marks the {@link com.destroystokyo.paper.event.server.AsyncTabCompleteEvent} event handled asynchronously,
* otherwise called synchronously.
*/
public class AsyncPlayerSendSuggestionsEvent extends PlayerEvent implements Cancellable {
private static final HandlerList handlers = new HandlerList();
private boolean cancelled = false;
private Suggestions suggestions;
private final String buffer;
public AsyncPlayerSendSuggestionsEvent(Player player, Suggestions suggestions, String buffer) {
super(player, !Bukkit.isPrimaryThread());
this.suggestions = suggestions;
this.buffer = buffer;
}
/**
* Gets the input buffer sent to request these suggestions.
*
* @return the input buffer
*/
public String getBuffer() {
return buffer;
}
/**
* Gets the suggestions to be sent to client.
*
* @return the suggestions
*/
public Suggestions getSuggestions() {
return suggestions;
}
/**
* Sets the suggestions to be sent to client.
*
* @param suggestions suggestions
*/
public void setSuggestions(Suggestions suggestions) {
this.suggestions = suggestions;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isCancelled() {
return this.cancelled;
}
/**
* Cancels sending suggestions to the client.
* {@inheritDoc}
*/
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
@NotNull
public HandlerList getHandlers() {
return handlers;
}
@NotNull
public static HandlerList getHandlerList() {
return handlers;
}
}

View file

@ -1,168 +0,0 @@
package com.destroystokyo.paper.event.brigadier;
import com.destroystokyo.paper.brigadier.BukkitBrigadierCommand;
import com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource;
import com.mojang.brigadier.tree.ArgumentCommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;
import com.mojang.brigadier.tree.RootCommandNode;
import org.bukkit.command.Command;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.server.ServerEvent;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
/**
* Fired anytime the server synchronizes Bukkit commands to Brigadier.
*
* <p>Allows a plugin to control the command node structure for its commands.
* This is done at Plugin Enable time after commands have been registered, but may also
* run at a later point in the server lifetime due to plugins, a server reload, etc.</p>
*
* <p>This is a draft/experimental API and is subject to change.</p>
*/
@ApiStatus.Experimental
public class CommandRegisteredEvent<S extends BukkitBrigadierCommandSource> extends ServerEvent implements Cancellable {
private static final HandlerList handlers = new HandlerList();
private final String commandLabel;
private final Command command;
private final BukkitBrigadierCommand<S> brigadierCommand;
private final RootCommandNode<S> root;
private final ArgumentCommandNode<S, String> defaultArgs;
private LiteralCommandNode<S> literal;
private boolean rawCommand = false;
private boolean cancelled = false;
public CommandRegisteredEvent(String commandLabel, BukkitBrigadierCommand<S> brigadierCommand, Command command, RootCommandNode<S> root, LiteralCommandNode<S> literal, ArgumentCommandNode<S, String> defaultArgs) {
this.commandLabel = commandLabel;
this.brigadierCommand = brigadierCommand;
this.command = command;
this.root = root;
this.literal = literal;
this.defaultArgs = defaultArgs;
}
/**
* Gets the command label of the {@link Command} being registered.
*
* @return the command label
*/
public String getCommandLabel() {
return this.commandLabel;
}
/**
* Gets the {@link BukkitBrigadierCommand} for the {@link Command} being registered. This can be used
* as the {@link com.mojang.brigadier.Command command executor} or
* {@link com.mojang.brigadier.suggestion.SuggestionProvider} of a {@link com.mojang.brigadier.tree.CommandNode}
* to delegate to the {@link Command} being registered.
*
* @return the {@link BukkitBrigadierCommand}
*/
public BukkitBrigadierCommand<S> getBrigadierCommand() {
return this.brigadierCommand;
}
/**
* Gets the {@link Command} being registered.
*
* @return the {@link Command}
*/
public Command getCommand() {
return this.command;
}
/**
* Gets the {@link RootCommandNode} which is being registered to.
*
* @return the {@link RootCommandNode}
*/
public RootCommandNode<S> getRoot() {
return this.root;
}
/**
* Gets the Bukkit APIs default arguments node (greedy string), for if
* you wish to reuse it.
*
* @return default arguments node
*/
public ArgumentCommandNode<S, String> getDefaultArgs() {
return this.defaultArgs;
}
/**
* Gets the {@link LiteralCommandNode} to be registered for the {@link Command}.
*
* @return the {@link LiteralCommandNode}
*/
public LiteralCommandNode<S> getLiteral() {
return this.literal;
}
/**
* Sets the {@link LiteralCommandNode} used to register this command. The default literal is mutable, so
* this is primarily if you want to completely replace the object.
*
* @param literal new node
*/
public void setLiteral(LiteralCommandNode<S> literal) {
this.literal = literal;
}
/**
* Gets whether this command should is treated as "raw".
*
* @see #setRawCommand(boolean)
* @return whether this command is treated as "raw"
*/
public boolean isRawCommand() {
return this.rawCommand;
}
/**
* Sets whether this command should be treated as "raw".
*
* <p>A "raw" command will only use the node provided by this event for
* sending the command tree to the client. For execution purposes, the default
* greedy string execution of a standard Bukkit {@link Command} is used.</p>
*
* <p>On older versions of Paper, this was the default and only behavior of this
* event.</p>
*
* @param rawCommand whether this command should be treated as "raw"
*/
public void setRawCommand(final boolean rawCommand) {
this.rawCommand = rawCommand;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isCancelled() {
return this.cancelled;
}
/**
* Cancels registering this command to Brigadier, but will remain in Bukkit Command Map. Can be used to hide a
* command from all players.
*
* {@inheritDoc}
*/
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
@NotNull
public HandlerList getHandlers() {
return handlers;
}
@NotNull
public static HandlerList getHandlerList() {
return handlers;
}
}

View file

@ -1,42 +0,0 @@
package io.papermc.paper.brigadier;
import com.mojang.brigadier.Message;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.TextComponent;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* Helper methods to bridge the gaps between Brigadier and Paper-MojangAPI.
*/
public final class PaperBrigadier {
private PaperBrigadier() {
throw new RuntimeException("PaperBrigadier is not to be instantiated!");
}
/**
* Create a new Brigadier {@link Message} from a {@link ComponentLike}.
*
* <p>Mostly useful for creating rich suggestion tooltips in combination with other Paper-MojangAPI APIs.</p>
*
* @param componentLike The {@link ComponentLike} to use for the {@link Message} contents
* @return A new Brigadier {@link Message}
*/
public static @NonNull Message message(final @NonNull ComponentLike componentLike) {
return PaperBrigadierProvider.instance().message(componentLike);
}
/**
* Create a new {@link Component} from a Brigadier {@link Message}.
*
* <p>If the {@link Message} was created from a {@link Component}, it will simply be
* converted back, otherwise a new {@link TextComponent} will be created with the
* content of {@link Message#getString()}</p>
*
* @param message The {@link Message} to create a {@link Component} from
* @return The created {@link Component}
*/
public static @NonNull Component componentFromMessage(final @NonNull Message message) {
return PaperBrigadierProvider.instance().componentFromMessage(message);
}
}

View file

@ -1,30 +0,0 @@
package io.papermc.paper.brigadier;
import com.mojang.brigadier.Message;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.NonNull;
import static java.util.Objects.requireNonNull;
interface PaperBrigadierProvider {
final class Holder {
private static @MonotonicNonNull PaperBrigadierProvider INSTANCE;
}
static @NonNull PaperBrigadierProvider instance() {
return requireNonNull(Holder.INSTANCE, "PaperBrigadierProvider has not yet been initialized!");
}
static void initialize(final @NonNull PaperBrigadierProvider instance) {
if (Holder.INSTANCE != null) {
throw new IllegalStateException("PaperBrigadierProvider has already been initialized!");
}
Holder.INSTANCE = instance;
}
@NonNull Message message(@NonNull ComponentLike componentLike);
@NonNull Component componentFromMessage(@NonNull Message message);
}

View file

@ -11,7 +11,7 @@ import kotlin.io.path.*
plugins {
java
`maven-publish`
id("io.papermc.paperweight.core") version "1.6.3"
id("io.papermc.paperweight.core") version "1.7.1"
}
allprojects {
@ -109,7 +109,6 @@ paperweight {
tasks.generateDevelopmentBundle {
apiCoordinates = "io.papermc.paper:paper-api"
mojangApiCoordinates = "io.papermc.paper:paper-mojangapi"
libraryRepositories.addAll(
"https://repo.maven.apache.org/maven2/",
paperMavenPublicUrl,

File diff suppressed because it is too large Load diff

View file

@ -46,8 +46,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
this.commandsConfiguration = YamlConfiguration.loadConfiguration(this.getCommandsConfigFile());
@@ -0,0 +0,0 @@ public final class CraftServer implements Server {
this.enablePlugins(PluginLoadOrder.POSTWORLD);
this.getPluginManager().callEvent(new ServerLoadEvent(ServerLoadEvent.LoadType.RELOAD));
if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.pluginsEnabled(); // Paper - Remap plugins
this.getPluginManager().callEvent(new ServerLoadEvent(ServerLoadEvent.LoadType.RELOAD));
+ org.spigotmc.WatchdogThread.hasStarted = true; // Paper - Disable watchdog early timeout on reload
}

View file

@ -230,9 +230,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/org/bukkit/craftbukkit/command/VanillaCommandWrapper.java
+++ b/src/main/java/org/bukkit/craftbukkit/command/VanillaCommandWrapper.java
@@ -0,0 +0,0 @@ public final class VanillaCommandWrapper extends BukkitCommand {
} else {
commandName = vanillaCommand.getRedirect().getName();
vanillaCommand = vanillaCommand.getRedirect();
}
final String commandName = vanillaCommand.getName();
+ if ("pgive".equals(stripDefaultNamespace(commandName))) {
+ return "bukkit.command.paper.pgive";
+ }

View file

@ -9,18 +9,6 @@ Adds AsyncPlayerSendCommandsEvent
Adds CommandRegisteredEvent
- Allows manipulating the CommandNode to add more children/metadata for the client
diff --git a/build.gradle.kts b/build.gradle.kts
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -0,0 +0,0 @@ val alsoShade: Configuration by configurations.creating
dependencies {
implementation(project(":paper-api"))
+ implementation(project(":paper-mojangapi"))
// Paper start
implementation("org.jline:jline-terminal-jansi:3.21.0")
implementation("net.minecrell:terminalconsoleappender:1.3.0")
diff --git a/src/main/java/com/mojang/brigadier/exceptions/CommandSyntaxException.java b/src/main/java/com/mojang/brigadier/exceptions/CommandSyntaxException.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/com/mojang/brigadier/exceptions/CommandSyntaxException.java

File diff suppressed because it is too large Load diff

View file

@ -40,8 +40,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ // Paper end - Configurable player collision
+
this.server.enablePlugins(org.bukkit.plugin.PluginLoadOrder.POSTWORLD);
this.server.getPluginManager().callEvent(new ServerLoadEvent(ServerLoadEvent.LoadType.STARTUP));
if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.pluginsEnabled(); // Paper - Remap plugins
this.server.getPluginManager().callEvent(new ServerLoadEvent(ServerLoadEvent.LoadType.STARTUP));
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/players/PlayerList.java

View file

@ -1557,13 +1557,13 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
this.server.enablePlugins(org.bukkit.plugin.PluginLoadOrder.POSTWORLD);
this.server.getPluginManager().callEvent(new ServerLoadEvent(ServerLoadEvent.LoadType.STARTUP));
+ if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.pluginsEnabled(); // Paper - Remap plugins
this.server.getPluginManager().callEvent(new ServerLoadEvent(ServerLoadEvent.LoadType.STARTUP));
this.connection.acceptConnections();
}
@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.server.disablePlugins();
}
@ -1908,10 +1908,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java
+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java
@@ -0,0 +0,0 @@ public final class CraftServer implements Server {
this.loadPlugins();
this.enablePlugins(PluginLoadOrder.STARTUP);
this.enablePlugins(PluginLoadOrder.POSTWORLD);
this.getPluginManager().callEvent(new ServerLoadEvent(ServerLoadEvent.LoadType.RELOAD));
+ if (io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper != null) io.papermc.paper.plugin.PluginInitializerManager.instance().pluginRemapper.pluginsEnabled(); // Paper - Remap plugins
this.getPluginManager().callEvent(new ServerLoadEvent(ServerLoadEvent.LoadType.RELOAD));
}
@Override

View file

@ -60,12 +60,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
public static String getPermission(CommandNode<CommandSourceStack> vanillaCommand) {
- return "minecraft.command." + ((vanillaCommand.getRedirect() == null) ? vanillaCommand.getName() : vanillaCommand.getRedirect().getName());
+ // Paper start - Vanilla command permission fixes
+ final String commandName;
+ if (vanillaCommand.getRedirect() == null) {
+ commandName = vanillaCommand.getName();
+ } else {
+ commandName = vanillaCommand.getRedirect().getName();
+ while (vanillaCommand.getRedirect() != null) {
+ vanillaCommand = vanillaCommand.getRedirect();
+ }
+ final String commandName = vanillaCommand.getName();
+ return "minecraft.command." + stripDefaultNamespace(commandName);
+ }
+

View file

@ -33,7 +33,7 @@ if (!file(".git").exists()) {
rootProject.name = "paper"
for (name in listOf("Paper-API", "Paper-Server", "Paper-MojangAPI")) {
for (name in listOf("Paper-API", "Paper-Server")) {
val projName = name.lowercase(Locale.ENGLISH)
include(projName)
findProject(":$projName")!!.projectDir = file(name)

View file

@ -2,7 +2,6 @@ version = "1.0.0-SNAPSHOT"
dependencies {
compileOnly(project(":paper-api"))
compileOnly(project(":paper-mojangapi"))
}
tasks.processResources {

View file

@ -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);
}
}

View file

@ -8,6 +8,7 @@ public class TestPluginBootstrap implements PluginBootstrap {
@Override
public void bootstrap(@NotNull BootstrapContext context) {
// io.papermc.testplugin.brigtests.Registration.registerViaBootstrap(context);
}
}

View file

@ -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));
}
}

View file

@ -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());
}
}

View file

@ -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));
}
}
);
}
}

View file

@ -0,0 +1,9 @@
package io.papermc.testplugin.brigtests.example;
public enum IceCreamType {
VANILLA,
CHOCOLATE,
BLUE_MOON,
STRAWBERRY,
WHOLE_MILK
}

View file

@ -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()
);
}
}

View file

@ -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();
}
}