Skip to content

Commit

Permalink
Add BasicCommand documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Strokkur424 committed Dec 29, 2024
1 parent 55997df commit 686529e
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 1 deletion.
5 changes: 4 additions & 1 deletion config/sidebar.paper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
},
],
},
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
280 changes: 280 additions & 0 deletions docs/paper/dev/api/command-api/misc/basic-command.mdx
Original file line number Diff line number Diff line change
@@ -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<String> 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<CommandSourceStack>` class before, you might recognize the first parameter of the execute method as the generic
parameter `S` from our `CommandContext<S>`, 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<String>` 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):

Check warning on line 103 in docs/paper/dev/api/command-api/misc/basic-command.mdx

View workflow job for this annotation

GitHub Actions / check

"arguents" should be "arguments".
```java
if (args.length == 0) {
commandSourceStack.getSender().sendRichMessage("<red>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(
"<red><bold>BROADCAST</red> <name> <dark_gray>»</dark_gray> <message>",
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("<red>You cannot send an empty broadcast!");
return;
}

final String message = String.join(" ", args);
final Component broadcastMessage = MiniMessage.miniMessage().deserialize(
"<red><bold>BROADCAST</red> <name> <dark_gray>»</dark_gray> <message>",
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:
<img src={BroadcastCommand}/>


## 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<String> 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:
<img src={BroadcastSuggestionsUnfinished}/>

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:
<img src={BroadcastSuggestionsFinished}/>

But when there is no player who starts with an input, it just suggests nothing:
<img src={BroadcastSuggestionsNone}/>

### 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("<red>You cannot send an empty broadcast!");
return;
}

final String message = String.join(" ", args);
final Component broadcastMessage = MiniMessage.miniMessage().deserialize(
"<red><bold>BROADCAST</red> <name> <dark_gray>»</dark_gray> <message>",
Placeholder.component("name", name),
Placeholder.unparsed("message", message)
);

Bukkit.broadcast(broadcastMessage);
}

@Override
public @Nullable String permission() {
return "example.broadcast.use";
}

@Override
public Collection<String> 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();
}
}
```

0 comments on commit 686529e

Please sign in to comment.