From aaa6386ca373c0588461da49983987a48745dc43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Tue, 6 Oct 2020 15:50:16 +0200 Subject: [PATCH] :sparkles: Add a system for creating task chains This will make it easier to use the asynchronous coordinator. --- .../parser/StandardParserRegistry.java | 2 +- cloud-minecraft/cloud-bukkit/build.gradle | 1 + .../bukkit/BukkitCommandManager.java | 17 ++ .../bukkit/BukkitSynchronizer.java | 76 ++++++ .../commandframework/tasks/TaskConsumer.java | 44 ++++ .../commandframework/tasks/TaskFactory.java | 53 ++++ .../commandframework/tasks/TaskFunction.java | 54 ++++ .../commandframework/tasks/TaskRecipe.java | 237 ++++++++++++++++++ .../tasks/TaskRecipeStep.java | 30 +++ .../tasks/TaskSynchronizer.java | 77 ++++++ .../commandframework/tasks/package-info.java | 28 +++ .../examples/bukkit/ExamplePlugin.java | 64 +++-- settings.gradle | 2 +- 13 files changed, 660 insertions(+), 25 deletions(-) create mode 100644 cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitSynchronizer.java create mode 100644 cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskConsumer.java create mode 100644 cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskFactory.java create mode 100644 cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskFunction.java create mode 100644 cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskRecipe.java create mode 100644 cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskRecipeStep.java create mode 100644 cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskSynchronizer.java create mode 100644 cloud-tasks/src/main/java/cloud/commandframework/tasks/package-info.java diff --git a/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParserRegistry.java b/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParserRegistry.java index f2e9fb7f..98c53c30 100644 --- a/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParserRegistry.java +++ b/cloud-core/src/main/java/cloud/commandframework/arguments/parser/StandardParserRegistry.java @@ -288,7 +288,7 @@ public final class StandardParserRegistry implements ParserRegistry { @NonNull ParserParameters> { @Override - public @NonNull ParserParameters apply(@NonNull final Greedy greedy, @NonNull final TypeToken typeToken) { + public @NonNull ParserParameters apply(final @NonNull Greedy greedy, final @NonNull TypeToken typeToken) { return ParserParameters.single(StandardParameters.GREEDY, true); } diff --git a/cloud-minecraft/cloud-bukkit/build.gradle b/cloud-minecraft/cloud-bukkit/build.gradle index 93cce3ec..63978e09 100644 --- a/cloud-minecraft/cloud-bukkit/build.gradle +++ b/cloud-minecraft/cloud-bukkit/build.gradle @@ -1,6 +1,7 @@ dependencies { api project(':cloud-core') api project(':cloud-brigadier') + api project(':cloud-tasks') compileOnly 'org.bukkit:bukkit:1.13.2-R0.1-SNAPSHOT' compileOnly 'me.lucko:commodore:1.9' } diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java index 36b7ab28..dda1c222 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java @@ -38,6 +38,8 @@ import cloud.commandframework.bukkit.parsers.selector.MultiplePlayerSelectorArgu import cloud.commandframework.bukkit.parsers.selector.SingleEntitySelectorArgument; import cloud.commandframework.bukkit.parsers.selector.SinglePlayerSelectorArgument; import cloud.commandframework.execution.CommandExecutionCoordinator; +import cloud.commandframework.tasks.TaskFactory; +import cloud.commandframework.tasks.TaskRecipe; import io.leangen.geantyref.TypeToken; import org.bukkit.Bukkit; import org.bukkit.Material; @@ -73,6 +75,9 @@ public class BukkitCommandManager extends CommandManager { private final Function commandSenderMapper; private final Function backwardsCommandSenderMapper; + private final BukkitSynchronizer bukkitSynchronizer; + private final TaskFactory taskFactory; + private boolean splitAliases = false; /** @@ -96,6 +101,9 @@ public class BukkitCommandManager extends CommandManager { this.commandSenderMapper = commandSenderMapper; this.backwardsCommandSenderMapper = backwardsCommandSenderMapper; + this.bukkitSynchronizer = new BukkitSynchronizer(owningPlugin); + this.taskFactory = new TaskFactory(this.bukkitSynchronizer); + /* Try to determine the Minecraft version */ int version = -1; try { @@ -142,6 +150,15 @@ public class BukkitCommandManager extends CommandManager { new MultiplePlayerSelectorArgument.MultiplePlayerSelectorParser<>()); } + /** + * Create a new task recipe. This can be used to create chains of synchronous/asynchronous method calls + * + * @return Task recipe + */ + public @NonNull TaskRecipe taskRecipe() { + return this.taskFactory.recipe(); + } + /** * Get the plugin that owns the manager * diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitSynchronizer.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitSynchronizer.java new file mode 100644 index 00000000..f919b8d3 --- /dev/null +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitSynchronizer.java @@ -0,0 +1,76 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.bukkit; + +import cloud.commandframework.tasks.TaskConsumer; +import cloud.commandframework.tasks.TaskFunction; +import cloud.commandframework.tasks.TaskSynchronizer; +import org.bukkit.plugin.Plugin; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.concurrent.CompletableFuture; + +final class BukkitSynchronizer implements TaskSynchronizer { + + private final Plugin plugin; + + BukkitSynchronizer(final @NonNull Plugin plugin) { + this.plugin = plugin; + } + + @Override + public CompletableFuture runSynchronous(final @NonNull I input, final @NonNull TaskConsumer consumer) { + final CompletableFuture future = new CompletableFuture<>(); + this.plugin.getServer().getScheduler().runTask(this.plugin, () -> { + consumer.accept(input); + future.complete(null); + }); + return future; + } + + @Override + public CompletableFuture runSynchronous(final @NonNull I input, final @NonNull TaskFunction function) { + final CompletableFuture future = new CompletableFuture<>(); + this.plugin.getServer().getScheduler().runTask(this.plugin, () -> future.complete(function.apply(input))); + return future; + } + + @Override + public CompletableFuture runAsynchronous(final @NonNull I input, final @NonNull TaskConsumer consumer) { + final CompletableFuture future = new CompletableFuture<>(); + this.plugin.getServer().getScheduler().runTaskAsynchronously(this.plugin, () -> { + consumer.accept(input); + future.complete(null); + }); + return future; + } + + @Override + public CompletableFuture runAsynchronous(final @NonNull I input, final @NonNull TaskFunction function) { + final CompletableFuture future = new CompletableFuture<>(); + this.plugin.getServer().getScheduler().runTaskAsynchronously(this.plugin, () -> future.complete(function.apply(input))); + return future; + } + +} diff --git a/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskConsumer.java b/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskConsumer.java new file mode 100644 index 00000000..765d69fd --- /dev/null +++ b/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskConsumer.java @@ -0,0 +1,44 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.tasks; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.function.Consumer; + +/** + * Task step that does not produce any output + * + * @param Input type + */ +@FunctionalInterface +public interface TaskConsumer extends Consumer<@NonNull I>, TaskRecipeStep { + + /** + * {@inheritDoc} + */ + @Override + void accept(@NonNull I input); + +} diff --git a/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskFactory.java b/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskFactory.java new file mode 100644 index 00000000..957cf364 --- /dev/null +++ b/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskFactory.java @@ -0,0 +1,53 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.tasks; + +import org.checkerframework.checker.nullness.qual.NonNull; + +/** + * Factory that produces {@link TaskRecipe recipes} + */ +public final class TaskFactory { + + private final TaskSynchronizer synchronizer; + + /** + * Construct a new task factory + * + * @param synchronizer Synchronizer that will be passed onto the task recipes + */ + public TaskFactory(final @NonNull TaskSynchronizer synchronizer) { + this.synchronizer = synchronizer; + } + + /** + * Create a new {@link TaskRecipe} + * + * @return New recipe instance + */ + public @NonNull TaskRecipe recipe() { + return new TaskRecipe(this.synchronizer); + } + +} diff --git a/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskFunction.java b/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskFunction.java new file mode 100644 index 00000000..ca0ee3e7 --- /dev/null +++ b/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskFunction.java @@ -0,0 +1,54 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.tasks; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.function.Function; + +/** + * Task step that produces output from given input + * + * @param Input type + * @param Output type + */ +public interface TaskFunction extends Function<@NonNull I, @NonNull O>, TaskRecipeStep { + + /** + * {@inheritDoc} + */ + @Override + @NonNull O apply(@NonNull I input); + + /** + * Equivalent to {@link Function#identity()} + * + * @param Input type + * @return Function that maps the input to itself + */ + static TaskFunction identity() { + return i -> i; + } + +} diff --git a/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskRecipe.java b/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskRecipe.java new file mode 100644 index 00000000..64781e24 --- /dev/null +++ b/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskRecipe.java @@ -0,0 +1,237 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.tasks; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiConsumer; + +/** + * A task recipe is a chain of tasks with optional synchronization steps, + * that can be used to produce some sort of result from some input + */ +@SuppressWarnings("ALL") +public final class TaskRecipe { + + private final TaskSynchronizer synchronizer; + private final LinkedHashMap recipeSteps = new LinkedHashMap<>(); + + TaskRecipe(final @NonNull TaskSynchronizer synchronizer) { + this.synchronizer = synchronizer; + } + + /** + * Begin the recipe. This step always runs asynchronously. + * + * @param input Input + * @param Input type + * @return Function that maps the input to itself + */ + public @NonNull TaskRecipeComponentOutputting begin(final @NonNull I input) { + this.addAsynchronous(TaskFunction.identity()); + return new TaskRecipeComponentOutputting<>(input); + } + + private void addAsynchronous(final TaskRecipeStep taskRecipeStep) { + this.recipeSteps.put(taskRecipeStep, false); + } + + private void addSynchronous(final TaskRecipeStep taskRecipeStep) { + this.recipeSteps.put(taskRecipeStep, true); + } + + private void execute(final @NonNull Object initialInput, final @NonNull Runnable callback) { + final Iterator> iterator = new LinkedHashMap<>(this.recipeSteps).entrySet().iterator(); + CompletableFuture completableFuture = CompletableFuture.completedFuture(initialInput); + completableFuture.whenComplete(this.execute(iterator, callback)); + } + + private BiConsumer execute(final @NonNull Iterator> iterator, + final @NonNull Runnable callback) { + return (o, o2) -> { + if (iterator.hasNext()) { + final Map.Entry entry = iterator.next(); + final boolean synchronous = entry.getValue(); + + CompletableFuture other; + if (entry.getKey() instanceof TaskFunction) { + final TaskFunction function = (TaskFunction) entry.getKey(); + if (synchronous) { + other = this.synchronizer.runSynchronous(o, function); + } else { + other = this.synchronizer.runAsynchronous(o, function); + } + } else { + final TaskConsumer consumer = (TaskConsumer) entry.getKey(); + if (synchronous) { + other = this.synchronizer.runSynchronous(o, consumer); + } else { + other = this.synchronizer.runAsynchronous(o, consumer); + } + } + + other.whenComplete(this.execute(iterator, callback)); + } else { + callback.run(); + } + }; + } + + + /** + * Represents a partial recipe + * + * @param Input type + * @param Output type + */ + public final class TaskRecipeComponentOutputting { + + private final Object initialInput; + + private TaskRecipeComponentOutputting(final @NonNull Object initialInput) { + this.initialInput = initialInput; + } + + /** + * Add a new synchronous step, consuming the input of the earlier step + * + * @param function Function mapping the input to some output + * @param Output type + * @return New task recipe component + */ + public TaskRecipeComponentOutputting synchronous(final @NonNull TaskFunction function) { + addSynchronous(function); + return new TaskRecipeComponentOutputting<>(this.initialInput); + } + + /** + * Add a new asynchronous step, consuming the input of the earlier step + * + * @param function Function mapping the input to some output + * @param Output type + * @return New task recipe component + */ + public TaskRecipeComponentOutputting asynchronous(final @NonNull TaskFunction function) { + addAsynchronous(function); + return new TaskRecipeComponentOutputting<>(this.initialInput); + } + + /** + * Add a new synchronous step, consuming the input of the earlier step + * + * @param consumer Consumer that consumes the input + * @return New task recipe component + */ + public TaskRecipeComponentVoid synchronous(final @NonNull TaskConsumer consumer) { + addSynchronous(consumer); + return new TaskRecipeComponentVoid<>(initialInput); + } + + /** + * Add a new asynchronous step, consuming the input of the earlier step + * + * @param consumer Consumer that consumes the input + * @return New task recipe component + */ + public TaskRecipeComponentVoid asynchronous(final @NonNull TaskConsumer consumer) { + addSynchronous(consumer); + return new TaskRecipeComponentVoid<>(initialInput); + } + + /** + * Execute the recipe + * + * @param callback Callback function + */ + public void execute(final @NonNull Runnable callback) { + TaskRecipe.this.execute(initialInput, callback); + } + + /** + * Execute the recipe + */ + public void execute() { + this.execute(() -> {}); + } + + } + + /** + * Represents a partial recipe + * + * @param Input type + */ + public final class TaskRecipeComponentVoid { + + private final Object initialInput; + + private TaskRecipeComponentVoid(final @NonNull Object initialInput) { + this.initialInput = initialInput; + } + + /** + * Add a new synchronous step, consuming the input of the earlier step + * + * @param consumer Consumer that consumes the input + * @return New task recipe component + */ + public TaskRecipeComponentVoid synchronous(final @NonNull TaskConsumer consumer) { + addSynchronous(consumer); + return new TaskRecipeComponentVoid<>(initialInput); + } + + /** + * Add a new asynchronous step, consuming the input of the earlier step + * + * @param consumer Consumer that consumes the input + * @return New task recipe component + */ + public TaskRecipeComponentVoid asynchronous(final @NonNull TaskConsumer consumer) { + addSynchronous(consumer); + return new TaskRecipeComponentVoid<>(initialInput); + } + + /** + * Execute the recipe + * + * @param callback Callback function + */ + public void execute(final @NonNull Runnable callback) { + TaskRecipe.this.execute(initialInput, callback); + } + + /** + * Execute the recipe + */ + public void execute() { + this.execute(() -> {}); + } + + } + +} diff --git a/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskRecipeStep.java b/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskRecipeStep.java new file mode 100644 index 00000000..7684e2e1 --- /dev/null +++ b/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskRecipeStep.java @@ -0,0 +1,30 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.tasks; + +/** + * Part of a {@link TaskRecipe} + */ +public interface TaskRecipeStep { +} diff --git a/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskSynchronizer.java b/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskSynchronizer.java new file mode 100644 index 00000000..49333b85 --- /dev/null +++ b/cloud-tasks/src/main/java/cloud/commandframework/tasks/TaskSynchronizer.java @@ -0,0 +1,77 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.tasks; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.concurrent.CompletableFuture; + +/** + * Utility responsible for synchronizing {@link TaskRecipeStep task recipe steps} + */ +public interface TaskSynchronizer { + + /** + * Accept input into the consumer synchronously + * + * @param input Input to pass to the consumer + * @param consumer Consumer of the input + * @param Input type + * @return Future that completes when the consumer is done + */ + CompletableFuture runSynchronous(@NonNull I input, @NonNull TaskConsumer consumer); + + /** + * Produce output from accepted input synchronously + * + * @param input Input to pass to the function + * @param function Function that produces the output + * @param Input type + * @param Output type + * @return Future that completes with the output + */ + CompletableFuture runSynchronous(@NonNull I input, @NonNull TaskFunction function); + + /** + * Accept input into the consumer asynchronously + * + * @param input Input to pass to the consumer + * @param consumer Consumer of the input + * @param Input type + * @return Future that completes when the consumer is done + */ + CompletableFuture runAsynchronous(@NonNull I input, @NonNull TaskConsumer consumer); + + /** + * Produce output from accepted input asynchronously + * + * @param input Input to pass to the function + * @param function Function that produces the output + * @param Input type + * @param Output type + * @return Future that completes with the output + */ + CompletableFuture runAsynchronous(@NonNull I input, @NonNull TaskFunction function); + +} diff --git a/cloud-tasks/src/main/java/cloud/commandframework/tasks/package-info.java b/cloud-tasks/src/main/java/cloud/commandframework/tasks/package-info.java new file mode 100644 index 00000000..ac50e963 --- /dev/null +++ b/cloud-tasks/src/main/java/cloud/commandframework/tasks/package-info.java @@ -0,0 +1,28 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +/** + * Cloud task library + */ +package cloud.commandframework.tasks; diff --git a/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java b/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java index c13e5fe4..997e9184 100644 --- a/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java +++ b/examples/example-bukkit/src/main/java/cloud/commandframework/examples/bukkit/ExamplePlugin.java @@ -44,6 +44,7 @@ import cloud.commandframework.bukkit.CloudBukkitCapabilities; import cloud.commandframework.bukkit.arguments.selector.SingleEntitySelector; import cloud.commandframework.bukkit.parsers.WorldArgument; import cloud.commandframework.bukkit.parsers.selector.SingleEntitySelectorArgument; +import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator; import cloud.commandframework.execution.CommandExecutionCoordinator; import cloud.commandframework.extra.confirmation.CommandConfirmationManager; import cloud.commandframework.meta.CommandMeta; @@ -53,6 +54,7 @@ import io.leangen.geantyref.TypeToken; import net.kyori.adventure.platform.bukkit.BukkitAudiences; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; import org.bukkit.ChatColor; import org.bukkit.Location; import org.bukkit.Material; @@ -86,13 +88,13 @@ public final class ExamplePlugin extends JavaPlugin { // This is a function that will provide a command execution coordinator that parses and executes commands // asynchronously // - // final Function, CommandExecutionCoordinator> executionCoordinatorFunction = - // AsynchronousCommandExecutionCoordinator.newBuilder().build(); - // - // However, in this example it is fine for us to run everything synchronously - // final Function, CommandExecutionCoordinator> executionCoordinatorFunction = - CommandExecutionCoordinator.simpleCoordinator(); + AsynchronousCommandExecutionCoordinator.newBuilder().build(); + // + // However, in many cases it is fine for to run everything synchronously: + // + // final Function, CommandExecutionCoordinator> executionCoordinatorFunction = + // CommandExecutionCoordinator.simpleCoordinator(); // // This function maps the command sender type of our choice to the bukkit command sender. // However, in this example we use the Bukkit command sender, and so we just need to map it @@ -207,29 +209,45 @@ public final class ExamplePlugin extends JavaPlugin { Triplet.of(Integer.class, Integer.class, Integer.class), triplet -> new Vector(triplet.getFirst(), triplet.getSecond(), triplet.getThird()), Description.of("Coordinates")) - .handler(context -> { - final Player player = (Player) context.getSender(); - final World world = context.get(worldArgument); - final Vector coords = context.get("coords"); - final Location location = coords.toLocation(world); - player.teleport(location); - })) + .handler(context -> manager.taskRecipe().begin(context) + .synchronous(commandContext -> { + final Player player = (Player) commandContext.getSender(); + final World world = commandContext.get(worldArgument); + final Vector coords = commandContext.get("coords"); + final Location location = coords.toLocation(world); + player.teleport(location); + }).execute())) .command(builder.literal("teleport") .literal("entity") .withSenderType(Player.class) .argument(SingleEntitySelectorArgument.of("entity"), Description.of("Entity to teleport")) .literal("here") - .handler(commandContext -> { - final Player player = (Player) commandContext.getSender(); - final SingleEntitySelector singleEntitySelector = commandContext.get("entity"); - if (singleEntitySelector.hasAny()) { - singleEntitySelector.getEntity().teleport(player); - player.sendMessage(ChatColor.GREEN + "The entity was teleported to you!"); - } else { - player.sendMessage(ChatColor.RED + "No entity matched your query."); - } - })); + .handler( + context -> manager.taskRecipe().begin(context) + .synchronous(commandContext -> { + final Player player = (Player) commandContext.getSender(); + final SingleEntitySelector singleEntitySelector = commandContext.get("entity"); + if (singleEntitySelector.hasAny()) { + singleEntitySelector.getEntity().teleport(player); + player.sendMessage(ChatColor.GREEN + "The entity was teleported to you!"); + } else { + player.sendMessage(ChatColor.RED + "No entity matched your query."); + } + }).execute() + )); + manager.command(builder.literal("tasktest") + .handler(context -> manager.taskRecipe() + .begin(context) + .asynchronous(c -> { + c.getSender().sendMessage("ASYNC: " + !Bukkit.isPrimaryThread()); + return c; + }) + .synchronous(c -> { + c.getSender().sendMessage("SYNC: " + Bukkit.isPrimaryThread()); + }) + .execute(() -> context.getSender().sendMessage("DONE!")) + )); } @CommandMethod("example help [query]") diff --git a/settings.gradle b/settings.gradle index 2377eeb4..b8eaf0fc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,6 +13,7 @@ include(':cloud-cloudburst') include(':cloud-javacord') include(':cloud-jda') include(':example-bukkit') +include(':cloud-tasks') project(':cloud-bukkit').projectDir = file('cloud-minecraft/cloud-bukkit') project(':cloud-paper').projectDir = file('cloud-minecraft/cloud-paper') project(':cloud-brigadier').projectDir = file('cloud-minecraft/cloud-brigadier') @@ -23,4 +24,3 @@ project(':cloud-cloudburst').projectDir = file('cloud-minecraft/cloud-cloudburst project(':cloud-javacord').projectDir = file('cloud-discord/cloud-javacord') project(':cloud-jda').projectDir = file('cloud-discord/cloud-jda') project(':example-bukkit').projectDir = file('examples/example-bukkit') -