Skip to content

dev/dev into master to see merge conflicts #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 25, 2025
33 changes: 32 additions & 1 deletion docs/en/create-commands/arguments/types/entities-arguments.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,41 @@ And there we have it! One thing to note is that entity selectors are still a val

## OfflinePlayer argument

The `OfflinePlayerArgument` class is identical to the `PlayerArgument` class, but instead of returning a `Player` object, it returns an `OfflinePlayer` object. Internally, this argument makes calls to Mojang servers (via Mojang's authlib), meaning it can be slightly slower than alternative methods (such as using a `StringArgument` and suggesting a list of existing offline players).
The `OfflinePlayerArgument` class is identical to the `PlayerArgument` class, but instead of returning a `Player` object, it returns an `OfflinePlayer` object. Internally, this argument makes calls to Mojang servers (via Mojang's authlib), meaning it can be slightly slower than alternative methods such as using a `AsyncOfflinePlayerArgument`, which runs the API call asynchronously, or using a `StringArgument` and suggesting a list of existing offline players.

The `OfflinePlayerArgument` _should_ be able to retrieve players that have never joined the server before.

## AsyncOfflinePlayer argument

The `AsyncOfflinePlayerArgument` class is identical to the `OfflinePlayerArgument` class, but instead of making the API call synchronously, it makes the API call asynchronously. This means that the command will not block the main thread while waiting for the API call to complete.

:::info
The `AsyncOfflinePlayerArgument` returns a `CompletableFuture<OfflinePlayer>` object, which can be used to retrieve the `OfflinePlayer` object when the API call is complete.
:::

::::tip Example - Checking if a player has joined before

Say we want to create a command that tells us if a player has joined the server before. We can use the `AsyncOfflinePlayerArgument` to fetch the `OfflinePlayer` object asynchronously. That way we simply wait for the request to complete, and once it does, we can check if the player has joined the server before. We want to create a command of the following form:

```mccmd
/playedbefore <player>
```

We now want to get the `CompletableFuture<OfflinePlayer>` object from the `AsyncOfflinePlayerArgument` and then use it to get the `OfflinePlayer` object. We can define it like this:

:::tabs
===Java
<<< @/../reference-code/src/main/java/createcommands/arguments/types/EntitiesArguments.java#playedBeforeArgumentExample
===Kotlin
<<< @/../reference-code/src/main/kotlin/createcommands/arguments/types/EntitiesArguments.kt#playedBeforeArgumentExample
===Kotlin DSL
<<< @/../reference-code/src/main/kotlin/createcommands/arguments/types/EntitiesArguments.kt#playedBeforeArgumentExampleDSL
:::

We now successfully ran a command that asynchronously checks if a player has joined the server before without blocking the main thread despite making an API call.

::::

## Entity type argument

![An image of an entity argument displaying a list of entity type suggestions](/images/arguments/entitytype.png)
Expand Down
21 changes: 20 additions & 1 deletion docs/en/create-commands/executors/native-sender.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,23 @@ This can now be used via the following command examples:
/execute in minecraft:overworld positioned 20 60 -20 run break
```

::::
::::

## Constructing a `NativeProxyCommandSender`

You can create a `NativeProxyCommandSender` object yourself using the static `from` method:

```java
NativeProxyCommandSender from(CommandSender caller, CommandSender callee, Location location, World world);
```

This `CommandSender` will work the same as any other `NativeProxyCommandSender` you would get while using `executesNative`. For example, you could use it to make a simple version of `/execute`, like so:

:::tabs
===Java
<<< @/../reference-code/src/main/java/createcommands/executors/NativeSender.java#constructorExample
===Kotlin
<<< @/../reference-code/src/main/kotlin/createcommands/executors/NativeSender.kt#constructorExample
===Kotlin DSL
<<< @/../reference-code/src/main/kotlin/createcommands/executors/NativeSender.kt#constructorExampleDSL
:::
2 changes: 1 addition & 1 deletion docs/en/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The CommandAPI does not follow the "standard" method of registering commands. In

- **Commands should not be declared in the `plugin.yml` file.**
- Commands are automatically registered under the namespace of the plugin that registered the command. For example, if you register a command `/hello`, you can also run it using `/<pluginname>:hello`. However, you can change the default namespace. More about this [on the command registration page](./create-commands/registration#registering-the-command).
- Commands are not "linked" to a certain plugin. In other words, you can’t look up which commands are registered by which plugin.
- Commands are not "linked" to a certain plugin. In other words, you can’t look up which commands are registered by which plugin. This is not the case for commands on Paper versions that have Paper's Brigadier API, or in other words, any Paper build starting in 1.20.6 with build 65.

## How this documentation works

Expand Down
20 changes: 20 additions & 0 deletions docs/en/upgrading-parts/9.7.0-to-10.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,23 @@ For 10.0.0, all previously deprecated methods have been removed. Please make sur
The default namespace has been updated from `minecraft` to the plugin's name. If you are not shading, the default namespace is going to be `commandapi`. If you are shading, the default namespace is going to be your plugin's name.

Along with this change, the `CommandAPIBukkitConfig#usePluginNamespace()` has been deprecated since it is now default behaviour.

-----

### `NativeProxyCommandSender` changes

`NativeProxyCommandSender` used to be a class, but this version changed it to an interface. Any code compiled against an earlier version that references any method of `NativeProxyCommandSender` may throw the following `IncompatibleClassChangeError` when run using the new version of the API:

```
java.lang.IncompatibleClassChangeError: Found interface dev.jorel.commandapi.wrappers.NativeProxyCommandSender, but class was expected
```

If this happens, the original code simply needs to be recompiled using the new API version.

Additionally, the constructor of `NativeProxyCommandSender` is no longer available. Instead, the static `from` method should be used:

```java
new NativeProxyCommandSender(caller, callee, location, world); // [!code --]

NativeProxyCommandSender.from(caller, callee, location, world); // [!code ++]
```
13 changes: 12 additions & 1 deletion docs/en/velocity/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ preferences: ['build-system']
authors:
- JorelAli
- DerEchtePilz
- WillKroboth
---

# Velocity
Expand Down Expand Up @@ -103,4 +104,14 @@ To accomplish that, we register the command like this:
<<< @/../reference-code/src/main/kotlin/velocity/Intro.kt#registerCommandExample
:::

::::
::::

## Updating Requirements

As explained on the page about command and argument [requirements](../create-commands/requirements.md#updating-requirements), you should run `CommandAPI.updateRequirements(player)` whenever conditions that affect whether a player can access a command change. However, Velocity is not able to resend commands to a client by itself; it only knows how to add commands to a packet the backend server sent. So, in order for `updateRequirements` to work, the CommandAPI needs to be able to tell the backend server to resend commands.

The CommandAPI Velocity plugin handles this by sending a plugin message to the backend server. In order for the backend server to process this message, you must add a plugin that understands CommandAPI plugin messages. You can easily do this by installing the CommandAPI Bukkit plugin or any plugin that shades CommandAPI version 10.0.0 or later to each backend server. However, the CommandAPI Bukkit plugin includes a lot of extra stuff to make registering commands work, which isn't necessary if you only need `updateRequirements` to work on Velocity. In this case, you can install the special CommandAPI Bukkit Networking plugin, which simply contains the code necessary to respond to plugin messages.

Whether you want to install the CommandAPI Bukkit plugin or the CommandAPI Bukkit Networking plugin, you can find the latest releases [here](https://github.com/CommandAPI/CommandAPI/releases/latest).

So far, the CommandAPI only uses the plugin messaging system to implement `updateRequirements` on Velocity. If you're not using `updateRequirements` on Velocity, you don't need to install a CommandAPI plugin on your backend servers. It is possible that future features will take advantage of the plugin messaging system as CommandAPI Velocity is further developed. If this ever happens, you should see a clear error message prompting you to update to a compatible version.
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@

import dev.jorel.commandapi.CommandAPICommand;
import dev.jorel.commandapi.arguments.Argument;
import dev.jorel.commandapi.arguments.AsyncOfflinePlayerArgument;
import dev.jorel.commandapi.arguments.EntitySelectorArgument;
import dev.jorel.commandapi.arguments.EntityTypeArgument;
import dev.jorel.commandapi.arguments.IntegerArgument;
import dev.jorel.commandapi.arguments.PlayerArgument;
import dev.jorel.commandapi.arguments.SafeSuggestions;
import dev.jorel.commandapi.executors.CommandArguments;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;

import java.util.Collection;
import java.util.concurrent.CompletableFuture;

class EntitiesArguments {
static {
Expand Down Expand Up @@ -51,6 +54,33 @@ class EntitiesArguments {
.register();
// #endregion noSelectorSuggestionsExample

// #region playedBeforeArgumentExample
new CommandAPICommand("playedbefore")
.withArguments(new AsyncOfflinePlayerArgument("player"))
.executes((sender, args) -> {
CompletableFuture<OfflinePlayer> player = (CompletableFuture<OfflinePlayer>) args.get("player");

// Directly sends a message to the sender, indicating that the command is running to prevent confusion
sender.sendMessage("Checking if the player has played before...");

player.thenAccept(offlinePlayer -> {
if (offlinePlayer.hasPlayedBefore()) {
sender.sendMessage("Player has played before");
} else {
sender.sendMessage("Player has never played before");
}
}).exceptionally(throwable -> {
// We have to partly handle exceptions ourselves, since we are using a CompletableFuture
Throwable cause = throwable.getCause();
Throwable rootCause = cause instanceof RuntimeException ? cause.getCause() : cause;

sender.sendMessage(rootCause.getMessage());
return null;
});
})
.register();
// #endregion playedBeforeArgumentExample

// #region entityTypeArgumentExample
new CommandAPICommand("spawnmob")
.withArguments(new EntityTypeArgument("entity"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
package createcommands.executors;

import dev.jorel.commandapi.CommandAPICommand;
import dev.jorel.commandapi.arguments.CommandArgument;
import dev.jorel.commandapi.arguments.EntitySelectorArgument;
import dev.jorel.commandapi.arguments.LocationArgument;
import dev.jorel.commandapi.arguments.WorldArgument;
import dev.jorel.commandapi.wrappers.CommandResult;
import dev.jorel.commandapi.wrappers.NativeProxyCommandSender;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.command.CommandSender;

class NativeSender {
static {
Expand All @@ -15,5 +23,26 @@ class NativeSender {
})
.register();
// #endregion breakCommandExample

// #region constructorExample
new CommandAPICommand("executeAs")
.withArguments(
new EntitySelectorArgument.OneEntity("target"),
new LocationArgument("location"),
new WorldArgument("world"),
new CommandArgument("command")
)
.executes((caller, args) -> {
CommandSender callee = (CommandSender) args.get("target");
Location location = (Location) args.get("location");
World world = (World) args.get("world");
CommandResult command = (CommandResult) args.get("command");

assert callee != null && location != null && world != null && command != null;

command.execute(NativeProxyCommandSender.from(caller, callee, location, world));
})
.register();
// #endregion constructorExample
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package createcommands.arguments.types

import dev.jorel.commandapi.CommandAPICommand
import dev.jorel.commandapi.arguments.AsyncOfflinePlayerArgument
import dev.jorel.commandapi.arguments.EntitySelectorArgument
import dev.jorel.commandapi.arguments.EntityTypeArgument
import dev.jorel.commandapi.arguments.IntegerArgument
Expand All @@ -10,14 +11,17 @@ import dev.jorel.commandapi.executors.CommandExecutor
import dev.jorel.commandapi.executors.PlayerCommandExecutor
import dev.jorel.commandapi.kotlindsl.anyExecutor
import dev.jorel.commandapi.kotlindsl.commandAPICommand
import dev.jorel.commandapi.kotlindsl.asyncOfflinePlayerArgument
import dev.jorel.commandapi.kotlindsl.entitySelectorArgumentManyEntities
import dev.jorel.commandapi.kotlindsl.entityTypeArgument
import dev.jorel.commandapi.kotlindsl.integerArgument
import dev.jorel.commandapi.kotlindsl.playerExecutor
import org.bukkit.Bukkit
import org.bukkit.OfflinePlayer
import org.bukkit.entity.Entity
import org.bukkit.entity.EntityType
import org.bukkit.entity.Player
import java.util.concurrent.CompletableFuture

fun entitiesArguments() {
// #region entitySelectorArgumentExample
Expand Down Expand Up @@ -53,6 +57,33 @@ fun entitiesArguments() {
.register()
// #endregion noSelectorSuggestionsExample

// #region playedBeforeArgumentExample
CommandAPICommand("playedbefore")
.withArguments(AsyncOfflinePlayerArgument("player"))
.executes(CommandExecutor { sender, args ->
val player = args["player"] as CompletableFuture<OfflinePlayer>

// Directly sends a message to the sender, indicating that the command is running to prevent confusion
sender.sendMessage("Checking if the player has played before...")

player.thenAccept { offlinePlayer ->
if (offlinePlayer.hasPlayedBefore()) {
sender.sendMessage("Player has played before")
} else {
sender.sendMessage("Player has never played before")
}
}.exceptionally { throwable ->
// We have to partly handle exceptions ourselves, since we are using a CompletableFuture
val cause = throwable.cause
val rootCause = if (cause is RuntimeException) cause.cause else cause

sender.sendMessage(rootCause?.message ?: "An error occurred")
null
}
})
.register()
// #endregion playedBeforeArgumentExample

// #region entityTypeArgumentExample
CommandAPICommand("spawnmob")
.withArguments(EntityTypeArgument("entity"))
Expand Down Expand Up @@ -83,6 +114,33 @@ fun entitiesArgumentsDSL() {
}
// #endregion entitySelectorArgumentExampleDSL

// #region playedBeforeArgumentExampleDSL
commandAPICommand("playedbefore") {
asyncOfflinePlayerArgument("player")
anyExecutor { sender, args ->
val player = args["player"] as CompletableFuture<OfflinePlayer>

// Directly sends a message to the sender, indicating that the command is running to prevent confusion
sender.sendMessage("Checking if the player has played before...")

player.thenAccept { offlinePlayer ->
if (offlinePlayer.hasPlayedBefore()) {
sender.sendMessage("Player has played before")
} else {
sender.sendMessage("Player has never played before")
}
}.exceptionally { throwable ->
// We have to partly handle exceptions ourselves, since we are using a CompletableFuture
val cause = throwable.cause
val rootCause = if (cause is RuntimeException) cause.cause else cause

sender.sendMessage(rootCause?.message ?: "An error occurred")
null
}
}
}
// #endregion playedBeforeArgumentExampleDSL

// #region entityTypeArgumentExampleDSL
commandAPICommand("spawnmob") {
entityTypeArgument("entity")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package createcommands.executors

import dev.jorel.commandapi.CommandAPICommand
import dev.jorel.commandapi.arguments.CommandArgument
import dev.jorel.commandapi.arguments.EntitySelectorArgument
import dev.jorel.commandapi.arguments.LocationArgument
import dev.jorel.commandapi.arguments.WorldArgument
import dev.jorel.commandapi.executors.CommandExecutor
import dev.jorel.commandapi.executors.NativeCommandExecutor
import dev.jorel.commandapi.kotlindsl.commandAPICommand
import dev.jorel.commandapi.kotlindsl.nativeExecutor
import dev.jorel.commandapi.kotlindsl.*
import dev.jorel.commandapi.wrappers.CommandResult
import dev.jorel.commandapi.wrappers.NativeProxyCommandSender
import org.bukkit.Location
import org.bukkit.World
import org.bukkit.command.CommandSender

fun nativeSender() {
// #region breakCommandExample
Expand All @@ -14,6 +23,25 @@ fun nativeSender() {
})
.register()
// #endregion breakCommandExample

// #region constructorExample
CommandAPICommand("executeAs")
.withArguments(
EntitySelectorArgument.OneEntity("target"),
LocationArgument("location"),
WorldArgument("world"),
CommandArgument("command")
)
.executes(CommandExecutor { caller, args ->
val callee = args["target"] as CommandSender
val location = args["location"] as Location
val world = args["world"] as World
val command = args["command"] as CommandResult

command.execute(NativeProxyCommandSender.from(caller, callee, location, world))
})
.register()
// #endregion constructorExample
}

fun nativeSenderDSL() {
Expand All @@ -25,4 +53,21 @@ fun nativeSenderDSL() {
}
}
// #endregion breakCommandExampleDSL

// #region constructorExampleDSL
commandAPICommand("executeAs") {
entitySelectorArgumentOneEntity("target")
locationArgument("location")
worldArgument("world")
commandArgument("command")
anyExecutor { caller, args ->
val callee = args["target"] as CommandSender
val location = args["location"] as Location
val world = args["world"] as World
val command = args["command"] as CommandResult

command.execute(NativeProxyCommandSender.from(caller, callee, location, world))
}
}
// #endregion constructorExampleDSL
}