diff --git a/README.md b/README.md index a9c9e71a..6aa0143c 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,14 @@ the existing APIs with suspendable commands, events and schedules. **Supported Game Servers:** -* Spigot -* Paper * CraftBukkit -* SpongeVanilla v7.x.x -* SpongeForge v7.x.x * Fabric +* Folia * Minestom +* Paper +* Spigot +* SpongeVanilla v7.x.x +* SpongeForge v7.x.x **Supported Proxies:** @@ -67,10 +68,11 @@ private suspend fun bob() { ## Resources * [MCCoroutine JavaDocs for the Bukkit-API](https://shynixn.github.io/MCCoroutine/apidocs/mccoroutine-root/com.github.shynixn.mccoroutine.bukkit/index.html) -* [MCCoroutine JavaDocs for the Sponge-v7.x.x-API](https://shynixn.github.io/MCCoroutine/apidocs/mccoroutine-root/com.github.shynixn.mccoroutine.sponge/index.html) +* [MCCoroutine JavaDocs for the BungeeCord-API](https://shynixn.github.io/MCCoroutine/apidocs/mccoroutine-root/com.github.shynixn.mccoroutine.bungeecord/index.html) * [MCCoroutine JavaDocs for the Fabric-API](https://shynixn.github.io/MCCoroutine/apidocs/mccoroutine-root/com.github.shynixn.mccoroutine.fabric/index.html) +* [MCCoroutine JavaDocs for the Folia-API](https://shynixn.github.io/MCCoroutine/apidocs/mccoroutine-root/com.github.shynixn.mccoroutine.folia/index.html) * [MCCoroutine JavaDocs for the Minestom-API](https://shynixn.github.io/MCCoroutine/apidocs/mccoroutine-root/com.github.shynixn.mccoroutine.minestom/index.html) -* [MCCoroutine JavaDocs for the BungeeCord-API](https://shynixn.github.io/MCCoroutine/apidocs/mccoroutine-root/com.github.shynixn.mccoroutine.bungeecord/index.html) +* [MCCoroutine JavaDocs for the Sponge-v7.x.x-API](https://shynixn.github.io/MCCoroutine/apidocs/mccoroutine-root/com.github.shynixn.mccoroutine.sponge/index.html) * [MCCoroutine JavaDocs for the Velocity-API](https://shynixn.github.io/MCCoroutine/apidocs/mccoroutine-root/com.github.shynixn.mccoroutine.velocity/index.html) * [Article on custom frameworks](https://github.com/Shynixn/MCCoroutine/blob/master/ARTICLE.md) diff --git a/build.gradle b/build.gradle index 43acd0b5..eff04b07 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,7 @@ tasks.register("printVersion") { subprojects { group 'com.github.shynixn.mccoroutine' - version '2.12.1' + version '2.13.0' sourceCompatibility = 1.8 diff --git a/docs/wiki/docs/README.md b/docs/wiki/docs/README.md index 5590ff00..b7cd629e 100644 --- a/docs/wiki/docs/README.md +++ b/docs/wiki/docs/README.md @@ -17,19 +17,20 @@ the existing APIs with suspendable commands, events and schedules. **Supported Game Servers:** -* Spigot -* Paper * CraftBukkit -* SpongeVanilla -* SpongeForge * Fabric +* Folia * Minestom +* Paper +* Spigot +* SpongeVanilla +* SpongeForge **Supported Proxies:** * BungeeCord -* Waterfall * Velocity +* Waterfall ## Features diff --git a/docs/wiki/docs/caching.md b/docs/wiki/docs/caching.md index 797941a8..19012fba 100644 --- a/docs/wiki/docs/caching.md +++ b/docs/wiki/docs/caching.md @@ -6,8 +6,10 @@ In minecraft plugins, players can perform many actions in a short time period. I every action in the database, creating a new database call for every single action may cause performance problems. Therefore, caches are often implemented, which is a lot easier when using coroutines. +!!! note "Important" + The following code examples are for Bukkit, but work in a similar way in other mccoroutine implementations. -## Implementing a Cache (Bukkit) +## Implementing a Cache When taking a look at the ``Database`` implementation below, we can observe quite a lot of redundant database accesses when a player rejoins a server in a very short timeframe. diff --git a/docs/wiki/docs/commandexecutor.md b/docs/wiki/docs/commandexecutor.md index 3cbce7b7..c18a6a58 100644 --- a/docs/wiki/docs/commandexecutor.md +++ b/docs/wiki/docs/commandexecutor.md @@ -61,6 +61,86 @@ plugins. } ```` +=== "Fabric" + + Create a traditional command executor but extend from ``SuspendingCommand`` instead of ``SuspendingCommand``. + + ````kotlin + import com.github.shynixn.mccoroutine.fabric.SuspendingCommand + import com.mojang.brigadier.context.CommandContext + import net.minecraft.entity.player.PlayerEntity + import net.minecraft.server.command.ServerCommandSource + + class PlayerDataCommandExecutor : SuspendingCommand { + override suspend fun run(context: CommandContext): Int { + if (context.source.entity is PlayerEntity) { + val sender = context.source.entity as PlayerEntity + println("[PlayerDataCommandExecutor] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + } + + return 1 + } + } + ```` + +=== "Folia" + + Folia schedules threads on the region of the entity who executed this command. For the console (globalregion) and command blocks (region) this rule + applies as well. Other than that, usage is almost identical to Bukkit. + + ````kotlin + import com.github.shynixn.mccoroutine.folia.SuspendingCommandExecutor + import org.bukkit.command.Command + import org.bukkit.command.CommandSender + import org.bukkit.entity.Player + + class PlayerDataCommandExecutor(private val database: Database) : SuspendingCommandExecutor { + override suspend fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean { + if (sender !is Player) { + return false + } + + if (args.size == 2 && args[0].equals("rename", true)) { + val name = args[1] + val playerData = database.getDataFromPlayer(sender) + playerData.name = name + database.saveData(sender, playerData) + return true + } + + return false + } + } + ```` + +=== "Minestom" + + Create a traditional command and user ``server.launch`` or ``extension.launch`` in the addSyntax handler. + + ````kotlin + import com.github.shynixn.mccoroutine.minestom.launch + import net.minestom.server.MinecraftServer + import net.minestom.server.command.builder.Command + import net.minestom.server.command.builder.arguments.ArgumentType + import net.minestom.server.entity.Player + + class PlayerDataCommandExecutor(private val server: MinecraftServer, private val database: Database) : Command("mycommand") { + init { + val nameArgument = ArgumentType.String("name") + addSyntax({ sender, context -> + server.launch { + if (sender is Player) { + val name : String = context.get(nameArgument) + val playerData = database.getDataFromPlayer(sender) + playerData.name = name + database.saveData(sender, playerData) + } + } + }) + } + } + ```` + === "Sponge" Create a traditional command executor but extend from ``SuspendingCommandExecutor`` instead of ``CommandExecutor``. Please @@ -127,56 +207,6 @@ plugins. A ``BrigadierCommand`` can be executed asynchronously using the ``executesSuspend`` extension function. More details below. -=== "Minestom" - - Create a traditional command and user ``server.launch`` or ``extension.launch`` in the addSyntax handler. - - ````kotlin - import com.github.shynixn.mccoroutine.minestom.launch - import net.minestom.server.MinecraftServer - import net.minestom.server.command.builder.Command - import net.minestom.server.command.builder.arguments.ArgumentType - import net.minestom.server.entity.Player - - class PlayerDataCommandExecutor(private val server: MinecraftServer, private val database: Database) : Command("mycommand") { - init { - val nameArgument = ArgumentType.String("name") - addSyntax({ sender, context -> - server.launch { - if (sender is Player) { - val name : String = context.get(nameArgument) - val playerData = database.getDataFromPlayer(sender) - playerData.name = name - database.saveData(sender, playerData) - } - } - }) - } - } - ```` - -=== "Fabric" - - Create a traditional command executor but extend from ``SuspendingCommand`` instead of ``SuspendingCommand``. - - ````kotlin - import com.github.shynixn.mccoroutine.fabric.SuspendingCommand - import com.mojang.brigadier.context.CommandContext - import net.minecraft.entity.player.PlayerEntity - import net.minecraft.server.command.ServerCommandSource - - class PlayerDataCommandExecutor : SuspendingCommand { - override suspend fun run(context: CommandContext): Int { - if (context.source.entity is PlayerEntity) { - val sender = context.source.entity as PlayerEntity - println("[PlayerDataCommandExecutor] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") - } - - return 1 - } - } - ```` - ## Register the CommandExecutor === "Bukkit" @@ -235,6 +265,69 @@ plugins. } ```` +=== "Fabric" + + ````kotlin + class MCCoroutineSampleServerMod : DedicatedServerModInitializer { + override fun onInitializeServer() { + ServerLifecycleEvents.SERVER_STARTING.register(ServerLifecycleEvents.ServerStarting { server -> + // Connect Native Minecraft Scheduler and MCCoroutine. + mcCoroutineConfiguration.minecraftExecutor = Executor { r -> + server.submitAndJoin(r) + } + launch { + onServerStarting(server) + } + }) + + ServerLifecycleEvents.SERVER_STOPPING.register { server -> + mcCoroutineConfiguration.disposePluginSession() + } + } + + /** + * MCCoroutine is ready after the server has started. + */ + private suspend fun onServerStarting(server : MinecraftServer) { + // Register command + val command = PlayerDataCommandExecutor() + server.commandManager.dispatcher.register(CommandManager.literal("mccor").executesSuspend(this, command)) + } + } + ```` + +=== "Folia" + + Instead of using ``setExecutor``, use the provided extension method ``setSuspendingExecutor`` to register a command executor. + + !!! note "Important" + Do not forget to declare the ``playerdata`` command in your plugin.yml. + + ````kotlin + import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin + import com.github.shynixn.mccoroutine.folia.registerSuspendingEvents + import com.github.shynixn.mccoroutine.folia.setSuspendingExecutor + + class MCCoroutineSamplePlugin : SuspendingJavaPlugin() { + private val database = Database() + + override suspend fun onEnableAsync() { + // Minecraft Main Thread + database.createDbIfNotExist() + server.pluginManager.registerSuspendingEvents(PlayerDataListener(database), this) + getCommand("playerdata")!!.setSuspendingExecutor(PlayerDataCommandExecutor(database)) + } + + override suspend fun onDisableAsync() { + // Minecraft Main Thread + } + } + ```` + +=== "Minestom" + + Register the command in the same way as a traditional command. + === "Sponge" Instead of using ``executor``, use the provided extension method ``suspendingExecutor`` to register a command executor. @@ -335,41 +428,6 @@ plugins. } ```` -=== "Minestom" - - Register the command in the same way as a traditional command. - -=== "Fabric" - - ````kotlin - class MCCoroutineSampleServerMod : DedicatedServerModInitializer { - override fun onInitializeServer() { - ServerLifecycleEvents.SERVER_STARTING.register(ServerLifecycleEvents.ServerStarting { server -> - // Connect Native Minecraft Scheduler and MCCoroutine. - mcCoroutineConfiguration.minecraftExecutor = Executor { r -> - server.submitAndJoin(r) - } - launch { - onServerStarting(server) - } - }) - - ServerLifecycleEvents.SERVER_STOPPING.register { server -> - mcCoroutineConfiguration.disposePluginSession() - } - } - - /** - * MCCoroutine is ready after the server has started. - */ - private suspend fun onServerStarting(server : MinecraftServer) { - // Register command - val command = PlayerDataCommandExecutor() - server.commandManager.dispatcher.register(CommandManager.literal("mccor").executesSuspend(this, command)) - } - } - ```` - ## Test the CommandExecutor Join your server and use the playerData command to observe ``getDataFromPlayer`` and ``saveData`` messages getting diff --git a/docs/wiki/docs/coroutine.md b/docs/wiki/docs/coroutine.md index 0d86b70e..e4aa898f 100644 --- a/docs/wiki/docs/coroutine.md +++ b/docs/wiki/docs/coroutine.md @@ -9,9 +9,9 @@ before you continue here. ### Starting a coroutine -For beginners, it is often confusing how to enter a coroutine. The examples in the official guide mostly -use ``runBlocking`` -because it makes sense for testing. However, keep in mind to **avoid** using ``runblocking`` in any of your plugins. +In order to start coroutine You may also encounter the function +``runBlocking`` because it makes sense for certain scenarios such as unittest. +However, keep in mind to **avoid** using ``runblocking`` in any of your plugins. * To enter a coroutine **anywhere** in your code at any time: @@ -24,6 +24,7 @@ because it makes sense for testing. However, keep in mind to **avoid** using ``r fun foo() { plugin.launch { // This will always be on the minecraft main thread. + // If you have been on the minecraft main thread before calling plugin.launch, this scope is entered immediately without any delay. } } ``` @@ -36,7 +37,54 @@ because it makes sense for testing. However, keep in mind to **avoid** using ``r fun foo() { plugin.launch { - // This will be a random thread on the BungeeCord threadpool + // This a random thread on the bungeeCord threadPool. + // If you have been on the bungeeCord threadPool before calling plugin.launch, this scope is executed in the next scheduler tick. + // If you pass CoroutineStart.UNDISPATCHED, you can enter this scope in the current tick. This is shown in a code example below. + } + } + ``` + +=== "Fabric" + + Fabric has got 3 lifecycle scopes, the ``ModInitializer`` (both client and server) ``ClientModInitializer`` (client) and ``DedicatedServerModInitializer`` scope. + This guide gives only ``DedicatedServerModInitializer`` examples but it works in the same way for the other scopes. + + ```kotlin + import com.github.shynixn.mccoroutine.fabric.launch + import net.fabricmc.api.DedicatedServerModInitializer + + fun foo(){ + mod.launch { + // This will always be on the minecraft main thread. + } + } + ``` + +=== "Folia" + + As Folia brings multithreading to Paper based servers, threading becomes more complicated and MCCoroutine requires you to think + everytime you call plugin.launch. In Bukkit based servers, MCCoroutine can assume the correct thread automatically and optimise ticking. (e.g. + not sending a task to the scheduler if you are already on the main thread). + + In Folia, there are many threadpools (explained below) and we do not have a main thread. + + !!! note "Important" + You can run mccoroutine-folia in standard Bukkit servers as well. MCCoroutine automatically falls back to the standard Bukkit + scheduler if the Folia schedulers are not found and the rules for mccoroutine-bukkit start to apply. + + ```kotlin + import com.github.shynixn.mccoroutine.folia.launch + import org.bukkit.plugin.Plugin + + fun foo(entity : Entity) { + // The plugin.entityDispatcher(entity) parameter ensures, that we end up on the scheduler for the entity in the specific region if we suspend + // inside the plugin.launch scope. (e.g. using delay) + // The CoroutineStart.UNDISPATCHED ensures, that we enter plugin.launch scope without any delay on the current thread. + // You are responsible to ensure that you are on the correct thread pool (in this case the thread pool for the entity), if you pass CoroutineStart.UNDISPATCHED. + // This is automatically the case if you use plugin.launch{} in events or commands. You can simply use CoroutineStart.UNDISPATCHED here. + // If you use CoroutineStart.DEFAULT, the plugin.launch scope is entered in the next scheduler tick. + plugin.launch(plugin.entityDispatcher(entity), CoroutineStart.UNDISPATCHED) { + // In this case this will be the correct thread for the given entity, if the thread was correct before calling plugin.launch. } } ``` @@ -98,126 +146,200 @@ because it makes sense for testing. However, keep in mind to **avoid** using ``r } ``` -=== "Fabric" +### Switching coroutine context - Fabric has got 3 lifecycle scopes, the ``ModInitializer`` (both client and server) ``ClientModInitializer`` (client) and ``DedicatedServerModInitializer`` scope. - This guide gives only ``DedicatedServerModInitializer`` examples but it works in the same way for the other scopes. +Later in the [Coroutines in Kotlin](https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html) guide, the terms +coroutine-context and dispatchers are explained. +A dispatcher determines what thread or threads the corresponding coroutine uses for its execution. - ```kotlin - import com.github.shynixn.mccoroutine.fabric.launch - import net.fabricmc.api.DedicatedServerModInitializer +=== "Bukkit" - fun foo(){ - mod.launch { + In Bukkit, MCCoroutine offers 2 custom dispatchers. + + * minecraftDispatcher (Allows to execute coroutines on the main minecraft thread) + * asyncDispatcher (Allows to execute coroutines on the async minecraft threadpool) + + !!! note "Important" + You may also use ``Dispatchers.IO`` instead of asyncDispatcher, to reduce the dependency on mccoroutine in your code. + + An example how this works is shown below: + + ```kotlin + fun foo() { + plugin.launch { // This will always be on the minecraft main thread. + // If you have been on the minecraft main thread before calling plugin.launch, this scope is entered immediately without any delay. + + val result1 = withContext(plugin.minecraftDispatcher) { + // Perform operations on the minecraft main thread. + "Player is " // Optionally, return a result. + } + + // Here we are automatically back on the main thread again. + + // Prefer using Dispatchers.IO instead of asyncDispatcher + val result2 = withContext(Dispatchers.IO) { + // Perform operations asynchronously. + " Max" + } + + // Here we are automatically back on the main thread again. + + println(result1 + result2) // Prints 'Player is Max' } } ``` + + Normally, you do not need to call ``plugin.minecraftDispatcher`` in your code. Instead, you are guaranteed to be always + on the minecraft main thread + in the ``plugin.launch{}`` scope and use sub coroutines (e.g. withContext) to perform asynchronous operations. Such a + case can be found below: + + ```kotlin + // This is a Bukkit example, but it works in the same way in every other framework. + @EventHandler + fun onPlayerJoinEvent(event: PlayerJoinEvent) { + plugin.launch { + // This will always be on the minecraft main thread. + // A PlayerJoinEvent arrives on the main thread, therefore this scope is entered immediately without any delay. + + val name = event.player.name + val listOfFriends = withContext(Dispatchers.IO) { + // IO Thread + val friendNames = Files.readAllLines(Paths.get("$name.txt")) + friendNames + } + + // Main Thread + val friendText = listOfFriends.joinToString(", ") + event.player.sendMessage("My friends are: $friendText") + } + } + + ``` + + ### Plugin launch Execution order + + If you use ``plugin.launch``, it is important to understand the execution order. + + ````kotlin + // This is a Bukkit example, but it works in the same way in every other framework. + class Foo(private val plugin: Plugin) { + + fun bar() { + // Main Thread + // If you have been on the minecraft main thread before calling plugin.launch, this scope is entered immediately without any delay. + println("I am first") + + val job = plugin.launch { + println("I am second") // The context is not suspended when switching to the same suspendable context. + delay(1000) + println("I am fourth") // The context is given back after 1000 milliseconds and continuous here. + bob() + } + + // When calling delay the suspendable context is suspended and the original context immediately continuous here. + println("I am third") + } + + private suspend fun bob() { + println("I am fifth") + } + } + ```` + + ````kotlin + "I am first" + "I am second" + "I am third" + "I am fourth" + "I am fifth" + ```` -### Switching coroutine context - -Later in the [Coroutines in Kotlin](https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html) guide, the terms -coroutine-context and dispatchers are explained. -A dispatcher determines what thread or threads the corresponding coroutine uses for its execution. Therefore, -MCCoroutine offers 2 custom dispatchers: +=== "BungeeCord" -* minecraftDispatcher (Allows to execute coroutines on the main minecraft thread) -* asyncDispatcher (Allows to execute coroutines on the async minecraft threadpool) + In BungeeCord, MCCoroutine offers 1 custom dispatcher. -!!! note "Important" - **However, it is highly recommend to use ``Dispatchers.IO`` instead of asyncDispatcher because the scheduling is more - accurate.** - Additional technical details can be found here: [GitHub Issue](https://github.com/Shynixn/MCCoroutine/issues/87). + * bungeeCordDispatcher (Allows to execute coroutines on the bungeeCord threadpool) + + An example how this works is shown below: + + ```kotlin + fun foo() { + plugin.launch { + // This a random thread on the bungeeCord threadPool. + // If you have been on the bungeeCord threadPool before calling plugin.launch, this scope is executed in the next scheduler tick. + // If you pass CoroutineStart.UNDISPATCHED, you can enter this scope in the current tick. This is shown in a code example below. + + val result = withContext(Dispatchers.IO) { + // Perform operations asynchronously. + "Playxer is Max" + } + + // Here we are automatically back on a new random thread on the bungeeCord threadPool. + println(result) // Prints 'Player is Max' + } + } + ``` -An example how this works is shown below: + ```kotlin + fun foo() { + plugin.launch(start = CoroutineStart.UNDISPATCHED) { + // This is the same thread before calling plugin.launch -```kotlin -fun foo() { - plugin.launch { - // This will always be on the minecraft main thread. + val result = withContext(Dispatchers.IO) { + // Perform operations asynchronously. + "Playxer is Max" + } - val result1 = withContext(plugin.minecraftDispatcher) { - // Perform operations on the minecraft main thread. - "Player is " // Optionally, return a result. + // Here we are automatically back on a new random thread on the bungeeCord threadPool. + println(result) // Prints 'Player is Max' } + } + ``` - // Here we are automatically back on the main thread again. +=== "Fabric" - // Prefer using Dispatchers.IO instead of asyncDispatcher - val result2 = withContext(Dispatchers.IO) { - // Perform operations asynchronously. - " Max" - } + TBD - // Here we are automatically back on the main thread again. +=== "Folia" - println(result1 + result2) // Prints 'Player is Max' - } -} -``` - -Normally, you do not need to call ``plugin.minecraftDispatcher`` in your code. Instead, you are guaranteed to be always -on the minecraft main thread -in the ``plugin.launch{}`` scope and use sub coroutines (e.g. withContext) to perform asynchronous operations. Such a -case can be found below: - -```kotlin -// This is a Bukkit example, but it works in the same way in every other framework. -@EventHandler -fun onPlayerJoinEvent(event: PlayerJoinEvent) { - plugin.launch { - // Main Thread - val name = event.player.name - val listOfFriends = withContext(Dispatchers.IO) { - // IO Thread - val friendNames = Files.readAllLines(Paths.get("$name.txt")) - friendNames - } + In Folia, MCCoroutine offers 4 custom dispatchers. - // Main Thread - val friendText = listOfFriends.joinToString(", ") - event.player.sendMessage("My friends are: $friendText") + * globalRegion (Allows to execute coroutines on the global region. e.g. Global Game Rules) + * regionDispatcher (Allows to execute coroutines on a specific location in a world) + * entityDispatcher (Allows to execute coroutines on a specific entity) + * asyncDispatcher (Allows to execute coroutines on the async thread pool) + + An example how this works is shown below: + + ```kotlin + fun foo(location: Location)) { + plugin.launch(plugin.regionDispatcher(location), CoroutineStart.UNDISPATCHED) { + // The correct thread for the given location without delay, if the thread was correct before calling plugin.launch. + + val result = withContext(Dispatchers.IO) { + // Perform operations asynchronously. + "Playxer is Max" + } + + // The correct thread for the given location. + println(result) // Prints 'Player is Max' + } } -} - -``` - -### Plugin launch Execution order + ``` -If you use ``plugin.launch``, it is important to understand the execution order. +=== "Sponge" -````kotlin -// This is a Bukkit example, but it works in the same way in every other framework. -class Foo(private val plugin: Plugin) { + TBD - fun bar() { - // Main Thread - println("I am first") +=== "Velocity" - val job = plugin.launch { - println("I am second") // The context is not suspended when switching to the same suspendable context. - delay(1000) - println("I am fourth") // The context is given back after 1000 milliseconds and continuous here. - bob() - } + TBD - // When calling delay the suspendable context is suspended and the original context immediately continuous here. - println("I am third") - } +=== "Minestom" - private suspend fun bob() { - println("I am fifth") - } -} -```` - -````kotlin -"I am first" -"I am second" -"I am third" -"I am fourth" -"I am fifth" -```` + TBD ### Coroutines everywhere diff --git a/docs/wiki/docs/exception.md b/docs/wiki/docs/exception.md index a5157743..cc4c9c6b 100644 --- a/docs/wiki/docs/exception.md +++ b/docs/wiki/docs/exception.md @@ -21,7 +21,7 @@ logger.log( You can handle exceptions by yourself by listening to the ``MCCoroutineExceptionEvent``. This event is sent to the event bus of the minecraft frame work (e.g. Bukkit, Sponge, BungeeCord) and can be used for logging. The following points should be considered: -* The event arrives at the main thread (Bukkit, Sponge, Minestom) +* The event arrives at the main thread in Bukkit, Sponge, Minestom. In Folia, it arrives on the globalRegionThread. * The event is also called for ``CoroutineCancellation`` * Exceptions arrive for every plugin using MCCoroutine. Check if ``event.plugin`` equals your plugin. * You can cancel the event to disable logging the event with the default exception behaviour diff --git a/docs/wiki/docs/installation.md b/docs/wiki/docs/installation.md index 6798153a..6bf44379 100644 --- a/docs/wiki/docs/installation.md +++ b/docs/wiki/docs/installation.md @@ -8,8 +8,8 @@ In order to use the MCCoroutine Kotlin API, you need to include the following li ```groovy dependencies { - implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:2.12.1") - implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:2.12.1") + implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:2.13.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:2.13.0") } ``` @@ -17,26 +17,26 @@ In order to use the MCCoroutine Kotlin API, you need to include the following li ```groovy dependencies { - implementation("com.github.shynixn.mccoroutine:mccoroutine-bungeecord-api:2.12.1") - implementation("com.github.shynixn.mccoroutine:mccoroutine-bungeecord-core:2.12.1") + implementation("com.github.shynixn.mccoroutine:mccoroutine-bungeecord-api:2.13.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-bungeecord-core:2.13.0") } ``` -=== "Sponge" +=== "Fabric" ```groovy dependencies { - implementation("com.github.shynixn.mccoroutine:mccoroutine-sponge-api:2.12.1") - implementation("com.github.shynixn.mccoroutine:mccoroutine-sponge-core:2.12.1") + implementation("com.github.shynixn.mccoroutine:mccoroutine-fabric-api:2.13.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-fabric-core:2.13.0") } ``` -=== "Velocity" +=== "Folia" ```groovy dependencies { - implementation("com.github.shynixn.mccoroutine:mccoroutine-velocity-api:2.12.1") - implementation("com.github.shynixn.mccoroutine:mccoroutine-velocity-core:2.12.1") + implementation("com.github.shynixn.mccoroutine:mccoroutine-folia-api:2.13.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-folia-core:2.13.0") } ``` @@ -44,17 +44,26 @@ In order to use the MCCoroutine Kotlin API, you need to include the following li ```groovy dependencies { - implementation("com.github.shynixn.mccoroutine:mccoroutine-minestom-api:2.12.1") - implementation("com.github.shynixn.mccoroutine:mccoroutine-minestom-core:2.12.1") + implementation("com.github.shynixn.mccoroutine:mccoroutine-minestom-api:2.13.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-minestom-core:2.13.0") } ``` -=== "Fabric" +=== "Sponge" ```groovy dependencies { - implementation("com.github.shynixn.mccoroutine:mccoroutine-fabric-api:2.12.1") - implementation("com.github.shynixn.mccoroutine:mccoroutine-fabric-core:2.12.1") + implementation("com.github.shynixn.mccoroutine:mccoroutine-sponge-api:2.13.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-sponge-core:2.13.0") + } + ``` + +=== "Velocity" + + ```groovy + dependencies { + implementation("com.github.shynixn.mccoroutine:mccoroutine-velocity-api:2.13.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-velocity-core:2.13.0") } ``` @@ -78,10 +87,20 @@ dependencies { **plugin.yml** ```yaml libraries: - - com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:2.12.1 - - com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:2.12.1 + - com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:2.13.0 + - com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:2.13.0 ``` +=== "Folia" + + **plugin.yml** + ```yaml + libraries: + - com.github.shynixn.mccoroutine:mccoroutine-folia-api:2.13.0 + - com.github.shynixn.mccoroutine:mccoroutine-folia-core:2.13.0 + ``` + + === "Other Server" Shade the libraries into your plugin.jar file using gradle or maven. diff --git a/docs/wiki/docs/listener.md b/docs/wiki/docs/listener.md index 2cebdbed..040cc924 100644 --- a/docs/wiki/docs/listener.md +++ b/docs/wiki/docs/listener.md @@ -72,50 +72,74 @@ suspendable functions). You can mix suspendable and non suspendable functions in } ```` -=== "Sponge" +=== "Fabric" ````kotlin - import org.spongepowered.api.event.Listener - import org.spongepowered.api.event.network.ClientConnectionEvent + import net.minecraft.entity.Entity + import net.minecraft.entity.player.PlayerEntity + import net.minecraft.util.Hand + import net.minecraft.util.hit.EntityHitResult + import net.minecraft.world.World import java.util.* class PlayerDataListener(private val database: Database) { - @Listener - suspend fun onPlayerJoinEvent(event: ClientConnectionEvent.Join) { - val player = event.targetEntity + suspend fun onPlayerAttackEvent( + player: PlayerEntity, + world: World, + hand: Hand, + entity: Entity, + hitResult: EntityHitResult? + ) { + val playerData = database.getDataFromPlayer(player) + playerData.name = player.name.toString() + playerData.lastJoinDate = Date() + database.saveData(player, playerData) + } + } + ```` + +=== "Folia" + + ````kotlin + import org.bukkit.event.EventHandler + import org.bukkit.event.Listener + import org.bukkit.event.player.PlayerJoinEvent + import org.bukkit.event.player.PlayerQuitEvent + import java.util.* + + class PlayerDataListener(private val database: Database) : Listener { + @EventHandler + suspend fun onPlayerJoinEvent(event: PlayerJoinEvent) { + val player = event.player val playerData = database.getDataFromPlayer(player) playerData.name = player.name playerData.lastJoinDate = Date() database.saveData(player, playerData) } - @Listener - suspend fun onPlayerQuitEvent(event: ClientConnectionEvent.Disconnect) { - val player = event.targetEntity - val playerData = database.getDataFromPlayer(player) - playerData.name = player.name - playerData.lastQuitDate = Date() - database.saveData(player, playerData) + @EventHandler + fun onPlayerQuitEvent(event: PlayerQuitEvent) { + // Alternative way to achieve the same thing + plugin.launch(plugin.entityDispatcher(event.player)), CoroutineStart.UNDISPATCHED) { + val player = event.player + val playerData = database.getDataFromPlayer(player) + playerData.name = player.name + playerData.lastQuitDate = Date() + database.saveData(player, playerData) + } } } ```` -=== "Velocity" - - In Velocity events can be [handled asynchronously](https://velocitypowered.com/wiki/developers/event-api/). This allows full - control over consuming, processing and resuming events when performing long running operations. When you create a suspend - function using MCCoroutine, they automatically handle ``Continuation`` and ``EventTask``. You do not have to do anything yourself, - all suspend functions are automatically processed asynchronously. +=== "Minestom" ````kotlin - import com.velocitypowered.api.event.Subscribe - import com.velocitypowered.api.event.connection.DisconnectEvent - import com.velocitypowered.api.event.connection.PostLoginEvent + import net.minestom.server.event.player.PlayerDisconnectEvent + import net.minestom.server.event.player.PlayerLoginEvent import java.util.* class PlayerDataListener(private val database: Database) { - @Subscribe - suspend fun onPlayerJoinEvent(event: PostLoginEvent) { + suspend fun onPlayerJoinEvent(event: PlayerLoginEvent) { val player = event.player val playerData = database.getDataFromPlayer(player) playerData.name = player.username @@ -123,8 +147,7 @@ suspendable functions). You can mix suspendable and non suspendable functions in database.saveData(player, playerData) } - @Subscribe - suspend fun onPlayerQuitEvent(event: DisconnectEvent) { + suspend fun onPlayerQuitEvent(event: PlayerDisconnectEvent) { val player = event.player val playerData = database.getDataFromPlayer(player) playerData.name = player.username @@ -134,54 +157,64 @@ suspendable functions). You can mix suspendable and non suspendable functions in } ```` -=== "Minestom" +=== "Sponge" ````kotlin - import net.minestom.server.event.player.PlayerDisconnectEvent - import net.minestom.server.event.player.PlayerLoginEvent + import org.spongepowered.api.event.Listener + import org.spongepowered.api.event.network.ClientConnectionEvent import java.util.* class PlayerDataListener(private val database: Database) { - suspend fun onPlayerJoinEvent(event: PlayerLoginEvent) { - val player = event.player + @Listener + suspend fun onPlayerJoinEvent(event: ClientConnectionEvent.Join) { + val player = event.targetEntity val playerData = database.getDataFromPlayer(player) - playerData.name = player.username + playerData.name = player.name playerData.lastJoinDate = Date() database.saveData(player, playerData) } - suspend fun onPlayerQuitEvent(event: PlayerDisconnectEvent) { - val player = event.player + @Listener + suspend fun onPlayerQuitEvent(event: ClientConnectionEvent.Disconnect) { + val player = event.targetEntity val playerData = database.getDataFromPlayer(player) - playerData.name = player.username + playerData.name = player.name playerData.lastQuitDate = Date() database.saveData(player, playerData) } } ```` -=== "Fabric" +=== "Velocity" + + In Velocity events can be [handled asynchronously](https://velocitypowered.com/wiki/developers/event-api/). This allows full + control over consuming, processing and resuming events when performing long running operations. When you create a suspend + function using MCCoroutine, they automatically handle ``Continuation`` and ``EventTask``. You do not have to do anything yourself, + all suspend functions are automatically processed asynchronously. ````kotlin - import net.minecraft.entity.Entity - import net.minecraft.entity.player.PlayerEntity - import net.minecraft.util.Hand - import net.minecraft.util.hit.EntityHitResult - import net.minecraft.world.World + import com.velocitypowered.api.event.Subscribe + import com.velocitypowered.api.event.connection.DisconnectEvent + import com.velocitypowered.api.event.connection.PostLoginEvent import java.util.* class PlayerDataListener(private val database: Database) { - suspend fun onPlayerAttackEvent( - player: PlayerEntity, - world: World, - hand: Hand, - entity: Entity, - hitResult: EntityHitResult? - ) { - val playerData = database.getDataFromPlayer(player) - playerData.name = player.name.toString() - playerData.lastJoinDate = Date() - database.saveData(player, playerData) + @Subscribe + suspend fun onPlayerJoinEvent(event: PostLoginEvent) { + val player = event.player + val playerData = database.getDataFromPlayer(player) + playerData.name = player.username + playerData.lastJoinDate = Date() + database.saveData(player, playerData) + } + + @Subscribe + suspend fun onPlayerQuitEvent(event: DisconnectEvent) { + val player = event.player + val playerData = database.getDataFromPlayer(player) + playerData.name = player.username + playerData.lastQuitDate = Date() + database.saveData(player, playerData) } } ```` @@ -236,6 +269,114 @@ suspendable functions). You can mix suspendable and non suspendable functions in } ```` +=== "Fabric" + + ````kotlin + class MCCoroutineSampleServerMod : DedicatedServerModInitializer { + override fun onInitializeServer() { + ServerLifecycleEvents.SERVER_STARTING.register(ServerLifecycleEvents.ServerStarting { server -> + // Connect Native Minecraft Scheduler and MCCoroutine. + mcCoroutineConfiguration.minecraftExecutor = Executor { r -> + server.submitAndJoin(r) + } + launch { + onServerStarting(server) + } + }) + + ServerLifecycleEvents.SERVER_STOPPING.register { server -> + mcCoroutineConfiguration.disposePluginSession() + } + } + + /** + * MCCoroutine is ready after the server has started. + */ + private suspend fun onServerStarting(server : MinecraftServer) { + // Minecraft Main Thread + val database = Database() + database.createDbIfNotExist() + + val listener = PlayerDataListener(database) + val mod = this + AttackEntityCallback.EVENT.register(AttackEntityCallback { player, world, hand, entity, hitResult -> + mod.launch { + listener.onPlayerAttackEvent(player, world, hand, entity, hitResult) + } + ActionResult.PASS + }) + } + } + ```` + +=== "Folia" + + Instead of using ``registerEvents``, use the provided extension method ``registerSuspendingEvents`` to allow + suspendable functions in your listener. Please notice, that timing measurements are no longer accurate for suspendable functions. + + ````kotlin + import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin + import com.github.shynixn.mccoroutine.folia.registerSuspendingEvents + + class MCCoroutineSamplePlugin : SuspendingJavaPlugin() { + private val database = Database() + + override suspend fun onEnableAsync() { + // Minecraft Main Thread + database.createDbIfNotExist() + val plugin = this + // MCCoroutine for Folia cannot assume the correct dispatcher per event. You need to define how each event + // should find its correct dispatcher in MCCoroutine. + val eventDispatcher = mapOf, (event: Event) -> CoroutineContext>( + Pair(PlayerJoinEvent::class.java) { + require(it is PlayerJoinEvent) + plugin.entityDispatcher(it.player) // For a player event, the dispatcher is always player related. + }, + Pair(PlayerQuitEvent::class.java) { + require(it is PlayerQuitEvent) + plugin.entityDispatcher(it.player) + } + ) + server.pluginManager.registerSuspendingEvents(PlayerDataListener(database), this, eventDispatcher) + } + + override suspend fun onDisableAsync() { + // Minecraft Main Thread + } + } + ```` + +=== "Minestom" + + Instead of using ``addListener``, use the provided extension method ``addSuspendingListener`` to allow + suspendable functions in your listener. Please notice, that timing measurements are no longer accurate for suspendable functions. + + ```kotlin + import com.github.shynixn.mccoroutine.minestom.addSuspendingListener + import com.github.shynixn.mccoroutine.minestom.launch + import com.github.shynixn.mccoroutine.minestom.sample.extension.impl.Database + import com.github.shynixn.mccoroutine.minestom.sample.extension.impl.PlayerDataListener + import net.minestom.server.MinecraftServer + import net.minestom.server.event.player.PlayerLoginEvent + + fun main(args: Array) { + val minecraftServer = MinecraftServer.init() + minecraftServer.launch { + val database = Database() + // Minecraft Main Thread + database.createDbIfNotExist() + + val listener = PlayerDataListener(database) + MinecraftServer.getGlobalEventHandler() + .addSuspendingListener(minecraftServer, PlayerLoginEvent::class.java) { e -> + listener.onPlayerJoinEvent(e) + } + } + + minecraftServer.start("0.0.0.0", 25565) + } + ``` + === "Sponge" Instead of using ``registerListeners``, use the provided extension method ``registerSuspendingListeners`` to allow @@ -316,77 +457,6 @@ suspendable functions). You can mix suspendable and non suspendable functions in } ```` -=== "Minestom" - - Instead of using ``addListener``, use the provided extension method ``addSuspendingListener`` to allow - suspendable functions in your listener. Please notice, that timing measurements are no longer accurate for suspendable functions. - - ```kotlin - import com.github.shynixn.mccoroutine.minestom.addSuspendingListener - import com.github.shynixn.mccoroutine.minestom.launch - import com.github.shynixn.mccoroutine.minestom.sample.extension.impl.Database - import com.github.shynixn.mccoroutine.minestom.sample.extension.impl.PlayerDataListener - import net.minestom.server.MinecraftServer - import net.minestom.server.event.player.PlayerLoginEvent - - fun main(args: Array) { - val minecraftServer = MinecraftServer.init() - minecraftServer.launch { - val database = Database() - // Minecraft Main Thread - database.createDbIfNotExist() - - val listener = PlayerDataListener(database) - MinecraftServer.getGlobalEventHandler() - .addSuspendingListener(minecraftServer, PlayerLoginEvent::class.java) { e -> - listener.onPlayerJoinEvent(e) - } - } - - minecraftServer.start("0.0.0.0", 25565) - } - ``` - -=== "Fabric" - - ````kotlin - class MCCoroutineSampleServerMod : DedicatedServerModInitializer { - override fun onInitializeServer() { - ServerLifecycleEvents.SERVER_STARTING.register(ServerLifecycleEvents.ServerStarting { server -> - // Connect Native Minecraft Scheduler and MCCoroutine. - mcCoroutineConfiguration.minecraftExecutor = Executor { r -> - server.submitAndJoin(r) - } - launch { - onServerStarting(server) - } - }) - - ServerLifecycleEvents.SERVER_STOPPING.register { server -> - mcCoroutineConfiguration.disposePluginSession() - } - } - - /** - * MCCoroutine is ready after the server has started. - */ - private suspend fun onServerStarting(server : MinecraftServer) { - // Minecraft Main Thread - val database = Database() - database.createDbIfNotExist() - - val listener = PlayerDataListener(database) - val mod = this - AttackEntityCallback.EVENT.register(AttackEntityCallback { player, world, hand, entity, hitResult -> - mod.launch { - listener.onPlayerAttackEvent(player, world, hand, entity, hitResult) - } - ActionResult.PASS - }) - } - } - ```` - ### Test the Listener Join and leave your server to observe ``getDataFromPlayer`` and ``saveData`` messages getting printed to your server log. diff --git a/docs/wiki/docs/plugin.md b/docs/wiki/docs/plugin.md index 9504f54b..6f106725 100644 --- a/docs/wiki/docs/plugin.md +++ b/docs/wiki/docs/plugin.md @@ -76,6 +76,94 @@ disposed automatically when you reload your plugin. Other plugins which are already enabled, may or may not already perform work in the background. Plugins, which may get enabled in the future, wait until this plugin is enabled. +=== "Fabric" + + MCCoroutine for Fabric does not have an dependency on Minecraft itself, therefore it is version independent from Minecraft. It only depends + on the Fabric Api. This however means, we need to manually setup and dispose MCCoroutine. Register the ``SERVER_STARTING`` event and + connect the native Minecraft Scheduler with MCCoroutine using an ``Executor``. Dispose MCCoroutine in ``SERVER_STOPPING``. + + ````kotlin + class MCCoroutineSampleServerMod : DedicatedServerModInitializer { + override fun onInitializeServer() { + ServerLifecycleEvents.SERVER_STARTING.register(ServerLifecycleEvents.ServerStarting { server -> + // Connect Native Minecraft Scheduler and MCCoroutine. + mcCoroutineConfiguration.minecraftExecutor = Executor { r -> + server.submitAndJoin(r) + } + launch { + onServerStarting(server) + } + }) + + ServerLifecycleEvents.SERVER_STOPPING.register { server -> + mcCoroutineConfiguration.disposePluginSession() + } + } + /** + * MCCoroutine is ready after the server has started. + */ + private suspend fun onServerStarting(server : MinecraftServer) { + // Minecraft Main Thread + // Your startup code with suspend support + + this.launch { + // Launch new corroutines + } + } + } + ```` + +=== "Folia" + + The first decision for Bukkit API based plugins is to decide between ``JavaPlugin`` or ``SuspendingJavaPlugin``, which is a new base + class extending ``JavaPlugin``. + + If you want to perform async operations or call other suspending functions from your plugin class, go with the newly + available type ``SuspendingJavaPlugin`` otherwise use ``JavaPlugin``. + + ````kotlin + import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin + + class MCCoroutineSamplePlugin : SuspendingJavaPlugin() { + override suspend fun onEnableAsync() { + // Global Region Thread + } + + override suspend fun onDisableAsync() { + // Global Region Thread + } + } + ```` + + !!! note "How onEnableAsync works" + The implementation which calls the ``onEnableAsync`` function manipulates the Bukkit Server implementation in the + following way: + If a context switch is made, it blocks the entire global region thread until the context is given back. This means, + in this method, you can switch contexts as you like but the plugin is not considered enabled until the context is given + back. + It allows for a clean startup as the plugin is not considered "enabled" until the context is given back. + Other plugins which are already enabled, may or may not already perform work in the background. + Plugins, which may get enabled in the future, wait until this plugin is enabled. + + +=== "Minestom" + + MCCoroutine can be used on server or on extension level. The example below shows using MCCoroutine on server level. + If you are developing an extension, you can use the instance of your ``Extension`` instead of the ``MinecraftServer`` + + ```kotlin + import com.github.shynixn.mccoroutine.minestom.launch + import net.minestom.server.MinecraftServer + + fun main(args: Array) { + val minecraftServer = MinecraftServer.init() + minecraftServer.launch { + // Suspendable operations + } + minecraftServer.start("0.0.0.0", 25565) + } + ``` + === "Sponge" The first decision for Sponge API based plugins is to decide, if you want to call other suspending functions from your plugin class. @@ -129,61 +217,6 @@ disposed automatically when you reload your plugin. } ```` -=== "Minestom" - - MCCoroutine can be used on server or on extension level. The example below shows using MCCoroutine on server level. - If you are developing an extension, you can use the instance of your ``Extension`` instead of the ``MinecraftServer`` - - ```kotlin - import com.github.shynixn.mccoroutine.minestom.launch - import net.minestom.server.MinecraftServer - - fun main(args: Array) { - val minecraftServer = MinecraftServer.init() - minecraftServer.launch { - // Suspendable operations - } - minecraftServer.start("0.0.0.0", 25565) - } - ``` - -=== "Fabric" - - MCCoroutine for Fabric does not have an dependency on Minecraft itself, therefore it is version independent from Minecraft. It only depends - on the Fabric Api. This however means, we need to manually setup and dispose MCCoroutine. Register the ``SERVER_STARTING`` event and - connect the native Minecraft Scheduler with MCCoroutine using an ``Executor``. Dispose MCCoroutine in ``SERVER_STOPPING``. - - ````kotlin - class MCCoroutineSampleServerMod : DedicatedServerModInitializer { - override fun onInitializeServer() { - ServerLifecycleEvents.SERVER_STARTING.register(ServerLifecycleEvents.ServerStarting { server -> - // Connect Native Minecraft Scheduler and MCCoroutine. - mcCoroutineConfiguration.minecraftExecutor = Executor { r -> - server.submitAndJoin(r) - } - launch { - onServerStarting(server) - } - }) - - ServerLifecycleEvents.SERVER_STOPPING.register { server -> - mcCoroutineConfiguration.disposePluginSession() - } - } - /** - * MCCoroutine is ready after the server has started. - */ - private suspend fun onServerStarting(server : MinecraftServer) { - // Minecraft Main Thread - // Your startup code with suspend support - - this.launch { - // Launch new corroutines - } - } - } - ```` - ## Calling a Database from Plugin Main class Create a class containing properties of data, which we want to store into a database. @@ -286,12 +319,12 @@ Here, it is important that we perform all IO calls on async threads and returns } ```` -=== "Sponge" +=== "Fabric" - ````kotlin + ```kotlin import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext - import org.spongepowered.api.entity.living.player.Player + import net.minecraft.entity.player.PlayerEntity import java.util.* class Database() { @@ -304,19 +337,19 @@ Here, it is important that we perform all IO calls on async threads and returns println("[createDbIfNotExist] End on minecraft thread " + Thread.currentThread().id) } - suspend fun getDataFromPlayer(player : Player) : PlayerData { + suspend fun getDataFromPlayer(player: PlayerEntity) : PlayerData { println("[getDataFromPlayer] Start on minecraft thread " + Thread.currentThread().id) val playerData = withContext(Dispatchers.IO) { println("[getDataFromPlayer] Retrieving player data on database io thread " + Thread.currentThread().id) // ... get from database by player uuid or create new playerData instance. - PlayerData(player.uniqueId, player.name, Date(), Date()) + PlayerData(player.uuid, player.name.toString(), Date(), Date()) } println("[getDataFromPlayer] End on minecraft thread " + Thread.currentThread().id) return playerData; } - suspend fun saveData(player : Player, playerData : PlayerData) { + suspend fun saveData(player: PlayerEntity, playerData : PlayerData) { println("[saveData] Start on minecraft thread " + Thread.currentThread().id) withContext(Dispatchers.IO){ @@ -327,51 +360,47 @@ Here, it is important that we perform all IO calls on async threads and returns println("[saveData] End on minecraft thread " + Thread.currentThread().id) } } - ```` - -=== "Velocity" + ``` - !!! note "Important" - Velocity does not have a main thread or minecraft thread. Instead it operates on different types of [thread pools](https://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html). - This means, the thread id is not always the same if we suspend an operation. Therefore, it is recommend to print the name of the thread instead of the id to see which threadpool you are currently on. +=== "Folia" ````kotlin - import com.velocitypowered.api.proxy.Player import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext + import org.bukkit.entity.Player import java.util.* class Database() { suspend fun createDbIfNotExist() { - println("[createDbIfNotExist] Start on any thread " + Thread.currentThread().name) - withContext(Dispatchers.IO) { - println("[createDbIfNotExist] Creating database on database io thread " + Thread.currentThread().name) + println("[createDbIfNotExist] Start on the caller thread " + Thread.currentThread().id) + withContext(Dispatchers.IO){ + println("[createDbIfNotExist] Creating database on database io thread " + Thread.currentThread().id) // ... create tables } - println("[createDbIfNotExist] End on velocity plugin threadpool " + Thread.currentThread().name) + println("[createDbIfNotExist] End on the caller thread " + Thread.currentThread().id) } - suspend fun getDataFromPlayer(player: Player): PlayerData { - println("[getDataFromPlayer] Start on any thread " + Thread.currentThread().name) + suspend fun getDataFromPlayer(player : Player) : PlayerData { + println("[getDataFromPlayer] Start on the caller thread " + Thread.currentThread().id) val playerData = withContext(Dispatchers.IO) { - println("[getDataFromPlayer] Retrieving player data on database io thread " + Thread.currentThread().name) + println("[getDataFromPlayer] Retrieving player data on database io thread " + Thread.currentThread().id) // ... get from database by player uuid or create new playerData instance. - PlayerData(player.uniqueId, player.username, Date(), Date()) + PlayerData(player.uniqueId, player.name, Date(), Date()) } - println("[getDataFromPlayer] End on velocity plugin threadpool " + Thread.currentThread().name) + println("[getDataFromPlayer] End on the caller thread " + Thread.currentThread().id) return playerData; } + + suspend fun saveData(player : Player, playerData : PlayerData) { + println("[saveData] Start on the caller thread " + Thread.currentThread().id) - suspend fun saveData(player: Player, playerData: PlayerData) { - println("[saveData] Start on any thread " + Thread.currentThread().name) - - withContext(Dispatchers.IO) { - println("[saveData] Saving player data on database io thread " + Thread.currentThread().name) + withContext(Dispatchers.IO){ + println("[saveData] Saving player data on database io thread " + Thread.currentThread().id) // insert or update playerData } - println("[saveData] End on velocity plugin threadpool " + Thread.currentThread().name) + println("[saveData] End on the caller thread " + Thread.currentThread().id) } } ```` @@ -419,12 +448,12 @@ Here, it is important that we perform all IO calls on async threads and returns } ``` -=== "Fabric" +=== "Sponge" - ```kotlin + ````kotlin import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext - import net.minecraft.entity.player.PlayerEntity + import org.spongepowered.api.entity.living.player.Player import java.util.* class Database() { @@ -437,19 +466,19 @@ Here, it is important that we perform all IO calls on async threads and returns println("[createDbIfNotExist] End on minecraft thread " + Thread.currentThread().id) } - suspend fun getDataFromPlayer(player: PlayerEntity) : PlayerData { + suspend fun getDataFromPlayer(player : Player) : PlayerData { println("[getDataFromPlayer] Start on minecraft thread " + Thread.currentThread().id) val playerData = withContext(Dispatchers.IO) { println("[getDataFromPlayer] Retrieving player data on database io thread " + Thread.currentThread().id) // ... get from database by player uuid or create new playerData instance. - PlayerData(player.uuid, player.name.toString(), Date(), Date()) + PlayerData(player.uniqueId, player.name, Date(), Date()) } println("[getDataFromPlayer] End on minecraft thread " + Thread.currentThread().id) return playerData; } - suspend fun saveData(player: PlayerEntity, playerData : PlayerData) { + suspend fun saveData(player : Player, playerData : PlayerData) { println("[saveData] Start on minecraft thread " + Thread.currentThread().id) withContext(Dispatchers.IO){ @@ -460,7 +489,55 @@ Here, it is important that we perform all IO calls on async threads and returns println("[saveData] End on minecraft thread " + Thread.currentThread().id) } } - ``` + ```` + +=== "Velocity" + + !!! note "Important" + Velocity does not have a main thread or minecraft thread. Instead it operates on different types of [thread pools](https://docs.oracle.com/javase/tutorial/essential/concurrency/pools.html). + This means, the thread id is not always the same if we suspend an operation. Therefore, it is recommend to print the name of the thread instead of the id to see which threadpool you are currently on. + + ````kotlin + import com.velocitypowered.api.proxy.Player + import kotlinx.coroutines.Dispatchers + import kotlinx.coroutines.withContext + import java.util.* + + class Database() { + suspend fun createDbIfNotExist() { + println("[createDbIfNotExist] Start on any thread " + Thread.currentThread().name) + withContext(Dispatchers.IO) { + println("[createDbIfNotExist] Creating database on database io thread " + Thread.currentThread().name) + // ... create tables + } + println("[createDbIfNotExist] End on velocity plugin threadpool " + Thread.currentThread().name) + } + + suspend fun getDataFromPlayer(player: Player): PlayerData { + println("[getDataFromPlayer] Start on any thread " + Thread.currentThread().name) + val playerData = withContext(Dispatchers.IO) { + println("[getDataFromPlayer] Retrieving player data on database io thread " + Thread.currentThread().name) + // ... get from database by player uuid or create new playerData instance. + PlayerData(player.uniqueId, player.username, Date(), Date()) + } + + println("[getDataFromPlayer] End on velocity plugin threadpool " + Thread.currentThread().name) + return playerData; + } + + suspend fun saveData(player: Player, playerData: PlayerData) { + println("[saveData] Start on any thread " + Thread.currentThread().name) + + withContext(Dispatchers.IO) { + println("[saveData] Saving player data on database io thread " + Thread.currentThread().name) + // insert or update playerData + } + + println("[saveData] End on velocity plugin threadpool " + Thread.currentThread().name) + } + } + ```` + Create a new instance of the database and call it in your main class. @@ -501,6 +578,72 @@ Create a new instance of the database and call it in your main class. } ```` +=== "Fabric" + + ````kotlin + class MCCoroutineSampleServerMod : DedicatedServerModInitializer { + override fun onInitializeServer() { + ServerLifecycleEvents.SERVER_STARTING.register(ServerLifecycleEvents.ServerStarting { server -> + // Connect Native Minecraft Scheduler and MCCoroutine. + mcCoroutineConfiguration.minecraftExecutor = Executor { r -> + server.submitAndJoin(r) + } + launch { + onServerStarting(server) + } + }) + + ServerLifecycleEvents.SERVER_STOPPING.register { server -> + mcCoroutineConfiguration.disposePluginSession() + } + } + /** + * MCCoroutine is ready after the server has started. + */ + private suspend fun onServerStarting(server : MinecraftServer) { + // Minecraft Main Thread + val database = Database() + database.createDbIfNotExist() + } + } + ```` + +=== "Folia" + + ````kotlin + import com.github.shynixn.mccoroutine.folia.SuspendingJavaPlugin + + class MCCoroutineSamplePlugin : SuspendingJavaPlugin() { + private val database = Database() + + override suspend fun onEnableAsync() { + // Global Region Thread + database.createDbIfNotExist() + } + + override suspend fun onDisableAsync() { + } + } + ```` + +=== "Minestom" + + ```kotlin + import com.github.shynixn.mccoroutine.minestom.launch + import net.minestom.server.MinecraftServer + + fun main(args: Array) { + val minecraftServer = MinecraftServer.init() + minecraftServer.launch { + // Minecraft Main Thread + val database = Database() + database.createDbIfNotExist() + } + minecraftServer.start("0.0.0.0", 25565) + } + ``` + + === "Sponge" ````kotlin @@ -556,53 +699,6 @@ Create a new instance of the database and call it in your main class. } ```` -=== "Minestom" - - ```kotlin - import com.github.shynixn.mccoroutine.minestom.launch - import net.minestom.server.MinecraftServer - - fun main(args: Array) { - val minecraftServer = MinecraftServer.init() - minecraftServer.launch { - // Minecraft Main Thread - val database = Database() - database.createDbIfNotExist() - } - minecraftServer.start("0.0.0.0", 25565) - } - ``` - -=== "Fabric" - - ````kotlin - class MCCoroutineSampleServerMod : DedicatedServerModInitializer { - override fun onInitializeServer() { - ServerLifecycleEvents.SERVER_STARTING.register(ServerLifecycleEvents.ServerStarting { server -> - // Connect Native Minecraft Scheduler and MCCoroutine. - mcCoroutineConfiguration.minecraftExecutor = Executor { r -> - server.submitAndJoin(r) - } - launch { - onServerStarting(server) - } - }) - - ServerLifecycleEvents.SERVER_STOPPING.register { server -> - mcCoroutineConfiguration.disposePluginSession() - } - } - /** - * MCCoroutine is ready after the server has started. - */ - private suspend fun onServerStarting(server : MinecraftServer) { - // Minecraft Main Thread - val database = Database() - database.createDbIfNotExist() - } - } - ```` - ## Test the Plugin Start your server to observe the ``createDbIfNotExist`` messages getting printed to your server log. diff --git a/docs/wiki/docs/unittests.md b/docs/wiki/docs/unittests.md index a1fae71c..0bcc173e 100644 --- a/docs/wiki/docs/unittests.md +++ b/docs/wiki/docs/unittests.md @@ -18,7 +18,7 @@ feedback to the real environment. ```kotlin dependencies { - testImplementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-test:2.12.1") + testImplementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-test:2.13.0") } ``` diff --git a/mccoroutine-bukkit-core/src/main/java/com/github/shynixn/mccoroutine/bukkit/dispatcher/AsyncCoroutineDispatcher.kt b/mccoroutine-bukkit-core/src/main/java/com/github/shynixn/mccoroutine/bukkit/dispatcher/AsyncCoroutineDispatcher.kt index c923954f..329de10e 100644 --- a/mccoroutine-bukkit-core/src/main/java/com/github/shynixn/mccoroutine/bukkit/dispatcher/AsyncCoroutineDispatcher.kt +++ b/mccoroutine-bukkit-core/src/main/java/com/github/shynixn/mccoroutine/bukkit/dispatcher/AsyncCoroutineDispatcher.kt @@ -6,7 +6,7 @@ import org.bukkit.plugin.Plugin import kotlin.coroutines.CoroutineContext /** - * CraftBukkit Async ThreadPool Dispatcher. Dispatches only if the call is at the primary thread. + * CraftBukkit Async ThreadPool Dispatcher. Dispatches always. */ internal open class AsyncCoroutineDispatcher( private val plugin: Plugin, diff --git a/mccoroutine-bukkit-sample/src/main/resources/plugin.yml b/mccoroutine-bukkit-sample/src/main/resources/plugin.yml index 80730694..4a401950 100644 --- a/mccoroutine-bukkit-sample/src/main/resources/plugin.yml +++ b/mccoroutine-bukkit-sample/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: MCCoroutine-Sample -version: 2.12.1 +version: 2.13.0 author: Shynixn main: com.github.shynixn.mccoroutine.bukkit.sample.MCCoroutineSamplePlugin commands: diff --git a/mccoroutine-bungeecord-sample/src/main/resources/plugin.yml b/mccoroutine-bungeecord-sample/src/main/resources/plugin.yml index 453a3e9e..3f8c6665 100644 --- a/mccoroutine-bungeecord-sample/src/main/resources/plugin.yml +++ b/mccoroutine-bungeecord-sample/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: MCCoroutine-Sample -version: 2.12.1 +version: 2.13.0 author: Shynixn main: com.github.shynixn.mccoroutine.bungeecord.sample.MCCoroutineSamplePlugin commands: diff --git a/mccoroutine-fabric-sample/build.gradle.kts b/mccoroutine-fabric-sample/build.gradle.kts index 2cac0140..c4e73083 100644 --- a/mccoroutine-fabric-sample/build.gradle.kts +++ b/mccoroutine-fabric-sample/build.gradle.kts @@ -9,8 +9,8 @@ repositories { mavenLocal() } dependencies { - implementation("com.github.shynixn.mccoroutine:mccoroutine-fabric-api:2.12.1") - implementation("com.github.shynixn.mccoroutine:mccoroutine-fabric-core:2.12.1") + implementation("com.github.shynixn.mccoroutine:mccoroutine-fabric-api:2.13.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-fabric-core:2.13.0") minecraft("com.mojang", "minecraft", project.extra["minecraft_version"] as String) mappings("net.fabricmc", "yarn", project.extra["yarn_mappings"] as String, null, "v2") diff --git a/mccoroutine-folia-api/build.gradle.kts b/mccoroutine-folia-api/build.gradle.kts new file mode 100644 index 00000000..55e3bb4a --- /dev/null +++ b/mccoroutine-folia-api/build.gradle.kts @@ -0,0 +1,16 @@ +repositories { + maven { + url = uri("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") + } +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +dependencies { + compileOnly("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT") + compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") +} diff --git a/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/CoroutineSession.kt b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/CoroutineSession.kt new file mode 100644 index 00000000..c94c2705 --- /dev/null +++ b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/CoroutineSession.kt @@ -0,0 +1,91 @@ +package com.github.shynixn.mccoroutine.folia + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import org.bukkit.World +import org.bukkit.command.PluginCommand +import org.bukkit.entity.Entity +import org.bukkit.event.Event +import org.bukkit.event.Listener +import kotlin.coroutines.CoroutineContext + +/** + * Facade of a coroutine session of a single plugin. + */ +interface CoroutineSession { + /** + * Plugin scope. + */ + val scope: CoroutineScope + + /** + * The global region dispatcher is simply used to perform edits on data that the global region owns, such as game rules, day time, weather, or to execute commands using the console command sender. + */ + val dispatcherGlobalRegion: CoroutineContext + + /** + * All async operations dispatcher. + */ + val dispatcherAsync: CoroutineContext + + /** + * Manipulates the bukkit server heart beat on startup. + */ + var isManipulatedServerHeartBeatEnabled: Boolean + + /** + * MCCoroutine Facade. + */ + val mcCoroutineConfiguration: MCCoroutineConfiguration + + /** + * Gets if the Folia schedulers where successfully loaded into MCCoroutine. + * Returns false if MCCoroutine falls back to the BukkitScheduler. + */ + val isFoliaLoaded: Boolean + + /** + * The RegionizedTaskQueue allows tasks to be scheduled to be executed on the next tick of a region that owns a specific location, or creating such region if it does not exist. + */ + fun getRegionDispatcher(world: World, chunkX: Int, chunkZ: Int): CoroutineContext + + /** + The EntityScheduler allows tasks to be scheduled to be executed on the region that owns the entity. + */ + fun getEntityDispatcher(entity: Entity): CoroutineContext + + /** + * Registers a suspend command executor. + */ + fun registerSuspendCommandExecutor( + context: CoroutineContext, + pluginCommand: PluginCommand, + commandExecutor: SuspendingCommandExecutor + ) + + /** + * Registers a suspend tab completer. + */ + fun registerSuspendTabCompleter( + context: CoroutineContext, + pluginCommand: PluginCommand, + tabCompleter: SuspendingTabCompleter + ) + + /** + * Registers a suspend listener. + */ + fun registerSuspendListener( + listener: Listener, + eventDispatcher: Map, (event: Event) -> CoroutineContext> + ) + + /** + * Fires a suspending [event] with the given [eventExecutionType]. + * @return Collection of receiver jobs. May already be completed. + */ + fun fireSuspendingEvent( + event: Event, + eventExecutionType: EventExecutionType + ): Collection +} diff --git a/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/CoroutineTimings.kt b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/CoroutineTimings.kt new file mode 100644 index 00000000..a91c7a8b --- /dev/null +++ b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/CoroutineTimings.kt @@ -0,0 +1,40 @@ +package com.github.shynixn.mccoroutine.folia + +import java.util.* +import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.coroutines.AbstractCoroutineContextElement +import kotlin.coroutines.CoroutineContext + +/** + * The spigot timings require a reference to the runnable to display the name of timings correctly. + * Now, Kotlin Coroutines does not allow to directly pass a runnable object, because a single coroutine + * may consist out of multiple runnables. This class is a workaround coroutine context element, which can be passed + * along the [minecraftDispatcher] to display a valid name for the coroutine. + */ +abstract class CoroutineTimings : AbstractCoroutineContextElement(CoroutineTimings), Runnable { + /** + * Key identifier of the context element. + */ + companion object Key : CoroutineContext.Key + + /** + * Multiple tasks can be assigned to a single coroutine. We implement this by a queue. + */ + var queue: Queue = ConcurrentLinkedQueue() + + /** + * When an object implementing interface `Runnable` is used + * to create a thread, starting the thread causes the object's + * `run` method to be called in that separately executing + * thread. + * + * + * The general contract of the method `run` is that it may + * take any action whatsoever. + * + * @see java.lang.Thread.run + */ + override fun run() { + queue.poll()?.run() + } +} diff --git a/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/EventExecutionType.kt b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/EventExecutionType.kt new file mode 100644 index 00000000..ff6c43a7 --- /dev/null +++ b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/EventExecutionType.kt @@ -0,0 +1,9 @@ +package com.github.shynixn.mccoroutine.folia + +/** + * The mode how suspendable events are executed if dispatched manually. + */ +enum class EventExecutionType { + Consecutive, + Concurrent +} diff --git a/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/MCCoroutine.kt b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/MCCoroutine.kt new file mode 100644 index 00000000..51eef326 --- /dev/null +++ b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/MCCoroutine.kt @@ -0,0 +1,275 @@ +package com.github.shynixn.mccoroutine.folia + +import kotlinx.coroutines.* +import org.bukkit.Location +import org.bukkit.World +import org.bukkit.command.PluginCommand +import org.bukkit.entity.Entity +import org.bukkit.event.Event +import org.bukkit.event.Listener +import org.bukkit.plugin.Plugin +import org.bukkit.plugin.PluginManager +import kotlin.coroutines.ContinuationInterceptor +import kotlin.coroutines.CoroutineContext + +/** + * Static session for all plugins. + */ +internal val mcCoroutine: MCCoroutine by lazy { + try { + Class.forName(MCCoroutine.Driver) + .getDeclaredConstructor().newInstance() as MCCoroutine + } catch (e: Exception) { + throw RuntimeException( + "Failed to load MCCoroutine implementation. Shade mccoroutine-folia-core into your plugin.", + e + ) + } +} + +/** + * Gets the configuration instance of MCCoroutine. + */ +val Plugin.mcCoroutineConfiguration: MCCoroutineConfiguration + get() { + return mcCoroutine.getCoroutineSession(this).mcCoroutineConfiguration + } + +/** + * Gets the dispatcher to perform edits on data that the global region owns, such as game rules, day time, weather, or to execute commands using the console command sender. + * If Folia is not loaded, this falls back to the bukkit minecraftDispatcher. + */ +val Plugin.globalRegionDispatcher: CoroutineContext + get() { + return mcCoroutine.getCoroutineSession(this).dispatcherGlobalRegion + } + +/** + * Gets the plugin async dispatcher. + */ +val Plugin.asyncDispatcher: CoroutineContext + get() { + return mcCoroutine.getCoroutineSession(this).dispatcherAsync + } + +/** + * Gets the dispatcher to schedule tasks on the region that owns the entity. + * If Folia is not loaded, this falls back to the bukkit minecraftDispatcher. + */ +fun Plugin.entityDispatcher(entity: Entity): CoroutineContext { + return mcCoroutine.getCoroutineSession(this).getEntityDispatcher(entity) +} + +/** + * Gets the dispatcher to schedule tasks on a particular region. + * If Folia is not loaded, this falls back to the bukkit minecraftDispatcher. + */ +fun Plugin.regionDispatcher(location: Location): CoroutineContext { + return mcCoroutine.getCoroutineSession(this) + .getRegionDispatcher(location.world!!, location.blockX shr 4, location.blockZ shr 4) +} + +/** + * Gets the dispatcher to schedule tasks on a particular region. + * If Folia is not loaded, this falls back to the bukkit minecraftDispatcher. + */ +fun Plugin.regionDispatcher(world: World, chunkX: Int, chunkZ: Int): CoroutineContext { + return mcCoroutine.getCoroutineSession(this) + .getRegionDispatcher(world, chunkX, chunkZ) +} + +/** + * Gets the plugin coroutine scope. + */ +val Plugin.scope: CoroutineScope + get() { + return mcCoroutine.getCoroutineSession(this).scope + } + +/** + * Launches a new coroutine on the current thread without blocking the current thread and returns a reference to the coroutine as a [Job]. + * The coroutine is cancelled when the resulting job is [cancelled][Job.cancel]. + * + * The coroutine context is inherited from a [Plugin.scope]. Additional context elements can be specified with [context] argument. + * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then Unconfined Dispatcher is used. + * The parent job is inherited from a [Plugin.scope] as well, but it can also be overridden + * with a corresponding [context] element. + * + * By default, the coroutine is immediately scheduled on the current calling thread. However, manipulating global data, entities or locations + * is not safe in this context. Use subsequent operations for this case e.g. withContext(plugin.entityDispatcher(entity)) {} or withContext(plugin.regionDispatcher(location)) {} + * Other start options can be specified via `start` parameter. See [CoroutineStart] for details. + * An optional [start] parameter can be set to [CoroutineStart.LAZY] to start coroutine _lazily_. In this case, + * the coroutine [Job] is created in _new_ state. It can be explicitly started with [start][Job.start] function + * and will be started implicitly on the first invocation of [join][Job.join]. + * + * Uncaught exceptions in this coroutine do not cancel the parent job or any other child jobs. All uncaught exceptions + * are logged to [Plugin.getLogger] by default. + * + * @param context The coroutine context to start. As the context of the current operation cannot be assumed automatically, the caller needs to specify a context. + * e.g. regionDispatcher, entityDispatcher or globalRegionDispatcher. + * @param start coroutine start option. The default value is [CoroutineStart.DEFAULT]. + * @param block the coroutine code which will be invoked in the context of the provided scope. + **/ +fun Plugin.launch( + context: CoroutineContext, + start: CoroutineStart, + block: suspend CoroutineScope.() -> Unit +): Job { + if (!scope.isActive) { + return Job() + } + + return scope.launch(context, start, block) +} + +/** + * Registers an event listener with suspending functions. + * Does exactly the same thing as PluginManager.registerEvents but makes suspend functions + * possible. + * Example: + * + * class MyPlayerJoinListener : Listener{ + * @EventHandler + * suspend fun onPlayerJoinEvent(event: PlayerJoinEvent) { + * + * } + * } + * + * @param listener Bukkit Listener. + * @param plugin Bukkit Plugin. + * @param eventDispatcher Folia uses different schedulers for different event types. MCCoroutine cannot detect them per default and requires a mapping for + * each used event type in the given [listener]. This method throws an exception if you forget to map an event type. See wiki for details. + */ +fun PluginManager.registerSuspendingEvents( + listener: Listener, + plugin: Plugin, + eventDispatcher: Map, (event: Event) -> CoroutineContext> +) { + return mcCoroutine.getCoroutineSession(plugin).registerSuspendListener(listener, eventDispatcher) +} + +/** + * Calls an event with the given details. + * If there are multiple suspend event receivers, each receiver is executed concurrently. + * Allows to await the completion of suspending event listeners. + * + * @param event Event details. + * @param plugin Plugin plugin. + * @return Collection of awaitable jobs. This job list may be empty if no suspending listener + * was called. Each job instance represents an awaitable job for each method being called in each suspending listener. + * For awaiting use callSuspendingEvent(..).joinAll(). + */ +fun PluginManager.callSuspendingEvent(event: Event, plugin: Plugin): Collection { + return callSuspendingEvent(event, plugin, EventExecutionType.Concurrent) +} + +/** + * Calls an event with the given details. + * Allows to await the completion of suspending event listeners. + * + * @param event Event details. + * @param plugin Plugin plugin. + * @param eventExecutionType Allows to specify how suspend receivers are executed. + * @return Collection of awaitable jobs. This job list may be empty if no suspending listener + * was called. Each job instance represents an awaitable job for each method being called in each suspending listener. + * For awaiting use callSuspendingEvent(..).joinAll(). + */ +fun PluginManager.callSuspendingEvent( + event: Event, + plugin: Plugin, + eventExecutionType: EventExecutionType +): Collection { + return mcCoroutine.getCoroutineSession(plugin).fireSuspendingEvent(event, eventExecutionType) +} + +/** + * Registers a command executor with suspending function. + * Does exactly the same as PluginCommand.setExecutor. + */ +fun PluginCommand.setSuspendingExecutor( + suspendingCommandExecutor: SuspendingCommandExecutor +) { + return mcCoroutine.getCoroutineSession(plugin).registerSuspendCommandExecutor( + plugin.globalRegionDispatcher, + this, + suspendingCommandExecutor + ) +} + +/** + * Registers a command executor with suspending function. + * Does exactly the same as PluginCommand.setExecutor. + * @param context The coroutine context to start. Should almost be always be [Plugin.minecraftDispatcher]. + */ +fun PluginCommand.setSuspendingExecutor( + context: CoroutineContext, + suspendingCommandExecutor: SuspendingCommandExecutor +) { + return mcCoroutine.getCoroutineSession(plugin).registerSuspendCommandExecutor( + context, + this, + suspendingCommandExecutor + ) +} + +/** + * Registers a tab completer with suspending function. + * Does exactly the same as PluginCommand.setExecutor. + */ +fun PluginCommand.setSuspendingTabCompleter(suspendingTabCompleter: SuspendingTabCompleter) { + return mcCoroutine.getCoroutineSession(plugin).registerSuspendTabCompleter( + plugin.globalRegionDispatcher, + this, + suspendingTabCompleter + ) +} + + +/** + * Registers a tab completer with suspending function. + * Does exactly the same as PluginCommand.setExecutor. + * @param context The coroutine context to start. Should almost be always be [Plugin.minecraftDispatcher]. + */ +fun PluginCommand.setSuspendingTabCompleter(context: CoroutineContext, suspendingTabCompleter: SuspendingTabCompleter) { + return mcCoroutine.getCoroutineSession(plugin).registerSuspendTabCompleter( + context, + this, + suspendingTabCompleter + ) +} + +/** + * Converts the number to ticks for being used together with delay(..). + * E.g. delay(1.ticks). + * Minecraft ticks 20 times per second, which means a tick appears every 50 milliseconds. However, + * delay() does not directly work with the FoliaScheduler and needs millisecond manipulation to + * work as expected. Therefore, 1 tick does not equal 50 milliseconds when using this method standalone and only + * sums up to 50 milliseconds if you use it together with delay. + */ +val Int.ticks: Long + get() { + return (this * 50L - 25) + } + +/** + * Hidden internal MCCoroutine interface. + */ +interface MCCoroutine { + companion object { + /** + * Allows to change the driver to load different kinds of MCCoroutine implementations. + * e.g. loading the implementation for UnitTests. + */ + var Driver: String = "com.github.shynixn.mccoroutine.folia.impl.MCCoroutineImpl" + } + + /** + * Get coroutine session for the given plugin. + */ + fun getCoroutineSession(plugin: Plugin): CoroutineSession + + /** + * Disposes the given plugin. + */ + fun disable(plugin: Plugin) +} diff --git a/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/MCCoroutineConfiguration.kt b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/MCCoroutineConfiguration.kt new file mode 100644 index 00000000..a455e8ea --- /dev/null +++ b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/MCCoroutineConfiguration.kt @@ -0,0 +1,26 @@ +package com.github.shynixn.mccoroutine.folia + +/** + * Additional configurations for MCCoroutine and communication. + */ +interface MCCoroutineConfiguration { + /** + * Strategy handling how MCCoroutine is disposed. + * Defaults to ShutdownStrategy.SCHEDULER. + * + * Changing this setting may have an impact on All suspend function you call in + * onDisable(). Carefully verify your changes. + */ + var shutdownStrategy: ShutdownStrategy + + /** + * Gets if the Folia schedulers where successfully loaded into MCCoroutine. + * Returns false if MCCoroutine falls back to the BukkitScheduler. + */ + val isFoliaLoaded: Boolean + + /** + * Manually disposes the MCCoroutine session for the current plugin. + */ + fun disposePluginSession() +} diff --git a/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/MCCoroutineExceptionEvent.kt b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/MCCoroutineExceptionEvent.kt new file mode 100644 index 00000000..ca555217 --- /dev/null +++ b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/MCCoroutineExceptionEvent.kt @@ -0,0 +1,62 @@ +package com.github.shynixn.mccoroutine.folia + +import org.bukkit.event.Cancellable +import org.bukkit.event.HandlerList +import org.bukkit.event.server.ServerEvent +import org.bukkit.plugin.Plugin + +/** + * A Bukkit event which is called when an exception is raised in one of the coroutines managed by MCCoroutine. + * Cancelling this exception causes the error to not get logged and offers to possibility for custom logging. + */ +class MCCoroutineExceptionEvent( + /** + * Plugin causing the exception. + */ + val plugin: Plugin, + /** + * The exception to be logged. + */ + val exception: Throwable + // Paper requires explicit isAsync false flag. +) : ServerEvent(false), Cancellable { + private var cancelled: Boolean = false + + /** + * Event. + */ + companion object { + private var handlers = HandlerList() + + /** + * Handlerlist. + */ + @JvmStatic + fun getHandlerList(): HandlerList { + return handlers + } + } + + /** + * Returns all handles. + */ + override fun getHandlers(): HandlerList { + return MCCoroutineExceptionEvent.handlers + } + + /** + * Gets if this event is cancelled. + */ + override fun isCancelled(): Boolean { + return cancelled + } + + /** + * Sets the event as cancelled or not. If the event is cancelled + * the exception is seen as an uncaught exception. Do only cancel this event + * if you want to log the exceptions on your own. + */ + override fun setCancelled(flag: Boolean) { + this.cancelled = flag + } +} diff --git a/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/ShutdownStrategy.kt b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/ShutdownStrategy.kt new file mode 100644 index 00000000..372494f9 --- /dev/null +++ b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/ShutdownStrategy.kt @@ -0,0 +1,17 @@ +package com.github.shynixn.mccoroutine.folia + +/** + * See https://shynixn.github.io/MCCoroutine/wiki/site/plugindisable for more details. + */ +enum class ShutdownStrategy { + /** + * Default shutdown strategy. The coroutine session is + * disposed automatically on plugin disable along with the BukkitScheduler. + */ + SCHEDULER, + + /** + * The coroutine session needs to be explicitly disposed. + */ + MANUAL +} diff --git a/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/SuspendingCommandExecutor.kt b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/SuspendingCommandExecutor.kt new file mode 100644 index 00000000..51bfc10d --- /dev/null +++ b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/SuspendingCommandExecutor.kt @@ -0,0 +1,20 @@ +package com.github.shynixn.mccoroutine.folia + +import org.bukkit.command.Command +import org.bukkit.command.CommandSender + +/** + * Represents a class which contains a single method for executing commands + */ +interface SuspendingCommandExecutor { + /** + * Executes the given command, returning its success. + * If false is returned, then the "usage" plugin.yml entry for this command (if defined) will be sent to the player. + * @param sender - Source of the command. + * @param command - Command which was executed. + * @param label - Alias of the command which was used. + * @param args - Passed command arguments. + * @return True if a valid command, otherwise false. + */ + suspend fun onCommand(sender: CommandSender, command: Command, label: String, args: Array): Boolean +} diff --git a/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/SuspendingJavaPlugin.kt b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/SuspendingJavaPlugin.kt new file mode 100644 index 00000000..dfbd76ab --- /dev/null +++ b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/SuspendingJavaPlugin.kt @@ -0,0 +1,65 @@ +package com.github.shynixn.mccoroutine.folia + +import kotlinx.coroutines.runBlocking +import org.bukkit.plugin.java.JavaPlugin + +/** + * Extension to the JavaPlugin for suspendable lifecycle functions. + */ +open class SuspendingJavaPlugin : JavaPlugin(), SuspendingPlugin { + /** + * Called when this plugin is enabled + */ + override suspend fun onEnableAsync() { + } + + /** + * Called when this plugin is disabled. + */ + override suspend fun onDisableAsync() { + } + + /** + * Called after a plugin is loaded but before it has been enabled. + * + * + * When multiple plugins are loaded, the onLoad() for all plugins is + * called before any onEnable() is called. + */ + override suspend fun onLoadAsync() { + } + + /** + * Called when this plugin is enabled + */ + override fun onEnable() { + mcCoroutine.getCoroutineSession(this).isManipulatedServerHeartBeatEnabled = true + runBlocking { + onEnableAsync() + } + // Disables runBlocking hack to not interfere with other tasks. + mcCoroutine.getCoroutineSession(this).isManipulatedServerHeartBeatEnabled = false + } + + /** + * Called when this plugin is disabled + */ + override fun onDisable() { + runBlocking { + onDisableAsync() + } + } + + /** + * Called after a plugin is loaded but before it has been enabled. + * + * + * When multiple plugins are loaded, the onLoad() for all plugins is + * called before any onEnable() is called. + */ + override fun onLoad() { + runBlocking { + onLoadAsync() + } + } +} diff --git a/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/SuspendingPlugin.kt b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/SuspendingPlugin.kt new file mode 100644 index 00000000..7ad4cc12 --- /dev/null +++ b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/SuspendingPlugin.kt @@ -0,0 +1,27 @@ +package com.github.shynixn.mccoroutine.folia + +import org.bukkit.plugin.Plugin + +/** + * Extension to the plugin interface for suspendable lifecycle functions. + */ +interface SuspendingPlugin : Plugin { + /** + * Called when this plugin is enabled + */ + suspend fun onEnableAsync(); + + /** + * Called when this plugin is disabled. + */ + suspend fun onDisableAsync(); + + /** + * Called after a plugin is loaded but before it has been enabled. + * + * + * When multiple plugins are loaded, the onLoad() for all plugins is + * called before any onEnable() is called. + */ + suspend fun onLoadAsync(); +} diff --git a/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/SuspendingTabCompleter.kt b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/SuspendingTabCompleter.kt new file mode 100644 index 00000000..7b6b5811 --- /dev/null +++ b/mccoroutine-folia-api/src/main/java/com/github/shynixn/mccoroutine/folia/SuspendingTabCompleter.kt @@ -0,0 +1,25 @@ +package com.github.shynixn.mccoroutine.folia + +import org.bukkit.command.Command +import org.bukkit.command.CommandSender + +/** + * Represents a suspending class which can suggest tab completions for commands. + */ +interface SuspendingTabCompleter { + /** + * Requests a list of possible completions for a command argument. + * If the call is suspended during the execution, the returned list will not be shown. + * @param sender - Source of the command. + * @param command - Command which was executed. + * @param alias - Alias of the command which was used. + * @param args - The arguments passed to the command, including final partial argument to be completed and command label. + * @return A List of possible completions for the final argument, or null to default to the command executor + */ + suspend fun onTabComplete( + sender: CommandSender, + command: Command, + alias: String, + args: Array + ): List? +} diff --git a/mccoroutine-folia-core/build.gradle.kts b/mccoroutine-folia-core/build.gradle.kts new file mode 100644 index 00000000..ef95f616 --- /dev/null +++ b/mccoroutine-folia-core/build.gradle.kts @@ -0,0 +1,22 @@ +repositories { + maven { + url = uri("https://papermc.io/repo/repository/maven-public/") + } +} + + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +dependencies { + implementation(project(":mccoroutine-folia-api")) + + compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") + compileOnly("dev.folia:folia-api:1.20.1-R0.1-20230615.235213-1") + + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") + testImplementation("dev.folia:folia-api:1.20.1-R0.1-20230615.235213-1") +} diff --git a/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/AsyncCoroutineDispatcher.kt b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/AsyncCoroutineDispatcher.kt new file mode 100644 index 00000000..2a0e90a2 --- /dev/null +++ b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/AsyncCoroutineDispatcher.kt @@ -0,0 +1,32 @@ +package com.github.shynixn.mccoroutine.folia.dispatcher + +import com.github.shynixn.mccoroutine.folia.service.WakeUpBlockServiceImpl +import kotlinx.coroutines.CoroutineDispatcher +import org.bukkit.plugin.Plugin +import kotlin.coroutines.CoroutineContext + +/** + * CraftBukkit Async ThreadPool Dispatcher. Dispatches always. + */ +internal open class AsyncCoroutineDispatcher( + private val plugin: Plugin, + private val wakeUpBlockService: WakeUpBlockServiceImpl +) : CoroutineDispatcher() { + /** + * Returns `true` if the execution of the coroutine should be performed with [dispatch] method. + * The default behavior for most dispatchers is to return `true`. + * This method should generally be exception-safe. An exception thrown from this method + * may leave the coroutines that use this dispatcher in the inconsistent and hard to debug state. + */ + override fun isDispatchNeeded(context: CoroutineContext): Boolean { + wakeUpBlockService.ensureWakeup() + return true + } + + /** + * Handles dispatching the coroutine on the correct thread. + */ + override fun dispatch(context: CoroutineContext, block: Runnable) { + plugin.server.scheduler.runTaskAsynchronously(plugin, block) + } +} diff --git a/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/AsyncFoliaCoroutineDispatcher.kt b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/AsyncFoliaCoroutineDispatcher.kt new file mode 100644 index 00000000..428e93d0 --- /dev/null +++ b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/AsyncFoliaCoroutineDispatcher.kt @@ -0,0 +1,32 @@ +package com.github.shynixn.mccoroutine.folia.dispatcher + +import com.github.shynixn.mccoroutine.folia.service.WakeUpBlockServiceImpl +import kotlinx.coroutines.CoroutineDispatcher +import org.bukkit.plugin.Plugin +import kotlin.coroutines.CoroutineContext + +/** + * CraftBukkit Async ThreadPool Dispatcher. Dispatches in all cases. + */ +internal open class AsyncFoliaCoroutineDispatcher( + private val plugin: Plugin, + private val wakeUpBlockService: WakeUpBlockServiceImpl +) : CoroutineDispatcher() { + /** + * Returns `true` if the execution of the coroutine should be performed with [dispatch] method. + * The default behavior for most dispatchers is to return `true`. + * This method should generally be exception-safe. An exception thrown from this method + * may leave the coroutines that use this dispatcher in the inconsistent and hard to debug state. + */ + override fun isDispatchNeeded(context: CoroutineContext): Boolean { + wakeUpBlockService.ensureWakeup() + return true + } + + /** + * Handles dispatching the coroutine on the correct thread. + */ + override fun dispatch(context: CoroutineContext, block: Runnable) { + plugin.server.asyncScheduler.runNow(plugin) { block.run() } + } +} diff --git a/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/EntityDispatcher.kt b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/EntityDispatcher.kt new file mode 100644 index 00000000..65a998b2 --- /dev/null +++ b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/EntityDispatcher.kt @@ -0,0 +1,39 @@ +package com.github.shynixn.mccoroutine.folia.dispatcher + +import com.github.shynixn.mccoroutine.folia.service.WakeUpBlockServiceImpl +import kotlinx.coroutines.CoroutineDispatcher +import org.bukkit.entity.Entity +import org.bukkit.plugin.Plugin +import kotlin.coroutines.CoroutineContext + +internal open class EntityDispatcher( + private val plugin: Plugin, + private val wakeUpBlockService: WakeUpBlockServiceImpl, + private val entity: Entity +) : CoroutineDispatcher() { + /** + * Returns `true` if the execution of the coroutine should be performed with [dispatch] method. + * The default behavior for most dispatchers is to return `true`. + * This method should generally be exception-safe. An exception thrown from this method + * may leave the coroutines that use this dispatcher in the inconsistent and hard to debug state. + */ + override fun isDispatchNeeded(context: CoroutineContext): Boolean { + wakeUpBlockService.ensureWakeup() + return true + } + + /** + * Handles dispatching the coroutine on the correct thread. + */ + override fun dispatch(context: CoroutineContext, block: Runnable) { + val task = entity.scheduler.run(plugin, { + block.run() + }, { + block.run() + }) + + if (task == null) { // Entity was removed. Execute on global region scheduler. + plugin.server.globalRegionScheduler.execute(plugin, block) + } + } +} diff --git a/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/GlobalRegionDispatcher.kt b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/GlobalRegionDispatcher.kt new file mode 100644 index 00000000..660471f4 --- /dev/null +++ b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/GlobalRegionDispatcher.kt @@ -0,0 +1,29 @@ +package com.github.shynixn.mccoroutine.folia.dispatcher + +import com.github.shynixn.mccoroutine.folia.service.WakeUpBlockServiceImpl +import kotlinx.coroutines.CoroutineDispatcher +import org.bukkit.plugin.Plugin +import kotlin.coroutines.CoroutineContext + +internal open class GlobalRegionDispatcher( + private val plugin: Plugin, + private val wakeUpBlockService: WakeUpBlockServiceImpl +) : CoroutineDispatcher() { + /** + * Returns `true` if the execution of the coroutine should be performed with [dispatch] method. + * The default behavior for most dispatchers is to return `true`. + * This method should generally be exception-safe. An exception thrown from this method + * may leave the coroutines that use this dispatcher in the inconsistent and hard to debug state. + */ + override fun isDispatchNeeded(context: CoroutineContext): Boolean { + wakeUpBlockService.ensureWakeup() + return true + } + + /** + * Handles dispatching the coroutine on the correct thread. + */ + override fun dispatch(context: CoroutineContext, block: Runnable) { + plugin.server.globalRegionScheduler.execute(plugin, block) + } +} diff --git a/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/MinecraftCoroutineDispatcher.kt b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/MinecraftCoroutineDispatcher.kt new file mode 100644 index 00000000..0a7aee7e --- /dev/null +++ b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/MinecraftCoroutineDispatcher.kt @@ -0,0 +1,43 @@ +package com.github.shynixn.mccoroutine.folia.dispatcher + +import com.github.shynixn.mccoroutine.folia.CoroutineTimings +import com.github.shynixn.mccoroutine.folia.service.WakeUpBlockServiceImpl +import kotlinx.coroutines.CoroutineDispatcher +import org.bukkit.plugin.Plugin +import kotlin.coroutines.CoroutineContext + +/** + * Server Main Thread Dispatcher. Dispatches only if the call is not at the primary thread yet. + */ +internal open class MinecraftCoroutineDispatcher( + private val plugin: Plugin, + private val wakeUpBlockService: WakeUpBlockServiceImpl +) : CoroutineDispatcher() { + /** + * Returns `true` if the execution of the coroutine should be performed with [dispatch] method. + * The default behavior for most dispatchers is to return `true`. + * This method should generally be exception-safe. An exception thrown from this method + * may leave the coroutines that use this dispatcher in the inconsistent and hard to debug state. + */ + override fun isDispatchNeeded(context: CoroutineContext): Boolean { + wakeUpBlockService.ensureWakeup() + return !plugin.server.isPrimaryThread && plugin.isEnabled + } + + /** + * Handles dispatching the coroutine on the correct thread. + */ + override fun dispatch(context: CoroutineContext, block: Runnable) { + if (!plugin.isEnabled) { + return + } + + val timedRunnable = context[CoroutineTimings.Key] + if (timedRunnable == null) { + plugin.server.scheduler.runTask(plugin, block) + } else { + timedRunnable.queue.add(block) + plugin.server.scheduler.runTask(plugin, timedRunnable) + } + } +} diff --git a/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/RegionDispatcher.kt b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/RegionDispatcher.kt new file mode 100644 index 00000000..7e19d5da --- /dev/null +++ b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/dispatcher/RegionDispatcher.kt @@ -0,0 +1,33 @@ +package com.github.shynixn.mccoroutine.folia.dispatcher + +import com.github.shynixn.mccoroutine.folia.service.WakeUpBlockServiceImpl +import kotlinx.coroutines.CoroutineDispatcher +import org.bukkit.World +import org.bukkit.plugin.Plugin +import kotlin.coroutines.CoroutineContext + +internal open class RegionDispatcher( + private val plugin: Plugin, + private val wakeUpBlockService: WakeUpBlockServiceImpl, + private val world: World, + private val chunkX: Int, + private val chunkZ: Int +) : CoroutineDispatcher() { + /** + * Returns `true` if the execution of the coroutine should be performed with [dispatch] method. + * The default behavior for most dispatchers is to return `true`. + * This method should generally be exception-safe. An exception thrown from this method + * may leave the coroutines that use this dispatcher in the inconsistent and hard to debug state. + */ + override fun isDispatchNeeded(context: CoroutineContext): Boolean { + wakeUpBlockService.ensureWakeup() + return true + } + + /** + * Handles dispatching the coroutine on the correct thread. + */ + override fun dispatch(context: CoroutineContext, block: Runnable) { + plugin.server.regionScheduler.execute(plugin, world, chunkX, chunkZ, block) + } +} diff --git a/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/extension/Extension.kt b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/extension/Extension.kt new file mode 100644 index 00000000..9f89e865 --- /dev/null +++ b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/extension/Extension.kt @@ -0,0 +1,39 @@ +package com.github.shynixn.mccoroutine.folia.extension + +import org.bukkit.plugin.Plugin +import java.lang.reflect.Method + +/** + * Internal reflection suspend. + */ +internal suspend fun Method.invokeSuspend(obj: Any, vararg args: Any?): Any? = + kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn { cont -> + invoke(obj, *args, cont) + } + + +private var serverVersionInternal: String? = null + +/** + * Gets the server NMS version. + */ +internal val Plugin.serverVersion: String + get() { + if (serverVersionInternal == null) { + serverVersionInternal = server.javaClass.getPackage().name.replace(".", ",").split(",")[3] + } + + return serverVersionInternal!! + } + +/** + * Finds the version compatible class. + */ +internal fun Plugin.findClazz(name: String): Class<*> { + return Class.forName( + name.replace( + "VERSION", + serverVersion + ) + ) +} diff --git a/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/impl/CoroutineSessionImpl.kt b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/impl/CoroutineSessionImpl.kt new file mode 100644 index 00000000..49f94ce4 --- /dev/null +++ b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/impl/CoroutineSessionImpl.kt @@ -0,0 +1,199 @@ +package com.github.shynixn.mccoroutine.folia.impl + +import com.github.shynixn.mccoroutine.folia.* +import com.github.shynixn.mccoroutine.folia.dispatcher.* +import com.github.shynixn.mccoroutine.folia.dispatcher.AsyncFoliaCoroutineDispatcher +import com.github.shynixn.mccoroutine.folia.dispatcher.EntityDispatcher +import com.github.shynixn.mccoroutine.folia.dispatcher.GlobalRegionDispatcher +import com.github.shynixn.mccoroutine.folia.dispatcher.RegionDispatcher +import com.github.shynixn.mccoroutine.folia.service.CommandServiceImpl +import com.github.shynixn.mccoroutine.folia.service.EventServiceImpl +import com.github.shynixn.mccoroutine.folia.service.WakeUpBlockServiceImpl +import kotlinx.coroutines.* +import org.bukkit.World +import org.bukkit.command.PluginCommand +import org.bukkit.entity.Entity +import org.bukkit.event.Event +import org.bukkit.event.Listener +import org.bukkit.plugin.Plugin +import java.util.logging.Level +import kotlin.coroutines.CoroutineContext + +internal class CoroutineSessionImpl( + private val plugin: Plugin, + override val mcCoroutineConfiguration: MCCoroutineConfiguration +) : + CoroutineSession { + /** + * Gets if the Folia schedulers where successfully loaded into MCCoroutine. + * Returns false if MCCoroutine falls back to the BukkitScheduler. + */ + override val isFoliaLoaded: Boolean by lazy { + try { + Class.forName("io.papermc.paper.threadedregions.scheduler.EntityScheduler") + true + } catch (e: ClassNotFoundException) { + false + } + } + + /** + * Gets the block service during startup. + */ + private val wakeUpBlockService: WakeUpBlockServiceImpl by lazy { + WakeUpBlockServiceImpl(plugin) + } + + /** + * Gets the event service. + */ + private val eventService: EventServiceImpl by lazy { + EventServiceImpl(plugin, this) + } + + /** + * Gets the command service. + */ + private val commandService: CommandServiceImpl by lazy { + CommandServiceImpl(plugin) + } + + /** + * Gets minecraft coroutine scope. + */ + override val scope: CoroutineScope + + /** + * The global region dispatcher is simply used to perform edits on data that the global region owns, such as game rules, day time, weather, or to execute commands using the console command sender. + */ + override val dispatcherGlobalRegion: CoroutineContext by lazy { + if (isFoliaLoaded) { + GlobalRegionDispatcher(plugin, wakeUpBlockService) + } else { + MinecraftCoroutineDispatcher(plugin, wakeUpBlockService) + } + } + + /** + * Gets the async dispatcher. + */ + override val dispatcherAsync: CoroutineContext by lazy { + if (isFoliaLoaded) { + AsyncFoliaCoroutineDispatcher(plugin, wakeUpBlockService) + } else { + AsyncCoroutineDispatcher(plugin, wakeUpBlockService) + } + } + + /** + * The RegionizedTaskQueue allows tasks to be scheduled to be executed on the next tick of a region that owns a specific location, or creating such region if it does not exist. + */ + override fun getRegionDispatcher(world: World, chunkX: Int, chunkZ: Int): CoroutineContext { + if (isFoliaLoaded) { + return RegionDispatcher(plugin, wakeUpBlockService, world, chunkX, chunkZ) + } + + return dispatcherGlobalRegion // minecraftDispatcher on BukkitOnly servers + } + + /** + The EntityScheduler allows tasks to be scheduled to be executed on the region that owns the entity. + */ + override fun getEntityDispatcher(entity: Entity): CoroutineContext { + if (isFoliaLoaded) { + return EntityDispatcher(plugin, wakeUpBlockService, entity) + } + + return dispatcherGlobalRegion // minecraftDispatcher on BukkitOnly servers + } + + /** + * Manipulates the bukkit server heart beat on startup. + */ + override var isManipulatedServerHeartBeatEnabled: Boolean + get() { + return wakeUpBlockService.isManipulatedServerHeartBeatEnabled + } + set(value) { + wakeUpBlockService.isManipulatedServerHeartBeatEnabled = value + } + + init { + // Root Exception Handler. All Exception which are not consumed by the caller end up here. + val exceptionHandler = CoroutineExceptionHandler { _, e -> + val mcCoroutineExceptionEvent = MCCoroutineExceptionEvent(plugin, e) + + if (plugin.isEnabled) { + plugin.launch(plugin.globalRegionDispatcher, CoroutineStart.DEFAULT){ + plugin.server.pluginManager.callEvent(mcCoroutineExceptionEvent) + + if (!mcCoroutineExceptionEvent.isCancelled) { + if (e !is CancellationException) { + plugin.logger.log( + Level.SEVERE, + "This is not an error of MCCoroutine! See sub exception for details.", + e + ) + } + } + } + } + } + + // Build Coroutine plugin scope for exception handling + val rootCoroutineScope = CoroutineScope(exceptionHandler) + + // Minecraft Scope is child of plugin scope and super visor job (e.g. children of a supervisor job can fail independently). + scope = rootCoroutineScope + SupervisorJob() + dispatcherGlobalRegion + } + + /** + * Registers a suspend command executor. + */ + override fun registerSuspendCommandExecutor( + context: CoroutineContext, + pluginCommand: PluginCommand, + commandExecutor: SuspendingCommandExecutor + ) { + commandService.registerSuspendCommandExecutor(context, pluginCommand, commandExecutor) + } + + /** + * Registers a suspend tab completer. + */ + override fun registerSuspendTabCompleter( + context: CoroutineContext, + pluginCommand: PluginCommand, + tabCompleter: SuspendingTabCompleter + ) { + commandService.registerSuspendTabCompleter(context, pluginCommand, tabCompleter) + } + + /** + * Registers a suspend listener. + */ + override fun registerSuspendListener( + listener: Listener, + eventDispatcher: Map, (event: Event) -> CoroutineContext> + ) { + eventService.registerSuspendListener(listener, eventDispatcher) + } + + + /** + * Fires a suspending [event] with the given [eventExecutionType]. + * @return Collection of receiver jobs. May already be completed. + */ + override fun fireSuspendingEvent(event: Event, eventExecutionType: EventExecutionType): Collection { + return eventService.fireSuspendingEvent(event, eventExecutionType) + } + + /** + * Disposes the session. + */ + fun dispose() { + scope.coroutineContext.cancelChildren() + scope.cancel() + wakeUpBlockService.dispose() + } +} diff --git a/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/impl/MCCoroutineConfigurationImpl.kt b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/impl/MCCoroutineConfigurationImpl.kt new file mode 100644 index 00000000..36502070 --- /dev/null +++ b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/impl/MCCoroutineConfigurationImpl.kt @@ -0,0 +1,31 @@ +package com.github.shynixn.mccoroutine.folia.impl + +import com.github.shynixn.mccoroutine.folia.MCCoroutine +import com.github.shynixn.mccoroutine.folia.MCCoroutineConfiguration +import com.github.shynixn.mccoroutine.folia.ShutdownStrategy +import org.bukkit.plugin.Plugin + +internal class MCCoroutineConfigurationImpl(private val plugin: Plugin, private val mcCoroutine: MCCoroutine) : + MCCoroutineConfiguration { + /** + * Strategy handling how MCCoroutine is disposed. + * Defaults to ShutdownStrategy.SCHEDULER. + */ + override var shutdownStrategy: ShutdownStrategy = ShutdownStrategy.SCHEDULER + + /** + * Gets if the Folia schedulers where successfully loaded into MCCoroutine. + * Returns false if MCCoroutine falls back to the BukkitScheduler. + */ + override val isFoliaLoaded: Boolean + get() { + return mcCoroutine.getCoroutineSession(plugin).isFoliaLoaded + } + + /** + * Manually disposes the MCCoroutine session for the given plugin. + */ + override fun disposePluginSession() { + mcCoroutine.disable(plugin) + } +} diff --git a/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/impl/MCCoroutineImpl.kt b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/impl/MCCoroutineImpl.kt new file mode 100644 index 00000000..8a73f017 --- /dev/null +++ b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/impl/MCCoroutineImpl.kt @@ -0,0 +1,51 @@ +package com.github.shynixn.mccoroutine.folia.impl + +import com.github.shynixn.mccoroutine.folia.CoroutineSession +import com.github.shynixn.mccoroutine.folia.MCCoroutine +import com.github.shynixn.mccoroutine.folia.listener.PluginListener +import org.bukkit.plugin.Plugin + +/** + * A singleton implementation which keeps all coroutine sessions of all plugins. + */ +class MCCoroutineImpl : MCCoroutine { + private val items = HashMap() + + /** + * Get coroutine session for the given plugin. + */ + override fun getCoroutineSession(plugin: Plugin): CoroutineSession { + if (!items.containsKey(plugin)) { + startCoroutineSession(plugin) + } + + return items[plugin]!! + } + + /** + * Disables coroutine for the given plugin. + */ + override fun disable(plugin: Plugin) { + if (!items.containsKey(plugin)) { + return + } + + val session = items[plugin]!! + session.dispose() + items.remove(plugin) + } + + /** + * Starts a new coroutine session. + */ + private fun startCoroutineSession(plugin: Plugin) { + if (!plugin.isEnabled) { + throw RuntimeException("Plugin ${plugin.name} attempted to start a new coroutine session while being disabled. Dispatchers such as plugin.minecraftDispatcher and plugin.asyncDispatcher are using the BukkitScheduler, which is already disposed at this point of time. If you are starting a coroutine in onDisable, consider using runBlocking or a different plugin.mcCoroutineConfiguration.shutdownStrategy. See https://shynixn.github.io/MCCoroutine/wiki/site/plugindisable for details.") + } + + val pluginListener = PluginListener(this, plugin) + val coroutineFacade = MCCoroutineConfigurationImpl(plugin, this) + items[plugin] = CoroutineSessionImpl(plugin, coroutineFacade) + plugin.server.pluginManager.registerEvents(pluginListener, plugin) + } +} diff --git a/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/listener/PluginListener.kt b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/listener/PluginListener.kt new file mode 100644 index 00000000..c0501d8d --- /dev/null +++ b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/listener/PluginListener.kt @@ -0,0 +1,34 @@ +package com.github.shynixn.mccoroutine.folia.listener + +import com.github.shynixn.mccoroutine.folia.MCCoroutine +import com.github.shynixn.mccoroutine.folia.ShutdownStrategy +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.server.PluginDisableEvent +import org.bukkit.plugin.Plugin + +internal class PluginListener : Listener { + private val mcCoroutine: MCCoroutine + private val plugin: Plugin + + constructor(mcCoroutine: MCCoroutine, plugin: Plugin) { + this.mcCoroutine = mcCoroutine + this.plugin = plugin + } + + /** + * Gets called when the plugin is disabled. + */ + @EventHandler + fun onPluginDisable(pluginEvent: PluginDisableEvent) { + if (pluginEvent.plugin != plugin) { + return + } + + val configuration = mcCoroutine.getCoroutineSession(plugin).mcCoroutineConfiguration + + if (configuration.shutdownStrategy == ShutdownStrategy.SCHEDULER) { + mcCoroutine.disable(pluginEvent.plugin) + } + } +} diff --git a/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/service/CommandServiceImpl.kt b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/service/CommandServiceImpl.kt new file mode 100644 index 00000000..ead650c9 --- /dev/null +++ b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/service/CommandServiceImpl.kt @@ -0,0 +1,60 @@ +package com.github.shynixn.mccoroutine.folia.service + +import com.github.shynixn.mccoroutine.folia.* +import kotlinx.coroutines.CoroutineStart +import org.bukkit.command.PluginCommand +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin +import kotlin.coroutines.CoroutineContext + +internal class CommandServiceImpl(private val plugin: Plugin) { + /** + * Registers a suspend command executor. + */ + fun registerSuspendCommandExecutor( + context: CoroutineContext, + pluginCommand: PluginCommand, + commandExecutor: SuspendingCommandExecutor + ) { + pluginCommand.setExecutor { p0, p1, p2, p3 -> + // If the result is delayed we can automatically assume it is true. + var success = true + val scheduleContext = if (context == plugin.globalRegionDispatcher && p0 is Player) { + plugin.entityDispatcher(p0) + } else { + context + } + // Commands in spigot always arrive synchronously. Therefore, we can simply use the default properties. + plugin.launch(scheduleContext, CoroutineStart.UNDISPATCHED) { + success = commandExecutor.onCommand(p0, p1, p2, p3) + } + + success + } + } + + /** + * Registers a suspend tab completer. + */ + fun registerSuspendTabCompleter( + context: CoroutineContext, + pluginCommand: PluginCommand, + tabCompleter: SuspendingTabCompleter + ) { + pluginCommand.setTabCompleter { sender, command, alias, args -> + var result: List? = null + val scheduleContext = if (context == plugin.globalRegionDispatcher && sender is Player) { + plugin.entityDispatcher(sender) + } else { + context + } + + // Tab Completes in spigot always arrive synchronously. Therefore, we can simply use the default properties. + plugin.launch(scheduleContext, CoroutineStart.UNDISPATCHED) { + result = tabCompleter.onTabComplete(sender, command, alias, args) + } + + result + } + } +} diff --git a/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/service/EventServiceImpl.kt b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/service/EventServiceImpl.kt new file mode 100644 index 00000000..87dd79a6 --- /dev/null +++ b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/service/EventServiceImpl.kt @@ -0,0 +1,285 @@ +package com.github.shynixn.mccoroutine.folia.service + +import com.github.shynixn.mccoroutine.folia.CoroutineSession +import com.github.shynixn.mccoroutine.folia.asyncDispatcher +import com.github.shynixn.mccoroutine.folia.extension.invokeSuspend +import com.github.shynixn.mccoroutine.folia.globalRegionDispatcher +import com.github.shynixn.mccoroutine.folia.launch +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import org.bukkit.Warning +import org.bukkit.event.* +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.plugin.* +import java.lang.Deprecated +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import java.util.logging.Level +import kotlin.Boolean +import kotlin.IllegalArgumentException +import kotlin.String +import kotlin.Throwable +import kotlin.check +import kotlin.coroutines.CoroutineContext + +internal class EventServiceImpl(private val plugin: Plugin, private val coroutineSession: CoroutineSession) { + /** + * Registers a suspend listener. + */ + fun registerSuspendListener( + listener: Listener, + eventDispatcher: Map, (event: Event) -> CoroutineContext> + ) { + val registeredListeners = createCoroutineListener(listener, plugin, eventDispatcher) + + val method = SimplePluginManager::class.java + .getDeclaredMethod("getEventListeners", Class::class.java) + method.isAccessible = true + + for (entry in registeredListeners.entries) { + val clazz = entry.key + val handlerList = method.invoke(plugin.server.pluginManager, clazz) as HandlerList + handlerList.registerAll(entry.value as MutableCollection) + } + } + + /** + * Fires a suspending [event] with the given [eventExecutionType]. + * @return Collection of receiver jobs. May already be completed. + */ + fun fireSuspendingEvent( + event: Event, + eventExecutionType: com.github.shynixn.mccoroutine.folia.EventExecutionType + ): Collection { + if (event.isAsynchronous) { + check(!Thread.holdsLock(this)) { event.eventName + " cannot be triggered asynchronously from inside synchronized code." } + check(!plugin.server.isPrimaryThread) { event.eventName + " cannot be triggered asynchronously from primary server thread." } + } else { + check(plugin.server.isPrimaryThread) { event.eventName + " cannot be triggered asynchronously from another thread." } + } + + val listeners = event.handlers.registeredListeners + val jobs = ArrayList() + + if (eventExecutionType == com.github.shynixn.mccoroutine.folia.EventExecutionType.Concurrent) { + for (registration in listeners) { + if (!registration.plugin.isEnabled) { + continue + } + + try { + if (registration is SuspendingRegisteredListener) { + val job = registration.callSuspendingEvent(event) + jobs.add(job) + } else { + registration.callEvent(event) + } + } catch (e: Throwable) { + plugin.logger.log( + Level.SEVERE, + "Could not pass event " + event.eventName + " to " + registration.plugin.description.fullName, e + ) + } + } + } else if (eventExecutionType == com.github.shynixn.mccoroutine.folia.EventExecutionType.Consecutive) { + jobs.add(plugin.launch(Dispatchers.Unconfined, CoroutineStart.UNDISPATCHED) { + for (registration in listeners) { + if (!registration.plugin.isEnabled) { + continue + } + try { + if (registration is SuspendingRegisteredListener) { + registration.callSuspendingEvent(event).join() + } else { + registration.callEvent(event) + } + } catch (e: Throwable) { + plugin.logger.log( + Level.SEVERE, + "Could not pass event " + event.eventName + " to " + registration.plugin.description.fullName, + e + ) + } + } + }) + } + + return jobs + } + + + /** + * Creates a listener according to the spigot implementation. + */ + private fun createCoroutineListener( + listener: Listener, + plugin: Plugin, + eventDispatcher: Map, (event: Event) -> CoroutineContext> + ): Map, MutableSet> { + val eventMethods = HashSet() + + try { + // Adds public methods of the current class and inherited classes + eventMethods.addAll(listener.javaClass.methods) + // Adds all methods of the current class + eventMethods.addAll(listener.javaClass.declaredMethods) + } catch (e: NoClassDefFoundError) { + plugin.logger.severe("Plugin " + plugin.description.fullName + " has failed to register events for " + listener.javaClass + " because " + e.message + " does not exist.") + return emptyMap() + } + + val result = mutableMapOf, MutableSet>() + + for (method in eventMethods) { + val annotation = method.getAnnotation(EventHandler::class.java) + + if (annotation == null || method.isBridge || method.isSynthetic) { + continue + } + + val eventClass = method.parameterTypes[0].asSubclass(Event::class.java) + method.isAccessible = true + + if (!result.containsKey(eventClass)) { + result[eventClass] = HashSet() + } + + var clazz: Class<*> = eventClass + + while (Event::class.java.isAssignableFrom(clazz)) { + if (clazz.getAnnotation(Deprecated::class.java) == null) { + clazz = clazz.superclass + continue + } + + val warning = clazz.getAnnotation(Warning::class.java) + val warningState = plugin.server.warningState + + if (!warningState.printFor(warning)) { + break + } + + plugin.logger.log( + Level.WARNING, + """"%s" has registered a listener for %s on method "%s", but the event is Deprecated. "%s"; please notify the authors %s.""".format( + plugin.description.fullName, + clazz.name, + method.toGenericString(), + if (warning?.reason?.isNotEmpty() == true) warning.reason else "Server performance will be affected", + plugin.description.authors.toTypedArray().contentToString() + ), + if (warningState == Warning.WarningState.ON) { + AuthorNagException(null as String?) + } else null + ) + } + + if (!eventDispatcher.containsKey(eventClass)) { + throw IllegalArgumentException("A event dispatcher for class '" + eventClass.name + "' needs to be added to registerSuspendingEvents.") + } + + val contextResolver = eventDispatcher[eventClass]!! + + val executor = SuspendingEventExecutor(eventClass, method, plugin, coroutineSession, contextResolver) + result[eventClass]!!.add( + SuspendingRegisteredListener( + listener, + executor, + annotation.priority, + plugin, + annotation.ignoreCancelled + ) + ) + } + + return result + } + + class SuspendingEventExecutor( + private val eventClass: Class<*>, + private val method: Method, + private val plugin: Plugin, + private val coroutineSession: CoroutineSession, + private val contextResolver: (event: Event) -> CoroutineContext + ) : EventExecutor { + var isSuspendMethod: Boolean? = null + fun executeSuspend(listener: Listener, event: Event): Job { + return executeEvent(listener, event) + } + + override fun execute(listener: Listener, event: Event) { + executeEvent(listener, event) + } + + private fun executeEvent(listener: Listener, event: Event): Job { + try { + if (eventClass.isAssignableFrom(event.javaClass)) { + val isAsync = event.isAsynchronous + + val dispatcher = if (isAsync) { + if (coroutineSession.isFoliaLoaded) { + plugin.globalRegionDispatcher // There are no async events in folia. + } else { + plugin.asyncDispatcher + } + } else { + if (coroutineSession.isFoliaLoaded) { + contextResolver.invoke(event) + } else { + plugin.globalRegionDispatcher // Load minecraft dispatcher + } + } + + // We want to start it on the same thread as the calling thread -> unDispatched. + // However, after a possible suspension we either end up on the asyncDispatcher or minecraft Dispatcher. + return plugin.launch(dispatcher, CoroutineStart.UNDISPATCHED) { + if (isSuspendMethod == null) { + try { + // Try as suspension function. + method.invokeSuspend(listener, event) + isSuspendMethod = true + } catch (e: IllegalArgumentException) { + // Try as ordinary function. + method.invoke(listener, event) + isSuspendMethod = false + } + } else if (isSuspendMethod!!) { + method.invokeSuspend(listener, event) + } else { + method.invoke(listener, event) + } + } + } + } catch (var4: InvocationTargetException) { + throw EventException(var4.cause) + } catch (var5: Throwable) { + throw EventException(var5) + } + return Job() + } + } + + class SuspendingRegisteredListener( + lister: Listener, + private val executorImpl: EventExecutor, + priority: EventPriority, + plugin: Plugin, + ignoreCancelled: Boolean + ) : RegisteredListener(lister, executorImpl, priority, plugin, ignoreCancelled) { + fun callSuspendingEvent(event: Event): Job { + if (event is Cancellable) { + if ((event as Cancellable).isCancelled && isIgnoringCancelled) { + return Job() + } + } + + return if (executorImpl is SuspendingEventExecutor) { + executorImpl.executeSuspend(listener, event) + } else { + executorImpl.execute(listener, event) + Job() + } + } + } +} diff --git a/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/service/WakeUpBlockServiceImpl.kt b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/service/WakeUpBlockServiceImpl.kt new file mode 100644 index 00000000..bd30d9b7 --- /dev/null +++ b/mccoroutine-folia-core/src/main/java/com/github/shynixn/mccoroutine/folia/service/WakeUpBlockServiceImpl.kt @@ -0,0 +1,79 @@ +package com.github.shynixn.mccoroutine.folia.service + +import com.github.shynixn.mccoroutine.folia.extension.findClazz +import org.bukkit.plugin.Plugin +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.locks.LockSupport + +/** + * This implementation is only active during plugin startup. Does not affect the server when running. + */ +internal class WakeUpBlockServiceImpl(private val plugin: Plugin) { + private var threadSupport: ExecutorService? = null + private val craftSchedulerClazz by lazy { + plugin.findClazz("org.bukkit.craftbukkit.VERSION.scheduler.CraftScheduler") + } + private val craftSchedulerTickField by lazy { + val field = craftSchedulerClazz.getDeclaredField("currentTick") + field.isAccessible = true + field + } + private val craftSchedulerHeartBeatMethod by lazy { + craftSchedulerClazz.getDeclaredMethod("mainThreadHeartbeat", Int::class.java) + } + + /** + * Enables or disables the server heartbeat hack. + */ + var isManipulatedServerHeartBeatEnabled: Boolean = false + + /** + * Reference to the primary server thread. + */ + var primaryThread: Thread? = null + + /** + * Calls scheduler management implementations to ensure the + * is not sleeping if a run is scheduled by blocking. + */ + fun ensureWakeup() { + if (!isManipulatedServerHeartBeatEnabled) { + if (threadSupport != null) { + threadSupport!!.shutdown() + threadSupport = null + } + + // In all cases except startup, the call immediately returns here. + return + } + + if (primaryThread == null && plugin.server.isPrimaryThread) { + primaryThread = Thread.currentThread() + } + + if (primaryThread == null) { + return + } + + if (threadSupport == null) { + threadSupport = Executors.newFixedThreadPool(1) + } + + threadSupport!!.submit { + val blockingCoroutine = LockSupport.getBlocker(primaryThread) + + if (blockingCoroutine != null) { + val currentTick = craftSchedulerTickField.get(plugin.server.scheduler) + craftSchedulerHeartBeatMethod.invoke(plugin.server.scheduler, currentTick) + } + } + } + + /** + * Disposes the service. + */ + fun dispose() { + threadSupport?.shutdown() + } +} diff --git a/mccoroutine-folia-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/mccoroutine-folia-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..ca6ee9ce --- /dev/null +++ b/mccoroutine-folia-core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/mccoroutine-folia-sample/build.gradle.kts b/mccoroutine-folia-sample/build.gradle.kts new file mode 100644 index 00000000..04844f98 --- /dev/null +++ b/mccoroutine-folia-sample/build.gradle.kts @@ -0,0 +1,45 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + id("com.github.johnrengelman.shadow") version ("2.0.4") +} + +publishing { + publications { + (findByName("mavenJava") as MavenPublication).artifact(tasks.findByName("shadowJar")!!) + } +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +tasks.withType { + dependsOn("jar") + classifier = "shadowJar" + archiveName = "$baseName-$version.$extension" + + // Change the output folder of the plugin. + // destinationDir = File("C:\\temp\\plugins\\") +} + +repositories { + maven { + url = uri("https://papermc.io/repo/repository/maven-public/") + } +} + +dependencies { + implementation(project(":mccoroutine-folia-api")) + implementation(project(":mccoroutine-folia-core")) + + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.9") + + compileOnly("dev.folia:folia-api:1.20.1-R0.1-20230615.235213-1") + + testImplementation("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT") +} diff --git a/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/MCCoroutineSamplePlugin.kt b/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/MCCoroutineSamplePlugin.kt new file mode 100644 index 00000000..481a25e8 --- /dev/null +++ b/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/MCCoroutineSamplePlugin.kt @@ -0,0 +1,92 @@ +package com.github.shynixn.mccoroutine.folia.sample + +import com.github.shynixn.mccoroutine.folia.* +import com.github.shynixn.mccoroutine.folia.sample.commandexecutor.AdminCommandExecutor +import com.github.shynixn.mccoroutine.folia.sample.impl.FakeDatabase +import com.github.shynixn.mccoroutine.folia.sample.impl.UserDataCache +import com.github.shynixn.mccoroutine.folia.sample.listener.EntityInteractListener +import com.github.shynixn.mccoroutine.folia.sample.listener.PlayerConnectListener +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.bukkit.Bukkit +import org.bukkit.event.Event +import org.bukkit.event.entity.EntitySpawnEvent +import org.bukkit.event.player.PlayerInteractAtEntityEvent +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerQuitEvent +import kotlin.coroutines.CoroutineContext + +class MCCoroutineSamplePlugin : SuspendingJavaPlugin() { + /** + * Called when this plugin is enabled + */ + override suspend fun onEnableAsync() { + println("[MCCoroutineSamplePlugin/onEnableAsync] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + + withContext(Dispatchers.IO) { + println("[MCCoroutineSamplePlugin/onEnableAsync] Simulating data load Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + Thread.sleep(500) + } + + val plugin = this + val database = FakeDatabase() + val cache = UserDataCache(this, database) + val eventDispatcher = mapOf, (event: Event) -> CoroutineContext>( + Pair(PlayerJoinEvent::class.java) { + require(it is PlayerJoinEvent) + plugin.entityDispatcher(it.player) + }, + Pair(PlayerQuitEvent::class.java) { + require(it is PlayerQuitEvent) + plugin.entityDispatcher(it.player) + }, + Pair(MCCoroutineExceptionEvent::class.java) { + require(it is MCCoroutineExceptionEvent) + plugin.globalRegionDispatcher + }, + Pair(EntitySpawnEvent::class.java) { + require(it is EntitySpawnEvent) + plugin.entityDispatcher(it.entity) + }, + Pair(PlayerInteractAtEntityEvent::class.java) { + require(it is PlayerInteractAtEntityEvent) + plugin.entityDispatcher(it.player) + }, + ) + // Extension to traditional registration. + server.pluginManager.registerSuspendingEvents( + PlayerConnectListener(this, cache), + this, + eventDispatcher + ) + server.pluginManager.registerSuspendingEvents( + EntityInteractListener( + cache + ), this, + eventDispatcher + ); + + val commandExecutor = AdminCommandExecutor(cache, this) + this.getCommand("mccor")!!.setSuspendingExecutor(commandExecutor) + this.getCommand("mccor")!!.setSuspendingTabCompleter(commandExecutor) + + println("[MCCoroutineSamplePlugin/onEnableAsync] Is ending on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + println("[MCCoroutineSamplePlugin/onEnableAsync] Is using Folia Schedulers: " + this.mcCoroutineConfiguration.isFoliaLoaded) + } + + + /** + * Called when this plugin is disabled. + */ + override suspend fun onDisableAsync() { + println("[MCCoroutineSamplePlugin/onDisableAsync] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + + // Do not use asyncDispatcher as it is already disposed at this point. + withContext(Dispatchers.IO) { + println("[MCCoroutineSamplePlugin/onDisableAsync] Simulating data save on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + Thread.sleep(500) + } + + println("[MCCoroutineSamplePlugin/onDisableAsync] Is ending on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + } +} diff --git a/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/commandexecutor/AdminCommandExecutor.kt b/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/commandexecutor/AdminCommandExecutor.kt new file mode 100644 index 00000000..a5a2891d --- /dev/null +++ b/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/commandexecutor/AdminCommandExecutor.kt @@ -0,0 +1,83 @@ +package com.github.shynixn.mccoroutine.folia.sample.commandexecutor + +import com.github.shynixn.mccoroutine.folia.SuspendingCommandExecutor +import com.github.shynixn.mccoroutine.folia.SuspendingTabCompleter +import com.github.shynixn.mccoroutine.folia.callSuspendingEvent +import com.github.shynixn.mccoroutine.folia.sample.impl.UserDataCache +import kotlinx.coroutines.joinAll +import org.bukkit.Bukkit +import org.bukkit.command.Command +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player +import org.bukkit.event.player.PlayerQuitEvent +import org.bukkit.plugin.Plugin + +class AdminCommandExecutor(private val userDataCache: UserDataCache, private val plugin: Plugin) : + SuspendingCommandExecutor, + SuspendingTabCompleter { + /** + * Executes the given command, returning its success. + * If false is returned, then the "usage" plugin.yml entry for this command (if defined) will be sent to the player. + */ + override suspend fun onCommand( + sender: CommandSender, + command: Command, + label: String, + args: Array + ): Boolean { + if (args.size == 3 && args[0].equals("set", true) && args[2].toIntOrNull() != null) { + val playerName = args[1] + val playerKills = args[2].toInt() + val otherPlayer = Bukkit.getPlayer(playerName)!! + + println("[AdmingCommandExecutor/onCommand] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + val userData = userDataCache.getUserDataFromPlayerAsync(otherPlayer).await() + userData.amountOfPlayerKills = playerKills + userDataCache.saveUserData(otherPlayer) + println("[AdmingCommandExecutor/onCommand] Is ending on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + return true + } + + if (args.size == 1 && args[0].equals("leave", true) && sender is Player) { + println("[AdmingCommandExecutor/onCommand] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + val event = PlayerQuitEvent(sender, null as String?) + Bukkit.getPluginManager().callSuspendingEvent(event, plugin).joinAll() + println("[AdmingCommandExecutor/onCommand] Is ending on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + } + + if (args.size == 1 && args[0].equals("exception", true) && sender is Player) { + throw RuntimeException("Some Exception!") + } + + if (args.isEmpty()) { + sender.sendMessage("/mccor set ") + sender.sendMessage("/mccor leave") + sender.sendMessage("/mccor exception") + return true + } + + return false + } + + /** + * Requests a list of possible completions for a command argument. + * If the call is suspended during the execution, the returned list will not be shown. + * @param sender - Source of the command. + * @param command - Command which was executed. + * @param alias - Alias of the command which was used. + * @param args - Passed command arguments. + * @return A list of possible completions for the final argument, or an empty list. + */ + override suspend fun onTabComplete( + sender: CommandSender, + command: Command, + alias: String, + args: Array + ): List { + if (args.size == 1) { + return arrayListOf("set") + } + + return emptyList() + } +} diff --git a/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/entity/UserData.kt b/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/entity/UserData.kt new file mode 100644 index 00000000..cf16f6fd --- /dev/null +++ b/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/entity/UserData.kt @@ -0,0 +1,6 @@ +package com.github.shynixn.mccoroutine.folia.sample.entity + +class UserData { + var amountOfPlayerKills: Int = 0 + var amountOfEntityKills: Int = 0 +} diff --git a/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/impl/FakeDatabase.kt b/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/impl/FakeDatabase.kt new file mode 100644 index 00000000..33d5dd26 --- /dev/null +++ b/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/impl/FakeDatabase.kt @@ -0,0 +1,24 @@ +package com.github.shynixn.mccoroutine.folia.sample.impl + +import com.github.shynixn.mccoroutine.folia.sample.entity.UserData +import org.bukkit.entity.Player + +class FakeDatabase { + /** + * Simulates a getUserData call to a real database by delaying the result. + */ + fun getUserDataFromPlayer(player: Player): UserData { + Thread.sleep(5000) + val userData = UserData() + userData.amountOfEntityKills = 20 + userData.amountOfPlayerKills = 30 + return userData + } + + /** + * Simulates a save User data call. + */ + fun saveUserData(userData: UserData) { + Thread.sleep(6000) + } +} diff --git a/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/impl/UserDataCache.kt b/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/impl/UserDataCache.kt new file mode 100644 index 00000000..873a2fcd --- /dev/null +++ b/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/impl/UserDataCache.kt @@ -0,0 +1,67 @@ +package com.github.shynixn.mccoroutine.folia.sample.impl + +import com.github.shynixn.mccoroutine.folia.asyncDispatcher +import com.github.shynixn.mccoroutine.folia.sample.entity.UserData +import com.github.shynixn.mccoroutine.folia.scope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.future.future +import kotlinx.coroutines.withContext +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin +import java.util.concurrent.CompletionStage + +class UserDataCache(private val plugin: Plugin, private val fakeDatabase: FakeDatabase) { + private val cache = HashMap>() + + /** + * Clears the player cache. + */ + fun clearCache(player: Player) { + cache.remove(player) + } + + /** + * Saves the cached data of the player. + */ + suspend fun saveUserData(player: Player) { + val userData = cache[player]!!.await() + withContext(plugin.asyncDispatcher) { + fakeDatabase.saveUserData(userData) + } + } + + /** + * Gets the user data from the player. + */ + suspend fun getUserDataFromPlayerAsync(player: Player): Deferred { + return coroutineScope { + println("[UserDataCache/getUserDataFromPlayerAsync] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + if (!cache.containsKey(player)) { + cache[player] = async(plugin.asyncDispatcher) { + println("[UserDataCache/getUserDataFromPlayerAsync] Is downloading data on Thread:${Thread.currentThread().name}/${Thread.currentThread().id})}") + fakeDatabase.getUserDataFromPlayer(player) + } + } + + cache[player]!! + } + } + + /** + * Gets the user data from the player. + * + * This method is only useful if you plan to access suspend functions from Java. It + * is not possible to call suspend functions directly from java, so we need to + * wrap it into a Java 8 CompletionStage. + * + * This might be useful if you plan to provide a Developer Api for your plugin as other + * plugins may be written in Java or if you have got Java code in your plugin. + */ + fun getUserDataFromPlayer(player: Player): CompletionStage { + return plugin.scope.future { + getUserDataFromPlayerAsync(player).await() + } + } +} diff --git a/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/listener/EntityInteractListener.java b/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/listener/EntityInteractListener.java new file mode 100644 index 00000000..ca4812eb --- /dev/null +++ b/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/listener/EntityInteractListener.java @@ -0,0 +1,31 @@ +package com.github.shynixn.mccoroutine.folia.sample.listener; + +import com.github.shynixn.mccoroutine.folia.sample.entity.UserData; +import com.github.shynixn.mccoroutine.folia.sample.impl.UserDataCache; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerInteractAtEntityEvent; + +import java.util.concurrent.CompletionStage; + +/** + * This is a Java example how to interact with suspend functions from Java. + */ +public class EntityInteractListener implements Listener { + private final UserDataCache userDataCache; + + public EntityInteractListener(UserDataCache userDataCache) { + this.userDataCache = userDataCache; + } + + @EventHandler + public void onPlayerInteractEvent(PlayerInteractAtEntityEvent event) { + CompletionStage future = this.userDataCache.getUserDataFromPlayer(event.getPlayer()); + future.thenAccept(useData -> { + System.out.println("Result: " + useData); + }).exceptionally(throwable -> { + throwable.printStackTrace(); + return null; + }); + } +} diff --git a/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/listener/PlayerConnectListener.kt b/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/listener/PlayerConnectListener.kt new file mode 100644 index 00000000..8538b8b0 --- /dev/null +++ b/mccoroutine-folia-sample/src/main/java/com/github/shynixn/mccoroutine/folia/sample/listener/PlayerConnectListener.kt @@ -0,0 +1,86 @@ +package com.github.shynixn.mccoroutine.folia.sample.listener + +import com.github.shynixn.mccoroutine.folia.* +import com.github.shynixn.mccoroutine.folia.sample.impl.UserDataCache +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import org.bukkit.Material +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.event.entity.EntitySpawnEvent +import org.bukkit.event.player.PlayerJoinEvent +import org.bukkit.event.player.PlayerQuitEvent +import org.bukkit.inventory.ItemStack +import org.bukkit.plugin.Plugin + +class PlayerConnectListener(private val plugin: Plugin, private val userDataCache: UserDataCache) : Listener { + /** + * Gets called on player join event. + */ + @EventHandler + suspend fun onPlayerJoinEvent(playerJoinEvent: PlayerJoinEvent) { + println("[PlayerConnectListener/onPlayerJoinEvent] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + val userData = userDataCache.getUserDataFromPlayerAsync(playerJoinEvent.player).await() + println("[PlayerConnectListener/onPlayerJoinEvent] Is ending on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + } + + /** + * Gets called on player quit event. + */ + @EventHandler + suspend fun onPlayerQuitEvent(playerQuitEvent: PlayerQuitEvent) { + println("[PlayerConnectListener/onPlayerQuitEvent] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + + val apple = withContext(plugin.asyncDispatcher) { + println("[PlayerConnectListener/onPlayerQuitEvent] Simulate data save on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + Thread.sleep(500) + ItemStack(Material.APPLE) + } + + userDataCache.clearCache(playerQuitEvent.player) + println("[PlayerConnectListener/onPlayerQuitEvent] Is ending on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + } + + @EventHandler + fun onEntitySpawnEvent(event: EntitySpawnEvent) { + println("[PlayerConnectListener/onEntitySpawnEvent] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + plugin.launch(plugin.entityDispatcher(event.entity), CoroutineStart.UNDISPATCHED) { + println("[PlayerConnectListener/onEntitySpawnEvent] Entering coroutine on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + delay(2000) + + val entityLocation = withContext(plugin.entityDispatcher(event.entity)) { + println("[PlayerConnectListener/onEntitySpawnEvent] Entity Dispatcher on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + event.entity.customName = "Coroutine Entity" + event.entity.location + } + + entityLocation.add(2.0, 0.0, 0.0) + delay(1000) + + withContext(plugin.regionDispatcher(entityLocation)) { + println("[PlayerConnectListener/onEntitySpawnEvent] Region Dispatcher on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + entityLocation.block.type = Material.GOLD_BLOCK + } + + delay(1000) + + withContext(plugin.entityDispatcher(event.entity)) { + println("[PlayerConnectListener/onEntitySpawnEvent] Entity Dispatcher on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + event.entity.teleportAsync(entityLocation) + } + } + } + + @EventHandler + fun onCoroutineException(event: MCCoroutineExceptionEvent) { + if (event.plugin != plugin) { + // Other plugin, we do not care. + return + } + + // Print Exception + event.exception.printStackTrace() + event.isCancelled = true + } +} diff --git a/mccoroutine-folia-sample/src/main/resources/plugin.yml b/mccoroutine-folia-sample/src/main/resources/plugin.yml new file mode 100644 index 00000000..d791b933 --- /dev/null +++ b/mccoroutine-folia-sample/src/main/resources/plugin.yml @@ -0,0 +1,11 @@ +name: MCCoroutine-Sample +version: 2.13.0 +author: Shynixn +main: com.github.shynixn.mccoroutine.folia.sample.MCCoroutineSamplePlugin +folia-supported: true +commands: + mccor: + description: Command for operations. + useage: / + permission: mccoroutine.sample + permission-message: You don't have permission. diff --git a/mccoroutine-minestom-api/build.gradle.kts b/mccoroutine-minestom-api/build.gradle.kts index fe5ca9bf..6379e48f 100644 --- a/mccoroutine-minestom-api/build.gradle.kts +++ b/mccoroutine-minestom-api/build.gradle.kts @@ -12,6 +12,6 @@ java { dependencies { compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") - compileOnly("com.github.Minestom:Minestom:1.19.3-SNAPSHOT") - testImplementation("com.github.Minestom:Minestom:1.19.3-SNAPSHOT") + compileOnly("com.github.Minestom:Minestom:c5047b8037") + testImplementation("com.github.Minestom:Minestom:c5047b8037") } diff --git a/mccoroutine-minestom-core/build.gradle.kts b/mccoroutine-minestom-core/build.gradle.kts index dc1f14f8..4fde3a3e 100644 --- a/mccoroutine-minestom-core/build.gradle.kts +++ b/mccoroutine-minestom-core/build.gradle.kts @@ -15,7 +15,7 @@ dependencies { compileOnly("net.kyori:adventure-text-logger-slf4j:4.12.0") compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") - compileOnly("com.github.Minestom:Minestom:1.19.3-SNAPSHOT") - testImplementation("com.github.Minestom:Minestom:1.19.3-SNAPSHOT") + compileOnly("com.github.Minestom:Minestom:c5047b8037") + testImplementation("com.github.Minestom:Minestom:c5047b8037") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") } diff --git a/mccoroutine-minestom-core/src/test/java/integrationtest/MinestomCommandTest.kt b/mccoroutine-minestom-core/src/test/java/integrationtest/MinestomCommandTest.kt index ce6630b2..9385fec8 100644 --- a/mccoroutine-minestom-core/src/test/java/integrationtest/MinestomCommandTest.kt +++ b/mccoroutine-minestom-core/src/test/java/integrationtest/MinestomCommandTest.kt @@ -38,9 +38,6 @@ class MinestomCommandTest { Assertions.assertNotEquals(unitTestThreadId, testCommandExecutor.callThreadId) Assertions.assertNotEquals(unitTestThreadId, testCommandExecutor.asyncThreadId) Assertions.assertNotEquals(unitTestThreadId, testCommandExecutor.leaveThreadId) - Assertions.assertEquals(testCommandExecutor.callThreadId, testCommandExecutor.leaveThreadId) - Assertions.assertNotEquals(testCommandExecutor.asyncThreadId, testCommandExecutor.leaveThreadId) - Assertions.assertNotEquals(testCommandExecutor.callThreadId, testCommandExecutor.asyncThreadId) } private class TestCommandExecutor(private val extension: Extension) : Command("unittest") { diff --git a/mccoroutine-minestom-sample/build.gradle.kts b/mccoroutine-minestom-sample/build.gradle.kts index 99805069..581813e4 100644 --- a/mccoroutine-minestom-sample/build.gradle.kts +++ b/mccoroutine-minestom-sample/build.gradle.kts @@ -24,7 +24,7 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.9") - implementation("com.github.Minestom:Minestom:1.19.3-SNAPSHOT") + implementation("com.github.Minestom:Minestom:c5047b8037") } tasks.withType { diff --git a/mccoroutine-minestom-sample/src/main/resources/extension.json b/mccoroutine-minestom-sample/src/main/resources/extension.json index 5286e9a3..80af5822 100644 --- a/mccoroutine-minestom-sample/src/main/resources/extension.json +++ b/mccoroutine-minestom-sample/src/main/resources/extension.json @@ -1,5 +1,5 @@ { "entrypoint": "com.github.shynixn.mccoroutine.minestom.sample.extension.MCCoroutineSampleExtension", "name": "MCCoroutineSampleExtension", - "version": "2.12.1" + "version": "2.13.0" } diff --git a/mccoroutine-sponge-sample/src/main/resources/mcmod.info b/mccoroutine-sponge-sample/src/main/resources/mcmod.info index 68d5a380..54c27115 100644 --- a/mccoroutine-sponge-sample/src/main/resources/mcmod.info +++ b/mccoroutine-sponge-sample/src/main/resources/mcmod.info @@ -1,7 +1,7 @@ [{ "modid": "mccoroutinesample", "name": "MCCoroutineSample", - "version": "2.12.1", + "version": "2.13.0", "description": "MCCoroutineSample is sample plugin to use MCCoroutine in Sponge.", "authorList": [ "Shynixn" diff --git a/settings.gradle.kts b/settings.gradle.kts index 6beea93d..0dfb1de5 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,6 +12,10 @@ include("mccoroutine-bungeecord-sample") include("mccoroutine-fabric-api") include("mccoroutine-fabric-core") +include("mccoroutine-folia-api") +include("mccoroutine-folia-core") +include("mccoroutine-folia-sample") + include("mccoroutine-minestom-api") include("mccoroutine-minestom-core") include("mccoroutine-minestom-sample")