diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/DeleteCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/DeleteCommand.java index 3189e6fb..dc311e84 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/DeleteCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/DeleteCommand.java @@ -1,6 +1,6 @@ package org.mvplugins.multiverse.core.commands; -import java.util.concurrent.CompletableFuture; +import java.util.Collections; import co.aikar.commands.MessageType; import co.aikar.commands.annotation.CommandAlias; @@ -24,6 +24,7 @@ import org.mvplugins.multiverse.core.commandtools.flags.CommandFlag; import org.mvplugins.multiverse.core.commandtools.flags.ParsedCommandFlags; import org.mvplugins.multiverse.core.commandtools.queue.QueuedCommand; import org.mvplugins.multiverse.core.utils.MVCorei18n; +import org.mvplugins.multiverse.core.utils.result.AsyncResult; import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; import org.mvplugins.multiverse.core.world.WorldManager; import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter; @@ -80,9 +81,9 @@ class DeleteCommand extends MultiverseCommand { private void runDeleteCommand(MVCommandIssuer issuer, LoadedMultiverseWorld world, ParsedCommandFlags parsedFlags) { issuer.sendInfo(MVCorei18n.DELETE_DELETING, "{world}", world.getName()); - CompletableFuture future = parsedFlags.hasFlag(REMOVE_PLAYERS_FLAG) - ? CompletableFuture.allOf(playerWorldTeleporter.removeFromWorld(world)) - : CompletableFuture.completedFuture(null); + var future = parsedFlags.hasFlag(REMOVE_PLAYERS_FLAG) + ? playerWorldTeleporter.removeFromWorld(world) + : AsyncResult.completedFuture(Collections.emptyList()); future.thenRun(() -> doWorldDeleting(issuer, world)); } diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/RegenCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/RegenCommand.java index f6e24389..0c6a8ffb 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/RegenCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/RegenCommand.java @@ -3,7 +3,6 @@ package org.mvplugins.multiverse.core.commands; import java.util.Collections; import java.util.List; import java.util.Random; -import java.util.concurrent.CompletableFuture; import co.aikar.commands.MessageType; import co.aikar.commands.annotation.CommandAlias; @@ -27,6 +26,7 @@ import org.mvplugins.multiverse.core.commandtools.flags.CommandValueFlag; import org.mvplugins.multiverse.core.commandtools.flags.ParsedCommandFlags; import org.mvplugins.multiverse.core.commandtools.queue.QueuedCommand; import org.mvplugins.multiverse.core.utils.MVCorei18n; +import org.mvplugins.multiverse.core.utils.result.AsyncResult; import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; import org.mvplugins.multiverse.core.world.WorldManager; import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter; @@ -99,9 +99,9 @@ class RegenCommand extends MultiverseCommand { issuer.sendInfo(MVCorei18n.REGEN_REGENERATING, "{world}", world.getName()); List worldPlayers = world.getPlayers().getOrElse(Collections.emptyList()); - CompletableFuture future = parsedFlags.hasFlag(REMOVE_PLAYERS_FLAG) - ? CompletableFuture.allOf(playerWorldTeleporter.removeFromWorld(world)) - : CompletableFuture.completedFuture(null); + var future = parsedFlags.hasFlag(REMOVE_PLAYERS_FLAG) + ? playerWorldTeleporter.removeFromWorld(world) + : AsyncResult.completedFuture(Collections.emptyList()); future.thenRun(() -> doWorldRegening(issuer, world, parsedFlags, worldPlayers)); } diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/RemoveCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/RemoveCommand.java index b8f77227..9a62bf6f 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/RemoveCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/RemoveCommand.java @@ -1,6 +1,6 @@ package org.mvplugins.multiverse.core.commands; -import java.util.concurrent.CompletableFuture; +import java.util.Collections; import co.aikar.commands.annotation.CommandAlias; import co.aikar.commands.annotation.CommandCompletion; @@ -22,6 +22,7 @@ import org.mvplugins.multiverse.core.commandtools.MultiverseCommand; import org.mvplugins.multiverse.core.commandtools.flags.CommandFlag; import org.mvplugins.multiverse.core.commandtools.flags.ParsedCommandFlags; import org.mvplugins.multiverse.core.utils.MVCorei18n; +import org.mvplugins.multiverse.core.utils.result.AsyncResult; import org.mvplugins.multiverse.core.world.WorldManager; import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter; @@ -66,11 +67,11 @@ class RemoveCommand extends MultiverseCommand { String[] flags) { ParsedCommandFlags parsedFlags = parseFlags(flags); - CompletableFuture future = parsedFlags.hasFlag(REMOVE_PLAYERS_FLAG) + var future = parsedFlags.hasFlag(REMOVE_PLAYERS_FLAG) ? worldManager.getLoadedWorld(worldName) - .map(world -> CompletableFuture.allOf(playerWorldTeleporter.removeFromWorld(world))) - .getOrElse(CompletableFuture.completedFuture(null)) - : CompletableFuture.completedFuture(null); + .map(playerWorldTeleporter::removeFromWorld) + .getOrElse(AsyncResult.completedFuture(Collections.emptyList())) + : AsyncResult.completedFuture(Collections.emptyList()); future.thenRun(() -> doWorldRemoving(issuer, worldName)); } diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/TeleportCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/TeleportCommand.java index de1c7e23..8b2e454b 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/TeleportCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/TeleportCommand.java @@ -1,7 +1,6 @@ package org.mvplugins.multiverse.core.commands; import java.util.Arrays; -import java.util.concurrent.CompletableFuture; import co.aikar.commands.CommandIssuer; import co.aikar.commands.annotation.CommandAlias; @@ -23,6 +22,7 @@ import org.mvplugins.multiverse.core.destination.ParsedDestination; import org.mvplugins.multiverse.core.permissions.CorePermissionsChecker; import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter; import org.mvplugins.multiverse.core.utils.MVCorei18n; +import org.mvplugins.multiverse.core.utils.result.AsyncResult; @Service @CommandAlias("mv") @@ -72,10 +72,14 @@ class TeleportCommand extends MultiverseCommand { issuer.sendInfo(MVCorei18n.TELEPORT_SUCCESS, "{player}", playerName, "{destination}", destination.toString()); - CompletableFuture - .allOf(safetyTeleporter.teleportSafely(issuer.getIssuer(), Arrays.stream(players).toList(), - destination)) - .thenAccept(result -> Logging.fine("Async teleport result: %s", result)); + AsyncResult.allOf(Arrays.stream(players) + .map(player -> safetyTeleporter.teleportSafely(issuer.getIssuer(), player, destination)) + .toList()) + .thenRun(() -> Logging.fine("Async teleport result: %s")) + .exceptionally(throwable -> { + Logging.severe("Error while teleporting %s to %s: %s", + playerName, destination, throwable.getMessage()); + }); } @Override diff --git a/src/main/java/org/mvplugins/multiverse/core/commands/UnloadCommand.java b/src/main/java/org/mvplugins/multiverse/core/commands/UnloadCommand.java index 9e5ad972..e9e9abdb 100644 --- a/src/main/java/org/mvplugins/multiverse/core/commands/UnloadCommand.java +++ b/src/main/java/org/mvplugins/multiverse/core/commands/UnloadCommand.java @@ -1,6 +1,6 @@ package org.mvplugins.multiverse.core.commands; -import java.util.concurrent.CompletableFuture; +import java.util.Collections; import co.aikar.commands.annotation.CommandAlias; import co.aikar.commands.annotation.CommandCompletion; @@ -20,6 +20,7 @@ import org.mvplugins.multiverse.core.commandtools.MultiverseCommand; import org.mvplugins.multiverse.core.commandtools.flags.CommandFlag; import org.mvplugins.multiverse.core.commandtools.flags.ParsedCommandFlags; import org.mvplugins.multiverse.core.utils.MVCorei18n; +import org.mvplugins.multiverse.core.utils.result.AsyncResult; import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; import org.mvplugins.multiverse.core.world.WorldManager; import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter; @@ -70,9 +71,9 @@ class UnloadCommand extends MultiverseCommand { issuer.sendInfo(MVCorei18n.UNLOAD_UNLOADING, "{world}", world.getAlias()); - CompletableFuture future = parsedFlags.hasFlag(REMOVE_PLAYERS_FLAG) - ? CompletableFuture.allOf(playerWorldTeleporter.removeFromWorld(world)) - : CompletableFuture.completedFuture(null); + var future = parsedFlags.hasFlag(REMOVE_PLAYERS_FLAG) + ? playerWorldTeleporter.removeFromWorld(world) + : AsyncResult.completedFuture(Collections.emptyList()); future.thenRun(() -> doWorldUnloading(issuer, world, parsedFlags)); } diff --git a/src/main/java/org/mvplugins/multiverse/core/teleportation/AsyncSafetyTeleporter.java b/src/main/java/org/mvplugins/multiverse/core/teleportation/AsyncSafetyTeleporter.java index 3a6fffc9..00c52fe4 100644 --- a/src/main/java/org/mvplugins/multiverse/core/teleportation/AsyncSafetyTeleporter.java +++ b/src/main/java/org/mvplugins/multiverse/core/teleportation/AsyncSafetyTeleporter.java @@ -1,7 +1,6 @@ package org.mvplugins.multiverse.core.teleportation; import java.util.List; -import java.util.concurrent.CompletableFuture; import com.dumptruckman.minecraft.util.Logging; import io.papermc.lib.PaperLib; @@ -17,6 +16,7 @@ import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.api.BlockSafety; import org.mvplugins.multiverse.core.destination.ParsedDestination; +import org.mvplugins.multiverse.core.utils.result.AsyncResult; import org.mvplugins.multiverse.core.utils.result.Result; @SuppressWarnings("unchecked") @@ -33,97 +33,97 @@ public class AsyncSafetyTeleporter { this.teleportQueue = teleportQueue; } - public CompletableFuture> teleportSafely( + public AsyncResult> teleportSafely( @NotNull Entity teleportee, @Nullable ParsedDestination destination) { return teleportSafely(null, teleportee, destination); } - public CompletableFuture>[] teleportSafely( + public AsyncResult>> teleportSafely( @Nullable CommandSender teleporter, @NotNull List teleportees, @Nullable ParsedDestination destination) { - return teleportees.stream() + return AsyncResult.allOf(teleportees.stream() .map(teleportee -> teleportSafely(teleporter, teleportee, destination)) - .toArray(CompletableFuture[]::new); + .toList()); } - public CompletableFuture> teleportSafely( + public AsyncResult> teleportSafely( @Nullable CommandSender teleporter, @NotNull Entity teleportee, @Nullable ParsedDestination destination) { if (destination == null) { - return CompletableFuture.completedFuture(Result.failure(TeleportResult.Failure.NULL_DESTINATION)); + return AsyncResult.completedFuture(Result.failure(TeleportResult.Failure.NULL_DESTINATION)); } return destination.getDestination().checkTeleportSafety() ? teleportSafely(teleporter, teleportee, destination.getLocation(teleportee)) : teleport(teleporter, teleportee, destination.getLocation(teleportee)); } - public CompletableFuture> teleportSafely( + public AsyncResult> teleportSafely( @NotNull Entity teleportee, @Nullable Location location) { return teleportSafely(null, teleportee, location); } - public CompletableFuture> teleportSafely( + public AsyncResult> teleportSafely( @Nullable CommandSender teleporter, @NotNull Entity teleportee, @Nullable Location location) { if (location == null) { - return CompletableFuture.completedFuture(Result.failure(TeleportResult.Failure.NULL_LOCATION)); + return AsyncResult.completedFuture(Result.failure(TeleportResult.Failure.NULL_LOCATION)); } Location safeLocation = blockSafety.getSafeLocation(location); if (safeLocation == null) { - return CompletableFuture.completedFuture(Result.failure(TeleportResult.Failure.UNSAFE_LOCATION)); + return AsyncResult.completedFuture(Result.failure(TeleportResult.Failure.UNSAFE_LOCATION)); } return teleport(teleporter, teleportee, safeLocation); } - public CompletableFuture>[] teleport( + public AsyncResult>> teleport( @NotNull List teleportees, @Nullable ParsedDestination destination) { - return teleportees.stream() + return AsyncResult.allOf(teleportees.stream() .map(teleportee -> teleport(teleportee, destination)) - .toArray(CompletableFuture[]::new); + .toList()); } - public CompletableFuture> teleport( + public AsyncResult> teleport( @NotNull Entity teleportee, @Nullable ParsedDestination destination) { return teleport(null, teleportee, destination); } - public CompletableFuture> teleport( + public AsyncResult> teleport( @Nullable CommandSender teleporter, @NotNull Entity teleportee, @Nullable ParsedDestination destination) { if (destination == null) { - return CompletableFuture.completedFuture(Result.failure(TeleportResult.Failure.NULL_DESTINATION)); + return AsyncResult.completedFuture(Result.failure(TeleportResult.Failure.NULL_DESTINATION)); } return teleport(teleporter, teleportee, destination.getLocation(teleportee)); } - public CompletableFuture>[] teleport( + public AsyncResult>> teleport( @NotNull List teleportees, @Nullable Location location) { - return teleportees.stream() + return AsyncResult.allOf(teleportees.stream() .map(teleportee -> teleport(teleportee, location)) - .toArray(CompletableFuture[]::new); + .toList()); } - public CompletableFuture> teleport( + public AsyncResult> teleport( @NotNull Entity teleportee, @Nullable Location location) { return teleport(null, teleportee, location); } - public CompletableFuture> teleport( + public AsyncResult> teleport( @Nullable CommandSender teleporter, @NotNull Entity teleportee, @Nullable Location location) { if (location == null) { - return CompletableFuture.completedFuture(Result.failure(TeleportResult.Failure.NULL_LOCATION)); + return AsyncResult.completedFuture(Result.failure(TeleportResult.Failure.NULL_LOCATION)); } boolean shouldAddToQueue = teleporter != null && teleportee instanceof Player; @@ -131,7 +131,7 @@ public class AsyncSafetyTeleporter { teleportQueue.addToQueue(teleporter.getName(), teleportee.getName()); } - CompletableFuture> future = new CompletableFuture<>(); + AsyncResult> future = new AsyncResult<>(); doAsyncTeleport(teleportee, location, future, shouldAddToQueue); return future; } @@ -139,7 +139,7 @@ public class AsyncSafetyTeleporter { private void doAsyncTeleport( @NotNull Entity teleportee, @NotNull Location location, - CompletableFuture> future, + AsyncResult> future, boolean shouldAddToQueue) { Try.run(() -> PaperLib.teleportAsync(teleportee, location).thenAccept(result -> { Logging.finer("Teleported async %s to %s", teleportee.getName(), location); diff --git a/src/main/java/org/mvplugins/multiverse/core/utils/result/AsyncResult.java b/src/main/java/org/mvplugins/multiverse/core/utils/result/AsyncResult.java new file mode 100644 index 00000000..3a306955 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/core/utils/result/AsyncResult.java @@ -0,0 +1,158 @@ +package org.mvplugins.multiverse.core.utils.result; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Function; + +public final class AsyncResult { + + /** + * Returns a new AsyncResult that is completed when all of the given AsyncResult complete. + * + * @param results The results to wait for. + * @return A new AsyncResult that is completed when all of the given AsyncResult complete. + */ + public static AsyncResult allOf(AsyncResult... results) { + return new AsyncResult<>(CompletableFuture.allOf(Arrays.stream(results) + .map(result -> result.future) + .toArray(CompletableFuture[]::new))); + } + + /** + * Returns a new AsyncResult that is completed when all of the given AsyncResult with the same type complete. + * + * @param results The results to wait for. + * @param The type of the AsyncResult. + * @return A new AsyncResult that is completed when all of the given AsyncResult complete. + */ + public static AsyncResult> allOf(List> results) { + return new AsyncResult<>(CompletableFuture.allOf(results.stream() + .map(result -> result.future) + .toArray(CompletableFuture[]::new)) + .thenApply(v -> results.stream() + .map(result -> result.future.join()) + .toList())); + } + + /** + * Wraps a CompletableFuture in an AsyncResult. + * + * @param future The future to wrap. + * @param The type of the future. + * @return A new AsyncResult that is completed when the given future completes. + */ + public static AsyncResult of(CompletableFuture future) { + return new AsyncResult<>(future); + } + + /** + * Returns a new AsyncResult that is already completed with the given value. + * + * @param value The value to complete the AsyncResult with. + * @param The type of the value. + * @return The completed AsyncResult. + */ + public static AsyncResult completedFuture(T value) { + return new AsyncResult<>(CompletableFuture.completedFuture(value)); + } + + /** + * Returns a new CompletableFuture that is already completed exceptionally with the given exception. + * + * @param throwable The exception to complete the AsyncResult with. + * @param The type of the value. + * @return The completed AsyncResult. + */ + public static AsyncResult failedFuture(Throwable throwable) { + return new AsyncResult<>(CompletableFuture.failedFuture(throwable)); + } + + private final CompletableFuture future; + + /** + * Creates a new AsyncResult. + */ + public AsyncResult() { + this(new CompletableFuture<>()); + } + + private AsyncResult(CompletableFuture future) { + this.future = future; + } + + /** + * If not already completed, sets the value returned by related methods to the given value. + * + * @param value The value to complete the AsyncResult with. + * @return true if this invocation caused this AsyncResult to transition to a completed state, else false. + */ + public boolean complete(T value) { + return future.complete(value); + } + + /** + * If not already completed, causes invocations of related methods to throw the given exception. + * + * @param throwable The exception to complete the AsyncResult with. + * @return true if this invocation caused this AsyncResult to transition to a completed state, else false. + */ + public boolean completeExceptionally(Throwable throwable) { + return future.completeExceptionally(throwable); + } + + /** + * Executes the given action when this AsyncResult completes. + * + * @param consumer The action to perform. + * @return This AsyncResult. + */ + public AsyncResult thenAccept(Consumer consumer) { + return new AsyncResult<>(future.thenAccept(consumer)); + } + + /** + * Executes the given action when this AsyncResult completes. + * + * @param runnable The action to perform. + * @return This AsyncResult. + */ + public AsyncResult thenRun(Runnable runnable) { + return new AsyncResult<>(future.thenRun(runnable)); + } + + /** + * Executes the given action when this AsyncResult completes and returns a new AsyncResult with the new value. + * + * @param function The action to perform. + * @param The type of the new value. + * @return A new AsyncResult with the new value. + */ + public AsyncResult thenApply(Function function) { + return new AsyncResult<>(future.thenApply(function)); + } + + /** + * Executes the given action when this AsyncResult completes with an exception. + * + * @param consumer The action to perform. + * @return This AsyncResult. + */ + public AsyncResult exceptionally(Consumer consumer) { + return new AsyncResult<>(future.exceptionally(throwable -> { + consumer.accept(throwable); + return null; + })); + } + + /** + * Executes the given action when this AsyncResult completes with an exception and returns a new AsyncResult with the new value. + * + * @param function The action to perform. + * @return A new AsyncResult with the new value. + */ + public AsyncResult exceptionally(Function function) { + return new AsyncResult<>(future.exceptionally(function)); + } +} diff --git a/src/main/java/org/mvplugins/multiverse/core/world/helpers/PlayerWorldTeleporter.java b/src/main/java/org/mvplugins/multiverse/core/world/helpers/PlayerWorldTeleporter.java index 29321f78..e340767d 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/helpers/PlayerWorldTeleporter.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/helpers/PlayerWorldTeleporter.java @@ -1,8 +1,6 @@ package org.mvplugins.multiverse.core.world.helpers; -import java.util.Collections; import java.util.List; -import java.util.concurrent.CompletableFuture; import jakarta.inject.Inject; import org.bukkit.Bukkit; @@ -14,6 +12,7 @@ import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.teleportation.AsyncSafetyTeleporter; import org.mvplugins.multiverse.core.teleportation.TeleportResult; +import org.mvplugins.multiverse.core.utils.result.AsyncResult; import org.mvplugins.multiverse.core.utils.result.Result; import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; @@ -34,7 +33,7 @@ public class PlayerWorldTeleporter { * * @param world The world to remove all players from. */ - public CompletableFuture>[] + public AsyncResult>> removeFromWorld(@NotNull LoadedMultiverseWorld world) { // TODO: Better handling of fallback world World toWorld = Bukkit.getWorlds().get(0); @@ -47,7 +46,7 @@ public class PlayerWorldTeleporter { * @param from The world to transfer players from. * @param to The location to transfer players to. */ - public CompletableFuture>[] + public AsyncResult>> transferFromWorldTo(@NotNull LoadedMultiverseWorld from, @NotNull LoadedMultiverseWorld to) { return transferAllFromWorldToLocation(from, to.getSpawnLocation()); } @@ -58,7 +57,7 @@ public class PlayerWorldTeleporter { * @param from The world to transfer players from. * @param to The world to transfer players to. */ - public CompletableFuture>[] + public AsyncResult>> transferFromWorldTo(@NotNull LoadedMultiverseWorld from, @NotNull World to) { return transferAllFromWorldToLocation(from, to.getSpawnLocation()); } @@ -70,11 +69,12 @@ public class PlayerWorldTeleporter { * @param location The location to transfer players to. * @return A list of futures that represent the teleportation of each player. */ - public CompletableFuture>[] + public AsyncResult>> transferAllFromWorldToLocation(@NotNull LoadedMultiverseWorld world, @NotNull Location location) { return world.getPlayers() .map(players -> safetyTeleporter.teleport(players, location)) - .getOrElse(new CompletableFuture[0]); + .getOrElse(() -> AsyncResult.failedFuture( + new IllegalStateException("Unable to get players from world" + world.getName()))); } /** @@ -83,7 +83,7 @@ public class PlayerWorldTeleporter { * @param players The players to teleport. * @param world The world to teleport players to. */ - public CompletableFuture>[] + public AsyncResult>> teleportPlayersToWorld(@NotNull List players, @NotNull LoadedMultiverseWorld world) { Location spawnLocation = world.getSpawnLocation(); return safetyTeleporter.teleport(players, spawnLocation);