diff --git a/config/sidebar.paper.ts b/config/sidebar.paper.ts index 99e90b1e..b9b04260 100755 --- a/config/sidebar.paper.ts +++ b/config/sidebar.paper.ts @@ -162,7 +162,10 @@ const paper: SidebarsConfig = { type: "category", label: "Misc", collapsed: true, - items: ["dev/api/command-api/misc/comparison-bukkit-brigadier"], + items: [ + "dev/api/command-api/misc/comparison-bukkit-brigadier", + "dev/api/command-api/misc/basic-command", + ], }, ], }, diff --git a/docs/paper/dev/api/command-api/misc/assets/broadcast-command.png b/docs/paper/dev/api/command-api/misc/assets/broadcast-command.png new file mode 100644 index 00000000..95776628 Binary files /dev/null and b/docs/paper/dev/api/command-api/misc/assets/broadcast-command.png differ diff --git a/docs/paper/dev/api/command-api/misc/assets/broadcast-suggestions-finished.png b/docs/paper/dev/api/command-api/misc/assets/broadcast-suggestions-finished.png new file mode 100644 index 00000000..f58766c6 Binary files /dev/null and b/docs/paper/dev/api/command-api/misc/assets/broadcast-suggestions-finished.png differ diff --git a/docs/paper/dev/api/command-api/misc/assets/broadcast-suggestions-none.png b/docs/paper/dev/api/command-api/misc/assets/broadcast-suggestions-none.png new file mode 100644 index 00000000..8f6033db Binary files /dev/null and b/docs/paper/dev/api/command-api/misc/assets/broadcast-suggestions-none.png differ diff --git a/docs/paper/dev/api/command-api/misc/assets/broadcast-suggestions-unfinished.png b/docs/paper/dev/api/command-api/misc/assets/broadcast-suggestions-unfinished.png new file mode 100644 index 00000000..d66d5c36 Binary files /dev/null and b/docs/paper/dev/api/command-api/misc/assets/broadcast-suggestions-unfinished.png differ diff --git a/docs/paper/dev/api/command-api/misc/basic-command.mdx b/docs/paper/dev/api/command-api/misc/basic-command.mdx new file mode 100644 index 00000000..ee6dc235 --- /dev/null +++ b/docs/paper/dev/api/command-api/misc/basic-command.mdx @@ -0,0 +1,280 @@ +--- +slug: /dev/command-api/misc/basic-command +description: Bukkit commands styled command declaration +--- + +import BroadcastCommand from "./assets/broadcast-command.png" +import BroadcastSuggestionsFinished from "./assets/broadcast-suggestions-finished.png" +import BroadcastSuggestionsUnfinished from "./assets/broadcast-suggestions-unfinished.png" +import BroadcastSuggestionsNone from "./assets/broadcast-suggestions-none.png" + +# Basic Commands +For very simple commands Paper has a way to declare Bukkit-style command by implementing the `BasicCommand` interface. + +This interface has one method you have to override: +- `void execute(CommandSourceStack commandSourceStack, String[] args)` + +And three more, optional methods which you can override: +- `Collection suggest(CommandSourceStack commandSourceStack, String[] args)` +- `boolean canUse(CommandSender sender)` +- `@Nullable String permission()` + +## Simple usage +Implementing the execute method, your class might look like this: +```java title="YourCommand.java" +package your.package.name; + +import io.papermc.paper.command.brigadier.BasicCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; + +public class YourCommand implements BasicCommand { + + @Override + public void execute(CommandSourceStack commandSourceStack, String[] args) { + + } +} +``` + +If you have seen the `CommandContext` class before, you might recognize the first parameter of the execute method as the generic +parameter `S` from our `CommandContext`, which is also used in the `executes` method from the `ArgumentBuilder`. + +With a `CommandSourceStack`, we can retrieve basic information about the sender of the command, the location the command was send from, and the executing entity. +For more information, check out [basics/command-executors](../basics/command-executors.mdx). + + +## The optional methods +You can freely choose whether to implement either of the at the top mentioned, optional methods. Here is a quick overview on what which one does: + +### `suggest(CommandSourceStack, String[])` +This method returns some sort of `Collection` and takes in a `CommandSourceStack` and a `String[] args` as parameters. This is similar to the +`onTabComplete(CommandSender, Command, String, String[])` method of the `TabCompleter` interface, which is commonly used for tab completion on Bukkit commands. + +Yeah entry in the collection that you return will be send to the client to be shown as suggestions, the same way as with Bukkit commands. + +### `canUse(CommandSender)` +With this method, you can set up a basic `requires` structure from Brigadier commands. ([Read more on that here] WIP). This method returns a `boolean`, which is +required to return `true` in order for a command sender to execute that command. + +### `permission()` +With the permission method you can set the required permission required to be able to execute and view this command. + + +## Example: Broadcast command +As an example, we can create a simple broadcast command. We start by declaring creating a class which implements BasicCommand and overrides `execute` and `permission`: +```java +package your.package.name; + +import io.papermc.paper.command.brigadier.BasicCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.jetbrains.annotations.Nullable; + +public class BroadcastCommand implements BasicCommand { + + @Override + public void execute(CommandSourceStack commandSourceStack, String[] args) { + + } + + @Override + public @Nullable String permission() { + return "example.broadcast.use"; + } +} +``` + +Our permission is set to `example.broadcast.use`. In order to give yourself that permission, it is suggested that you use a plugin like **LuckPerms** or just give yourself +operator permissions. You can also set this permission to be `true` by default. For this, please check out the [plugin.yml documentation](../../../getting-started/plugin-yml.mdx). + +Now, in our `execute` method, we can retrieve the name of the executor of that command. If we do not find one, we can just get the name of the command sender, like this: +```java +final Component name; +if (commandSourceStack.getExecutor() != null) { + name = commandSourceStack.getExecutor().name(); +} +else { + name = commandSourceStack.getSender().name(); +} +``` + +This makes sure that we cover all cases and even allow the command to work correctly with `/execute as`. + +Next, we retrieve all arguments and join them to a String or telling the sender that at least one argument is required in order to send a broadcast +(sending empty broadcasts make no sense, after all), if they defined no arguents (`args` has a length of 0): +```java +if (args.length == 0) { + commandSourceStack.getSender().sendRichMessage("You cannot send an empty broadcast!"); + return; +} + +final String message = String.join(" ", args); +``` + +Finally, we can build our broadcast message and send if via `Bukkit.broadcast(Component)`: +```java +final Component broadcastMessage = MiniMessage.miniMessage().deserialize( + "BROADCAST » ", + Placeholder.component("name", name), + Placeholder.unparsed("message", message) +); + +Bukkit.broadcast(broadcastMessage); +``` + +And we are done! As you can see, this is a very simple way to define commands. Here is the final result of our class: +```java title="BroadcastCommand.java" +package your.package.name; + +import io.papermc.paper.command.brigadier.BasicCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.bukkit.Bukkit; +import org.jetbrains.annotations.Nullable; + +public class BroadcastCommand implements BasicCommand { + + @Override + public void execute(CommandSourceStack commandSourceStack, String[] args) { + final Component name; + if (commandSourceStack.getExecutor() != null) { + name = commandSourceStack.getExecutor().name(); + } + else { + name = commandSourceStack.getSender().name(); + } + + if (args.length == 0) { + commandSourceStack.getSender().sendRichMessage("You cannot send an empty broadcast!"); + return; + } + + final String message = String.join(" ", args); + final Component broadcastMessage = MiniMessage.miniMessage().deserialize( + "BROADCAST » ", + Placeholder.component("name", name), + Placeholder.unparsed("message", message) + ); + + Bukkit.broadcast(broadcastMessage); + } + + @Override + public @Nullable String permission() { + return "example.broadcast.use"; + } +} +``` + +Registering our command looks like this: +```java title="PluginMainClass.java" +@Override +public void onEnable() { + this.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, + event -> event.registrar().register("broadcast", new BroadcastCommand()) + ); +} +``` + +And this is how it looks like in-game: + + + +## Example: Adding suggestions +Our broadcast command works pretty well, but it is lacking on suggestions. A very common kind of suggestion for text based commands are player names. +In order to suggest player names, we can just map all online players to their name, like this: +```java +@Override +public Collection suggest(CommandSourceStack commandSourceStack, String[] args) { + return Bukkit.getOnlinePlayers().stream().map(Player::getName).toList(); +} +``` + +This works great, but as you can see here, it will always suggest all players, regardless of user input, which can feel unnatural at times: + + +In order to fix this, we have to do some changes: + +First, we early return what we already have in case there is no arguments, as we can not filter by input then: +```java +if (args.length == 0) { + return Bukkit.getOnlinePlayers().stream().map(Player::getName).toList(); +} +``` + +After this, we can add a `filter` clause to our stream, where we filter all names by whether they start with our current input, which is `args[args.length - 1]`: +```java +return Bukkit.getOnlinePlayers().stream() + .map(Player::getName) + .filter(name -> name.toLowerCase().startsWith(args[args.length - 1].toLowerCase())) + .toList(); +``` + +And we are done! As you can see, suggestions still work fine: + + +But when there is no player who starts with an input, it just suggests nothing: + + +### Final code +Here is the final code for our whole `BroadcastCommand` class, including the suggestions: +```java +package your.package.name; + +import io.papermc.paper.command.brigadier.BasicCommand; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; + +public class BroadcastCommand implements BasicCommand { + + @Override + public void execute(CommandSourceStack commandSourceStack, String[] args) { + final Component name; + if (commandSourceStack.getExecutor() != null) { + name = commandSourceStack.getExecutor().name(); + } + else { + name = commandSourceStack.getSender().name(); + } + + if (args.length == 0) { + commandSourceStack.getSender().sendRichMessage("You cannot send an empty broadcast!"); + return; + } + + final String message = String.join(" ", args); + final Component broadcastMessage = MiniMessage.miniMessage().deserialize( + "BROADCAST » ", + Placeholder.component("name", name), + Placeholder.unparsed("message", message) + ); + + Bukkit.broadcast(broadcastMessage); + } + + @Override + public @Nullable String permission() { + return "example.broadcast.use"; + } + + @Override + public Collection suggest(CommandSourceStack commandSourceStack, String[] args) { + if (args.length == 0) { + return Bukkit.getOnlinePlayers().stream().map(Player::getName).toList(); + } + + return Bukkit.getOnlinePlayers().stream() + .map(Player::getName) + .filter(name -> name.toLowerCase().startsWith(args[args.length - 1].toLowerCase())) + .toList(); + } +} +``` \ No newline at end of file