From 36511875cc307849292cf2c6fc88cb87bbb8f0c0 Mon Sep 17 00:00:00 2001 From: Bram Date: Mon, 13 Jan 2025 10:16:24 +0100 Subject: [PATCH] Added: AsyncOfflinePlayerArgument --- .../arguments/types/entities-arguments.md | 33 ++++++++++- .../arguments/types/EntitiesArguments.java | 30 ++++++++++ .../arguments/types/EntitiesArguments.kt | 58 +++++++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) diff --git a/docs/en/create-commands/arguments/types/entities-arguments.md b/docs/en/create-commands/arguments/types/entities-arguments.md index 042e864bb..cfb1d8438 100644 --- a/docs/en/create-commands/arguments/types/entities-arguments.md +++ b/docs/en/create-commands/arguments/types/entities-arguments.md @@ -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` 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 +``` + +We now want to get the `CompletableFuture` 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) diff --git a/reference-code/src/main/java/createcommands/arguments/types/EntitiesArguments.java b/reference-code/src/main/java/createcommands/arguments/types/EntitiesArguments.java index a92ecc669..e7c828c6a 100644 --- a/reference-code/src/main/java/createcommands/arguments/types/EntitiesArguments.java +++ b/reference-code/src/main/java/createcommands/arguments/types/EntitiesArguments.java @@ -2,6 +2,7 @@ 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; @@ -9,11 +10,13 @@ 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 { @@ -51,6 +54,33 @@ class EntitiesArguments { .register(); // #endregion noSelectorSuggestionsExample + // #region playedBeforeArgumentExample + new CommandAPICommand("playedbefore") + .withArguments(new AsyncOfflinePlayerArgument("player")) + .executes((sender, args) -> { + CompletableFuture player = (CompletableFuture) 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")) diff --git a/reference-code/src/main/kotlin/createcommands/arguments/types/EntitiesArguments.kt b/reference-code/src/main/kotlin/createcommands/arguments/types/EntitiesArguments.kt index ac205d1af..b36173bea 100644 --- a/reference-code/src/main/kotlin/createcommands/arguments/types/EntitiesArguments.kt +++ b/reference-code/src/main/kotlin/createcommands/arguments/types/EntitiesArguments.kt @@ -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 @@ -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 @@ -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 + + // 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")) @@ -83,6 +114,33 @@ fun entitiesArgumentsDSL() { } // #endregion entitySelectorArgumentExampleDSL + // #region playedBeforeArgumentExampleDSL + commandAPICommand("playedbefore") { + asyncOfflinePlayerArgument("player") + anyExecutor { sender, args -> + val player = args["player"] as CompletableFuture + + // 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")