diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9c89fee7..bf7e5c45 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -181,17 +181,27 @@ jobs: asset_name: MCCoroutine-Velocity-Core.jar asset_content_type: application/jar - - name: Upload Velocity Sample Plugin to Github + - name: Upload Minestom Api to Github if: "contains(github.event.head_commit.message, '--release') && contains(github.ref, 'master')" uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: /home/runner/work/MCCoroutine/MCCoroutine/mccoroutine-velocity-sample/build/libs/mccoroutine-velocity-sample-${{ env.RELEASE_VERSION }}.jar - asset_name: MCCoroutine-Velocity-Plugin-Sample.jar + asset_path: /home/runner/work/MCCoroutine/MCCoroutine/mccoroutine-minestom-api/build/libs/mccoroutine-minestom-api-${{ env.RELEASE_VERSION }}.jar + asset_name: MCCoroutine-Minestom-Api.jar asset_content_type: application/jar + - name: Upload Minestom Core to Github + if: "contains(github.event.head_commit.message, '--release') && contains(github.ref, 'master')" + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: /home/runner/work/MCCoroutine/MCCoroutine/mccoroutine-minestom-core/build/libs/mccoroutine-minestom-core-${{ env.RELEASE_VERSION }}.jar + asset_name: MCCoroutine-Minestom-Core.jar + asset_content_type: application/jar Documentation: runs-on: ubuntu-latest @@ -217,6 +227,7 @@ jobs: ./gradlew generateSpongeJavaDocPages > /dev/null ./gradlew generateBungeeCordJavaDocPages > /dev/null ./gradlew generateVelocityJavaDocPages > /dev/null + ./gradlew generateMinestomJavaDocPages > /dev/null sudo apt-get install -y mkdocs pip install mkdocs-material pip install Pygments diff --git a/.gitignore b/.gitignore index 9ddd6944..ef452308 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ local.properties *.log *.class .gradle -build \ No newline at end of file +build +extensions/* diff --git a/README.md b/README.md index 039dafce..bca18d4d 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ the existing APIs with suspendable commands, events and schedules. * CraftBukkit * SpongeVanilla * SpongeForge +* Minestom **Supported Proxies:** @@ -66,6 +67,7 @@ private suspend fun bob() { * [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-API](https://shynixn.github.io/MCCoroutine/apidocs/mccoroutine-root/com.github.shynixn.mccoroutine.sponge/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 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 48262770..72cc0f41 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { mavenCentral() } dependencies { - classpath group: 'org.jetbrains.kotlin', name: 'kotlin-gradle-plugin', version: '1.3.72' + classpath group: 'org.jetbrains.kotlin', name: 'kotlin-gradle-plugin', version: '1.7.10' classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.5.31" } } @@ -43,7 +43,7 @@ tasks.register("printVersion") { subprojects { group 'com.github.shynixn.mccoroutine' - version '2.9.0' + version '2.10.0' sourceCompatibility = 1.8 @@ -144,12 +144,9 @@ subprojects { } dependencies { - testCompile 'org.jetbrains.kotlin:kotlin-test' - testCompile 'org.jetbrains.kotlin:kotlin-test-junit' - testCompile 'org.junit.jupiter:junit-jupiter-api:5.3.1' - testCompile 'org.mockito:mockito-core:2.23.0' - - testRuntime 'org.junit.jupiter:junit-jupiter-engine:5.3.1' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1' + testImplementation 'org.mockito:mockito-core:2.23.0' + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.3.1' } } @@ -196,3 +193,12 @@ task generateVelocityJavaDocPages(type: org.jetbrains.dokka.gradle.DokkaTask) { } } } + +task generateMinestomJavaDocPages(type: org.jetbrains.dokka.gradle.DokkaTask) { + dokkaSourceSets { + named("main") { + outputDirectory = file("docs/apidocs") + sourceRoots.from(file("mccoroutine-minestom-api/src/main/java")) + } + } +} diff --git a/docs/wiki/docs/README.md b/docs/wiki/docs/README.md index 2b76fc83..4780caf9 100644 --- a/docs/wiki/docs/README.md +++ b/docs/wiki/docs/README.md @@ -22,6 +22,7 @@ the existing APIs with suspendable commands, events and schedules. * CraftBukkit * SpongeVanilla * SpongeForge +* Minestom **Supported Proxies:** diff --git a/docs/wiki/docs/commandexecutor.md b/docs/wiki/docs/commandexecutor.md index f1dc5f9f..20b1a924 100644 --- a/docs/wiki/docs/commandexecutor.md +++ b/docs/wiki/docs/commandexecutor.md @@ -1,6 +1,7 @@ # Suspending Commandexecutors -This page explains how you can use Kotlin Coroutines using the suspend key word for command executors in minecraft plugins. +This page explains how you can use Kotlin Coroutines using the suspend key word for command executors in minecraft +plugins. ## Create the CommandExecutor @@ -126,6 +127,34 @@ This page explains how you can use Kotlin Coroutines using the suspend key word 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) + } + } + }) + } + } + ```` + ## Register the CommandExecutor === "Bukkit" @@ -284,6 +313,11 @@ This page explains how you can use Kotlin Coroutines using the suspend key word } ```` +=== "Minestom" + + Register the command in the same way as a traditional command. + ## Test the CommandExecutor -Join your server and use the playerData command to observe ``getDataFromPlayer`` and ``saveData`` messages getting printed to your server log. +Join your server and use the playerData command to observe ``getDataFromPlayer`` and ``saveData`` messages getting +printed to your server log. diff --git a/docs/wiki/docs/coroutine.md b/docs/wiki/docs/coroutine.md index db470005..74310fd2 100644 --- a/docs/wiki/docs/coroutine.md +++ b/docs/wiki/docs/coroutine.md @@ -1,36 +1,117 @@ # Kotlin Coroutines and Minecraft Plugins When starting with [Coroutines in Kotlin](https://kotlinlang.org/docs/coroutines-basics.html), it is interesting -how this can be translated to the world of minecraft plugins. It is recommended to learn how Kotlin Coroutines work before you continue here. +how this can be translated to the world of minecraft plugins. It is recommended to learn how Kotlin Coroutines work +before you continue here. !!! note "Important" Make sure you have already installed MCCoroutine. See [Installation](/gettingstarted) for details. ### Starting a coroutine -For beginners, it is often confusing how to enter a coroutine. The examples in the official guide mostly use ``runBlocking`` +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. * To enter a coroutine **anywhere** in your code at any time: -```kotlin -fun foo() { - plugin.launch { - // This will always be on the minecraft main thread. +=== "Bukkit" + + ```kotlin + import com.github.shynixn.mccoroutine.bukkit.launch + import org.bukkit.plugin.Plugin + + fun foo() { + plugin.launch { + // This will always be on the minecraft main thread. + } } -} -``` + ``` + +=== "BungeeCord" + + ```kotlin + import com.github.shynixn.mccoroutine.bungeecord.launch + import net.md_5.bungee.api.plugin.Plugin + + fun foo() { + plugin.launch { + // This will be a random thread on the BungeeCord threadpool + } + } + ``` + +=== "Sponge" + + ```kotlin + import com.github.shynixn.mccoroutine.sponge.launch + import org.spongepowered.api.plugin.PluginContainer + + fun foo() { + plugin.launch { + // This will always be on the minecraft main thread. + } + } + ``` + +=== "Velocity" + + ```kotlin + import com.github.shynixn.mccoroutine.velocity.launch + import com.velocitypowered.api.plugin.PluginContainer + + fun foo() { + plugin.launch { + // This will be a random thread on the Velocity threadpool + } + } + ``` + +=== "Minestom" + + Minestom has got 2 lifecycle scopes, the server scope and the extension scope. + When this guide talks about a ``plugin``, the corresponding class in Minestom is ``Extension`` or ``MinecraftServer`` depending on your usecase. + + Server level (if you are developing a new server): + + ```kotlin + import com.github.shynixn.mccoroutine.minestom.launch + import net.minestom.server.MinecraftServer + + fun foo() { + server.launch { + // This will always be on the minecraft main thread. + } + } + ``` + + Extension level (if you are developing a new extension): + + ```kotlin + import com.github.shynixn.mccoroutine.minestom.launch + import net.minestom.server.extensions.Extension + + fun foo() { + extension.launch { + // This will always be on the minecraft main thread. + } + } + ``` + ### 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: + +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: * minecraftDispatcher (Allows to execute coroutines on the main minecraft thread) * asyncDispatcher (Allows to execute coroutines on the async minecraft threadpool) !!! note "Important" - **However, it is highly recommend to use ``Dispatchers.IO`` instead of asyncDispatcher because the scheduling is more accurate.** + **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). An example how this works is shown below: @@ -60,10 +141,13 @@ fun foo() { } ``` -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: +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 { @@ -74,7 +158,7 @@ fun onPlayerJoinEvent(event: PlayerJoinEvent) { val friendNames = Files.readAllLines(Paths.get("$name.txt")) friendNames } - + // Main Thread val friendText = listOfFriends.joinToString(", ") event.player.sendMessage("My friends are: $friendText") @@ -88,24 +172,25 @@ fun onPlayerJoinEvent(event: PlayerJoinEvent) { If you use ``plugin.launch``, it is important to understand the execution order. ````kotlin -class Foo(private val plugin : Plugin) { +// 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 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(){ + private suspend fun bob() { println("I am fifth") } } @@ -119,9 +204,10 @@ class Foo(private val plugin : Plugin) { "I am fifth" ```` -### Coroutines everywhere +### Coroutines everywhere -Using ``plugin.launch{}``is valuable if you migrate existing plugins to use coroutines. However, if you write a new plugin from scratch, you may consider using +Using ``plugin.launch{}``is valuable if you migrate existing plugins to use coroutines. However, if you write a new +plugin from scratch, you may consider using convenience integrations provided by MCCoroutine such as: * Suspending Plugin diff --git a/docs/wiki/docs/exception.md b/docs/wiki/docs/exception.md index 335a6a95..a5157743 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) +* The event arrives at the main thread (Bukkit, Sponge, Minestom) * 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 07c82709..b2001101 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.9.0") - implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:2.9.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:2.10.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:2.10.0") } ``` @@ -17,8 +17,8 @@ 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.9.0") - implementation("com.github.shynixn.mccoroutine:mccoroutine-bungeecord-core:2.9.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-bungeecord-api:2.10.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-bungeecord-core:2.10.0") } ``` @@ -26,8 +26,8 @@ In order to use the MCCoroutine Kotlin API, you need to include the following li ```groovy dependencies { - implementation("com.github.shynixn.mccoroutine:mccoroutine-sponge-api:2.9.0") - implementation("com.github.shynixn.mccoroutine:mccoroutine-sponge-core:2.9.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-sponge-api:2.10.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-sponge-core:2.10.0") } ``` @@ -35,8 +35,17 @@ In order to use the MCCoroutine Kotlin API, you need to include the following li ```groovy dependencies { - implementation("com.github.shynixn.mccoroutine:mccoroutine-velocity-api:2.9.0") - implementation("com.github.shynixn.mccoroutine:mccoroutine-velocity-core:2.9.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-velocity-api:2.10.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-velocity-core:2.10.0") + } + ``` + +=== "Minestom" + + ```groovy + dependencies { + implementation("com.github.shynixn.mccoroutine:mccoroutine-minestom-api:2.10.0") + implementation("com.github.shynixn.mccoroutine:mccoroutine-minestom-core:2.10.0") } ``` @@ -60,8 +69,8 @@ dependencies { **plugin.yml** ```yaml libraries: - - com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:2.9.0 - - com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:2.9.0 + - com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:2.10.0 + - com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:2.10.0 ``` === "Other Server" @@ -74,6 +83,6 @@ dependencies { Try to call ``launch{}`` in your ``onEnable()`` function in your ``Plugin`` class. !!! note "Further help" - Please take a look at the sample plugins ``mccoroutine-bukkit-sample`` or ``mccoroutine-sponge-sample`` which + Please take a look at the sample plugins (e.g. ``mccoroutine-bukkit-sample`` or ``mccoroutine-sponge-sample``) which can be found on [Github](https://github.com/Shynixn/MCCoroutine). A real production plugin using MCCoroutine can be found [here](https://github.com/Shynixn/BlockBall). diff --git a/docs/wiki/docs/listener.md b/docs/wiki/docs/listener.md index 02a30ea1..3d3f664b 100644 --- a/docs/wiki/docs/listener.md +++ b/docs/wiki/docs/listener.md @@ -134,6 +134,32 @@ suspendable functions). You can mix suspendable and non suspendable functions in } ```` +=== "Minestom" + + ````kotlin + import net.minestom.server.event.player.PlayerDisconnectEvent + import net.minestom.server.event.player.PlayerLoginEvent + import java.util.* + + class PlayerDataListener(private val database: Database) { + suspend fun onPlayerJoinEvent(event: PlayerLoginEvent) { + val player = event.player + val playerData = database.getDataFromPlayer(player) + playerData.name = player.username + playerData.lastJoinDate = Date() + database.saveData(player, playerData) + } + + suspend fun onPlayerQuitEvent(event: PlayerDisconnectEvent) { + val player = event.player + val playerData = database.getDataFromPlayer(player) + playerData.name = player.username + playerData.lastQuitDate = Date() + database.saveData(player, playerData) + } + } + ```` + ### Register the Listener === "Bukkit" @@ -264,6 +290,37 @@ 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) + } + ``` + ### 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 1dcab5d0..f9328903 100644 --- a/docs/wiki/docs/plugin.md +++ b/docs/wiki/docs/plugin.md @@ -1,6 +1,6 @@ # Suspending Plugin -This guide explains how Kotlin Coroutines can be used in minecraft plugins in various ways using MCCoroutine. +This guide explains how Kotlin Coroutines can be used in minecraft plugins in various ways using MCCoroutine. For this, a new plugin is developed from scratch to handle asynchronous and synchronous code. !!! note "Important" @@ -8,8 +8,9 @@ For this, a new plugin is developed from scratch to handle asynchronous and sync ## Plugin Main class -MCCoroutine does not need to be called explicitly in your plugin main class. It is started implicitly when you use it for the first time and -disposed automatically when you reload your plugin. +MCCoroutine does not need to be called explicitly in your plugin main class. It is started implicitly when you use it +for the first time and +disposed automatically when you reload your plugin. === "Bukkit" @@ -43,7 +44,6 @@ 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. - === "BungeeCord" The first decision for BungeeCord API based plugins is to decide between ``Plugin`` or ``SuspendingPlugin``, which is a new base @@ -129,16 +129,34 @@ 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) + } + ``` + ## Calling a Database from Plugin Main class Create a class containing properties of data, which we want to store into a database. ````kotlin -class PlayerData(var uuid: UUID, var name: String, var lastJoinDate: Date, var lastQuitDate : Date) { +class PlayerData(var uuid: UUID, var name: String, var lastJoinDate: Date, var lastQuitDate: Date) { } ```` -Create a class ``Database``, which is responsible to store/retrieve this data into/from a database. +Create a class ``Database``, which is responsible to store/retrieve this data into/from a database. Here, it is important that we perform all IO calls on async threads and returns on the minecraft main thread. === "Bukkit" @@ -321,6 +339,49 @@ Here, it is important that we perform all IO calls on async threads and returns } ```` +=== "Minestom" + + ```kotlin + import kotlinx.coroutines.Dispatchers + import kotlinx.coroutines.withContext + import net.minestom.server.entity.Player + import java.util.* + + class Database() { + suspend fun createDbIfNotExist() { + println("[createDbIfNotExist] Start on minecraft thread " + Thread.currentThread().id) + withContext(Dispatchers.IO){ + println("[createDbIfNotExist] Creating database on database io thread " + Thread.currentThread().id) + // ... create tables + } + println("[createDbIfNotExist] End on minecraft thread " + Thread.currentThread().id) + } + + 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.username, Date(), Date()) + } + + println("[getDataFromPlayer] End on minecraft thread " + Thread.currentThread().id) + return playerData; + } + + suspend fun saveData(player : Player, playerData : PlayerData) { + println("[saveData] Start on minecraft thread " + Thread.currentThread().id) + + withContext(Dispatchers.IO){ + println("[saveData] Saving player data on database io thread " + Thread.currentThread().id) + // insert or update playerData + } + + println("[saveData] End on minecraft thread " + Thread.currentThread().id) + } + } + ``` + Create a new instance of the database and call it in your main class. === "Bukkit" @@ -415,6 +476,23 @@ 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) + } + ``` + ## 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 f002f4a5..2776d605 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.9.0") + testImplementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-test:2.10.0") } ``` diff --git a/mccoroutine-bukkit-api/build.gradle.kts b/mccoroutine-bukkit-api/build.gradle.kts index d0a15f71..f42cf9f7 100644 --- a/mccoroutine-bukkit-api/build.gradle.kts +++ b/mccoroutine-bukkit-api/build.gradle.kts @@ -7,5 +7,5 @@ repositories { dependencies { compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") compileOnly("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT") - testCompile("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT") + testImplementation("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT") } diff --git a/mccoroutine-bukkit-core/build.gradle.kts b/mccoroutine-bukkit-core/build.gradle.kts index 90dcfaf7..889474fe 100644 --- a/mccoroutine-bukkit-core/build.gradle.kts +++ b/mccoroutine-bukkit-core/build.gradle.kts @@ -10,6 +10,6 @@ dependencies { compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") compileOnly("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT") - testCompile("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") - testCompile("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") + testImplementation("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT") } diff --git a/mccoroutine-bukkit-core/src/test/java/integrationtest/BukkitEventPriorityTest.kt b/mccoroutine-bukkit-core/src/test/java/integrationtest/BukkitEventPriorityTest.kt index 5f81215d..340f8ce2 100644 --- a/mccoroutine-bukkit-core/src/test/java/integrationtest/BukkitEventPriorityTest.kt +++ b/mccoroutine-bukkit-core/src/test/java/integrationtest/BukkitEventPriorityTest.kt @@ -14,9 +14,9 @@ import org.bukkit.event.EventHandler import org.bukkit.event.EventPriority import org.bukkit.event.Listener import org.bukkit.event.player.PlayerJoinEvent +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.mockito.Mockito -import kotlin.test.assertEquals class BukkitEventPriorityTest { /** @@ -44,11 +44,11 @@ class BukkitEventPriorityTest { val actualResult = classUnderTest.resultList // Assert - assertEquals(server.mainThreadId, classUnderTest.startThreadId) - assertEquals(server.mainThreadId, classUnderTest.endThreadId) - assertEquals(2, actualResult[0]) - assertEquals(3, actualResult[1]) - assertEquals(1, actualResult[2]) + Assertions.assertEquals(server.mainThreadId, classUnderTest.startThreadId) + Assertions.assertEquals(server.mainThreadId, classUnderTest.endThreadId) + Assertions.assertEquals(2, actualResult[0]) + Assertions.assertEquals(3, actualResult[1]) + Assertions.assertEquals(1, actualResult[2]) } /** @@ -77,11 +77,11 @@ class BukkitEventPriorityTest { val actualResult = classUnderTest.resultList // Assert - assertEquals(server.mainThreadId, classUnderTest.startThreadId) - assertEquals(server.mainThreadId, classUnderTest.endThreadId) - assertEquals(1, actualResult[0]) - assertEquals(2, actualResult[1]) - assertEquals(3, actualResult[2]) + Assertions.assertEquals(server.mainThreadId, classUnderTest.startThreadId) + Assertions.assertEquals(server.mainThreadId, classUnderTest.endThreadId) + Assertions.assertEquals(1, actualResult[0]) + Assertions.assertEquals(2, actualResult[1]) + Assertions.assertEquals(3, actualResult[2]) } private class TestEventListener : Listener { diff --git a/mccoroutine-bukkit-core/src/test/java/integrationtest/BukkitEventTest.kt b/mccoroutine-bukkit-core/src/test/java/integrationtest/BukkitEventTest.kt index 710aac39..f8e38749 100644 --- a/mccoroutine-bukkit-core/src/test/java/integrationtest/BukkitEventTest.kt +++ b/mccoroutine-bukkit-core/src/test/java/integrationtest/BukkitEventTest.kt @@ -14,10 +14,9 @@ import org.bukkit.event.player.AsyncPlayerChatEvent import org.bukkit.event.player.PlayerJoinEvent import org.bukkit.event.player.PlayerQuitEvent import org.bukkit.plugin.Plugin +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.mockito.Mockito -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals class BukkitEventTest { /** @@ -42,7 +41,7 @@ class BukkitEventTest { } // Assert - assertEquals(server.mainThreadId, testListener.joinEventCalledId) + Assertions.assertEquals(server.mainThreadId, testListener.joinEventCalledId) } /** @@ -67,7 +66,7 @@ class BukkitEventTest { } // Assert - assertEquals(server.mainThreadId, testListener.quitEventCalledId) + Assertions.assertEquals(server.mainThreadId, testListener.quitEventCalledId) } /** @@ -92,7 +91,7 @@ class BukkitEventTest { } // Assert - assertNotEquals(server.mainThreadId, testListener.asyncChatEventCalledId) + Assertions.assertNotEquals(server.mainThreadId, testListener.asyncChatEventCalledId) } private fun createWithDependencies(plugin: Plugin): EventServiceImpl { diff --git a/mccoroutine-bukkit-core/src/test/java/unittest/BukkitMCCoroutineTest.kt b/mccoroutine-bukkit-core/src/test/java/unittest/BukkitMCCoroutineTest.kt index 8d922ca0..b60ba499 100644 --- a/mccoroutine-bukkit-core/src/test/java/unittest/BukkitMCCoroutineTest.kt +++ b/mccoroutine-bukkit-core/src/test/java/unittest/BukkitMCCoroutineTest.kt @@ -4,9 +4,9 @@ import com.github.shynixn.mccoroutine.bukkit.impl.MCCoroutineImpl import org.bukkit.Server import org.bukkit.plugin.Plugin import org.bukkit.plugin.PluginManager +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.mockito.Mockito -import kotlin.test.assertEquals class BukkitMCCoroutineTest { /** @@ -30,7 +30,7 @@ class BukkitMCCoroutineTest { val session2 = classUnderTest.getCoroutineSession(plugin) // Assert - assertEquals(session1, session2) + Assertions.assertEquals(session1, session2) } private fun createWithDependencies(): MCCoroutineImpl { diff --git a/mccoroutine-bukkit-core/src/test/java/unittest/BukkitPluginListenerTest.kt b/mccoroutine-bukkit-core/src/test/java/unittest/BukkitPluginListenerTest.kt index 1735fbbc..203e67d2 100644 --- a/mccoroutine-bukkit-core/src/test/java/unittest/BukkitPluginListenerTest.kt +++ b/mccoroutine-bukkit-core/src/test/java/unittest/BukkitPluginListenerTest.kt @@ -7,10 +7,9 @@ import com.github.shynixn.mccoroutine.bukkit.impl.CoroutineSessionImpl import com.github.shynixn.mccoroutine.bukkit.listener.PluginListener import org.bukkit.event.server.PluginDisableEvent import org.bukkit.plugin.Plugin +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.mockito.Mockito -import kotlin.test.assertFalse -import kotlin.test.assertTrue class BukkitPluginListenerTest { /** @@ -30,7 +29,7 @@ class BukkitPluginListenerTest { classUnderTest.onPluginDisable(pluginDisableEvent) // Assert - assertTrue(mcCoroutine.disableCalled) + Assertions.assertTrue(mcCoroutine.disableCalled) } /** @@ -49,7 +48,7 @@ class BukkitPluginListenerTest { classUnderTest.onPluginDisable(pluginDisableEvent) // Assert - assertFalse(mcCoroutine.disableCalled) + Assertions.assertFalse(mcCoroutine.disableCalled) } private fun createWithDependencies( diff --git a/mccoroutine-bukkit-sample/build.gradle.kts b/mccoroutine-bukkit-sample/build.gradle.kts index ae9f2003..cdc81152 100644 --- a/mccoroutine-bukkit-sample/build.gradle.kts +++ b/mccoroutine-bukkit-sample/build.gradle.kts @@ -36,5 +36,5 @@ dependencies { compileOnly("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT") testImplementation(project(":mccoroutine-bukkit-test")) - testCompile("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT") + testImplementation("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT") } diff --git a/mccoroutine-bukkit-sample/src/main/resources/plugin.yml b/mccoroutine-bukkit-sample/src/main/resources/plugin.yml index fbe82f2d..71177afc 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.9.0 +version: 2.10.0 author: Shynixn main: com.github.shynixn.mccoroutine.bukkit.sample.MCCoroutineSamplePlugin commands: diff --git a/mccoroutine-bukkit-sample/src/test/java/ExampleUnitTest.kt b/mccoroutine-bukkit-sample/src/test/java/ExampleUnitTest.kt index c71a3564..cea63cfa 100644 --- a/mccoroutine-bukkit-sample/src/test/java/ExampleUnitTest.kt +++ b/mccoroutine-bukkit-sample/src/test/java/ExampleUnitTest.kt @@ -11,9 +11,9 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.bukkit.entity.Player import org.bukkit.plugin.Plugin +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.mockito.Mockito -import kotlin.test.assertEquals class ExampleUnitTest { @@ -76,7 +76,7 @@ class ExampleUnitTest { val data2 = classUnderTest.getUserDataFromPlayerAsync(player) // Should be the same instance because of cache hit. Hashcode should be equal. - assertEquals(data1, data2) + Assertions.assertEquals(data1, data2) } } } diff --git a/mccoroutine-bukkit-test/build.gradle.kts b/mccoroutine-bukkit-test/build.gradle.kts index 90dcfaf7..889474fe 100644 --- a/mccoroutine-bukkit-test/build.gradle.kts +++ b/mccoroutine-bukkit-test/build.gradle.kts @@ -10,6 +10,6 @@ dependencies { compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") compileOnly("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT") - testCompile("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") - testCompile("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") + testImplementation("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT") } diff --git a/mccoroutine-bungeecord-api/build.gradle.kts b/mccoroutine-bungeecord-api/build.gradle.kts index 6b6686e2..4080e848 100644 --- a/mccoroutine-bungeecord-api/build.gradle.kts +++ b/mccoroutine-bungeecord-api/build.gradle.kts @@ -7,5 +7,5 @@ repositories { dependencies { compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") compileOnly("net.md-5:bungeecord-api:1.16-R0.5-SNAPSHOT") - testCompile("net.md-5:bungeecord-api:1.16-R0.5-SNAPSHOT") + testImplementation("net.md-5:bungeecord-api:1.16-R0.5-SNAPSHOT") } diff --git a/mccoroutine-bungeecord-core/build.gradle.kts b/mccoroutine-bungeecord-core/build.gradle.kts index e663f727..698ebf5d 100644 --- a/mccoroutine-bungeecord-core/build.gradle.kts +++ b/mccoroutine-bungeecord-core/build.gradle.kts @@ -12,6 +12,6 @@ dependencies { compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.9") compileOnly("net.md-5:bungeecord-api:1.16-R0.5-SNAPSHOT") - testCompile("net.md-5:bungeecord-api:1.16-R0.5-SNAPSHOT") - testCompile("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") + testImplementation("net.md-5:bungeecord-api:1.16-R0.5-SNAPSHOT") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") } diff --git a/mccoroutine-bungeecord-core/src/test/java/unittest/BungeeCordMCCoroutineTest.kt b/mccoroutine-bungeecord-core/src/test/java/unittest/BungeeCordMCCoroutineTest.kt index f7005405..3fe8ad54 100644 --- a/mccoroutine-bungeecord-core/src/test/java/unittest/BungeeCordMCCoroutineTest.kt +++ b/mccoroutine-bungeecord-core/src/test/java/unittest/BungeeCordMCCoroutineTest.kt @@ -2,10 +2,10 @@ package unittest import com.github.shynixn.mccoroutine.bungeecord.impl.MCCoroutineImpl import net.md_5.bungee.api.plugin.Plugin +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.mockito.Mockito import java.util.concurrent.Executors -import kotlin.test.assertEquals class BungeeCordMCCoroutineTest { /** @@ -26,7 +26,7 @@ class BungeeCordMCCoroutineTest { val session2 = classUnderTest.getCoroutineSession(plugin) // Assert - assertEquals(session1, session2) + Assertions.assertEquals(session1, session2) } private fun createWithDependencies(): MCCoroutineImpl { diff --git a/mccoroutine-bungeecord-sample/build.gradle.kts b/mccoroutine-bungeecord-sample/build.gradle.kts index 620ed919..f05c3726 100644 --- a/mccoroutine-bungeecord-sample/build.gradle.kts +++ b/mccoroutine-bungeecord-sample/build.gradle.kts @@ -34,5 +34,5 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.9") compileOnly("net.md-5:bungeecord-api:1.16-R0.5-SNAPSHOT") - testCompile("net.md-5:bungeecord-api:1.16-R0.5-SNAPSHOT") + testImplementation("net.md-5:bungeecord-api:1.16-R0.5-SNAPSHOT") } diff --git a/mccoroutine-bungeecord-sample/src/main/resources/plugin.yml b/mccoroutine-bungeecord-sample/src/main/resources/plugin.yml index 0b2f8709..37734861 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.9.0 +version: 2.10.0 author: Shynixn main: com.github.shynixn.mccoroutine.bungeecord.sample.MCCoroutineSamplePlugin commands: diff --git a/mccoroutine-minestom-api/build.gradle.kts b/mccoroutine-minestom-api/build.gradle.kts new file mode 100644 index 00000000..117933c3 --- /dev/null +++ b/mccoroutine-minestom-api/build.gradle.kts @@ -0,0 +1,17 @@ +repositories { + maven { + url = uri("https://jitpack.io") + } +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +dependencies { + compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") + compileOnly("com.github.Minestom:Minestom:8eb089bf3e") + testImplementation("com.github.Minestom:Minestom:8eb089bf3e") +} diff --git a/mccoroutine-minestom-api/src/main/java/com/github/shynixn/mccoroutine/minestom/CoroutineSession.kt b/mccoroutine-minestom-api/src/main/java/com/github/shynixn/mccoroutine/minestom/CoroutineSession.kt new file mode 100644 index 00000000..5d5136c5 --- /dev/null +++ b/mccoroutine-minestom-api/src/main/java/com/github/shynixn/mccoroutine/minestom/CoroutineSession.kt @@ -0,0 +1,29 @@ +package com.github.shynixn.mccoroutine.minestom + +import kotlinx.coroutines.CoroutineScope +import kotlin.coroutines.CoroutineContext + +/** + * Facade of a coroutine session of a single extension or entire server. + */ +interface CoroutineSession { + /** + * Lifetime scope. + */ + val scope: CoroutineScope + + /** + * Minecraft Dispatcher. + */ + val dispatcherMinecraft: CoroutineContext + + /** + * Async Dispatcher. + */ + val dispatcherAsync: CoroutineContext + + /** + * MCCoroutine Facade. + */ + val mcCoroutineConfiguration: MCCoroutineConfiguration +} diff --git a/mccoroutine-minestom-api/src/main/java/com/github/shynixn/mccoroutine/minestom/MCCoroutine.kt b/mccoroutine-minestom-api/src/main/java/com/github/shynixn/mccoroutine/minestom/MCCoroutine.kt new file mode 100644 index 00000000..33d6210d --- /dev/null +++ b/mccoroutine-minestom-api/src/main/java/com/github/shynixn/mccoroutine/minestom/MCCoroutine.kt @@ -0,0 +1,345 @@ +package com.github.shynixn.mccoroutine.minestom + +import kotlinx.coroutines.* +import net.minestom.server.MinecraftServer +import net.minestom.server.command.CommandSender +import net.minestom.server.command.builder.Command +import net.minestom.server.command.builder.CommandContext +import net.minestom.server.event.Event +import net.minestom.server.event.EventListener +import net.minestom.server.event.EventNode +import net.minestom.server.extensions.Extension +import net.minestom.server.thread.Acquirable +import net.minestom.server.thread.AcquirableCollection +import kotlin.coroutines.ContinuationInterceptor +import kotlin.coroutines.CoroutineContext + +/** + * Static session. + */ +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-minestom-core into your application.", + e + ) + } +} + +/** + * Gets the configuration instance of MCCoroutine. + */ +val Extension.mcCoroutineConfiguration: MCCoroutineConfiguration + get() { + return mcCoroutine.getCoroutineSession(this).mcCoroutineConfiguration + } + +/** + * Gets the configuration instance of MCCoroutine. + */ +val MinecraftServer.mcCoroutineConfiguration: MCCoroutineConfiguration + get() { + return mcCoroutine.getCoroutineSession(this).mcCoroutineConfiguration + } + +/** + * Gets the extension minecraft dispatcher. + */ +val Extension.minecraftDispatcher: CoroutineContext + get() { + return mcCoroutine.getCoroutineSession(this).dispatcherMinecraft + } + +/** + * Gets the server minecraft dispatcher. + */ +val MinecraftServer.minecraftDispatcher: CoroutineContext + get() { + return mcCoroutine.getCoroutineSession(this).dispatcherMinecraft + } + +/** + * Gets the extension async dispatcher. + */ +val Extension.asyncDispatcher: CoroutineContext + get() { + return mcCoroutine.getCoroutineSession(this).dispatcherAsync + } + +/** + * Gets the server async dispatcher. + */ +val MinecraftServer.asyncDispatcher: CoroutineContext + get() { + return mcCoroutine.getCoroutineSession(this).dispatcherAsync + } + +/** + * Gets the extension coroutine scope. + */ +val Extension.scope: CoroutineScope + get() { + return mcCoroutine.getCoroutineSession(this).scope + } + +/** + * Gets the extension coroutine scope. + */ +val MinecraftServer.scope: CoroutineScope + get() { + return mcCoroutine.getCoroutineSession(this).scope + } + +/** + * Launches a new coroutine on the minecraft main 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 [scope]. Additional context elements can be specified with [context] argument. + * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [minecraftDispatcher] is used. + * The parent job is inherited from a [scope] as well, but it can also be overridden + * with a corresponding [context] element. + * + * By default, the coroutine is immediately scheduled for execution if the current thread is already the minecraft server thread. + * If the current thread is not the minecraft server thread, the coroutine is moved to the [net.minestom.server.timer.Scheduler] and executed + * in the next server tick schedule. + * 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 extension or server logger by default. + * + * @param context The coroutine context to start. Should almost be always be [minecraftDispatcher]. Async operations should be + * be created using [withContext] after using the default parameters of this method. + * @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 Extension.launch( + context: CoroutineContext = minecraftDispatcher, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> Unit +): Job { + if (!scope.isActive) { + return Job() + } + + return scope.launch(context, start, block) +} + +/** + * Launches a new coroutine on the minecraft main 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 [scope]. Additional context elements can be specified with [context] argument. + * If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [minecraftDispatcher] is used. + * The parent job is inherited from a [scope] as well, but it can also be overridden + * with a corresponding [context] element. + * + * By default, the coroutine is immediately scheduled for execution if the current thread is already the minecraft server thread. + * If the current thread is not the minecraft server thread, the coroutine is moved to the [net.minestom.server.timer.Scheduler] and executed + * in the next server tick schedule. + * 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 extension or server logger by default. + * + * @param context The coroutine context to start. Should almost be always be [minecraftDispatcher]. Async operations should be + * be created using [withContext] after using the default parameters of this method. + * @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 MinecraftServer.launch( + context: CoroutineContext = minecraftDispatcher, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> Unit +): Job { + if (!scope.isActive) { + return Job() + } + + return scope.launch(context, start, block) +} + +/** + * 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 MineStomScheduler 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) + } + +/** + * Resolves the object (e.g. Entity) hidden inside the [Acquirable] object without blocking. + * [Acquirable] are resolved immediately if already reserved for the current thread or resolved on a different + * thread and the function is executed on it. You should only read/write the currently resolved object in the callback parameter. Do + * not do anything like accessing fields or methods, do that outside of the callback parameter. + * If you want to edit multiple [Acquirable] objects at once, put it into a collection [e.g. List, Set, etc.] and use Collection#asyncSuspend. + */ +suspend fun Acquirable.asyncSuspend(f: (T) -> R): R { + val acquire = this + return withContext(Dispatchers.IO) { + var result: R? = null + acquire.sync { element -> + result = f.invoke(element) + } + result!! + } +} + +/** + * Resolves the objects (e.g. Entities) hidden inside the [Acquirable] object without blocking. + * [Acquirable] are resolved immediately if already reserved for the current thread or resolved on a different + * thread and the function is executed on it. You should only read/write the currently resolved object in the callback parameter. Do + * not do anything like accessing fields or methods, do that outside of the callback parameter. + */ +suspend fun Collection>.asyncSuspend(f: (Collection) -> R): R { + val acquirableCollection = AcquirableCollection(this) + return withContext(Dispatchers.IO) { + val resolved = ArrayList() + acquirableCollection.acquireSync { + resolved.add(it) + } + f.invoke(resolved) + } +} + +/** + * Sets the default {@link CommandExecutor}. + * + * @param server MineCraft server. + * @param executor the new default executor. + * @see #getDefaultExecutor() + */ +fun Command.setSuspendingDefaultExecutor( + server: MinecraftServer, + executor: suspend (CommandSender, CommandContext) -> Unit +) { + this.setDefaultExecutor { sender: CommandSender, context -> + server.launch { + executor.invoke(sender, context) + } + } +} + +/** + * Sets the default {@link CommandExecutor}. + * + * @param server MineCraft server. + * @param executor the new default executor. + * @see #getDefaultExecutor() + */ +fun Command.setSuspendingDefaultExecutor( + extension: Extension, + executor: suspend (CommandSender, CommandContext) -> Unit +) { + this.setDefaultExecutor { sender: CommandSender, context -> + extension.launch { + executor.invoke(sender, context) + } + } +} + +/** + * Adds a new suspendable listener to this event node. + */ +fun EventNode.addSuspendingListener( + server: MinecraftServer, + eventType: Class, + listener: suspend (E) -> Unit +) { + this.addListener(eventType) { e -> + server.launch { + listener.invoke(e) + } + } +} + +/** + * Adds a new suspendable listener to this event node. + */ +fun EventNode.addSuspendingListener( + extension: Extension, + eventType: Class, + listener: suspend (E) -> Unit +) { + this.addListener(eventType) { e -> + extension.launch { + listener.invoke(e) + } + } +} + +/** + * Adds a new suspendable handler to this builder. + */ +fun EventListener.Builder.suspendingHandler( + server: MinecraftServer, + listener: suspend (E) -> Unit +): EventListener.Builder { + return this.handler { e -> + server.launch { + listener.invoke(e) + } + } +} + +/** + * Adds a new suspendable handler to this builder. + */ +fun EventListener.Builder.suspendingHandler( + extension: Extension, + listener: suspend (E) -> Unit +): EventListener.Builder { + return this.handler { e -> + extension.launch { + listener.invoke(e) + } + } +} + +/** + * 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.minestom.impl.MCCoroutineImpl" + } + + /** + * Get coroutine session for the given extension. + * When using an extension, coroutine scope is bound to the lifetime of the extension. + */ + fun getCoroutineSession(extension: Extension): CoroutineSession + + /** + * Get coroutine session for the given server. + * When using a server, coroutine scope is bound to the lifetime of the entire server. + */ + fun getCoroutineSession(minecraftServer: MinecraftServer): CoroutineSession + + /** + * Disposes the given extension coroutine session. + */ + fun disable(extension: Extension) + + /** + * Disposes the given server coroutine session. + */ + fun disable(minecraftServer: MinecraftServer) +} diff --git a/mccoroutine-minestom-api/src/main/java/com/github/shynixn/mccoroutine/minestom/MCCoroutineConfiguration.kt b/mccoroutine-minestom-api/src/main/java/com/github/shynixn/mccoroutine/minestom/MCCoroutineConfiguration.kt new file mode 100644 index 00000000..889b199b --- /dev/null +++ b/mccoroutine-minestom-api/src/main/java/com/github/shynixn/mccoroutine/minestom/MCCoroutineConfiguration.kt @@ -0,0 +1,20 @@ +package com.github.shynixn.mccoroutine.minestom + +/** + * 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 + + /** + * Manually disposes the MCCoroutine session for the current extension or server. + */ + fun disposePluginSession() +} diff --git a/mccoroutine-minestom-api/src/main/java/com/github/shynixn/mccoroutine/minestom/MCCoroutineExceptionEvent.kt b/mccoroutine-minestom-api/src/main/java/com/github/shynixn/mccoroutine/minestom/MCCoroutineExceptionEvent.kt new file mode 100644 index 00000000..0243c087 --- /dev/null +++ b/mccoroutine-minestom-api/src/main/java/com/github/shynixn/mccoroutine/minestom/MCCoroutineExceptionEvent.kt @@ -0,0 +1,41 @@ +package com.github.shynixn.mccoroutine.minestom + +import net.minestom.server.event.Event +import net.minestom.server.event.trait.CancellableEvent +import net.minestom.server.extensions.Extension + +/** + * A Minestom 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( + /** + * Extension causing the exception. + * Is null if the exception is thrown from the root Minecraft Server implementation. + */ + val extension: Extension?, + /** + * The exception to be logged. + */ + val exception: Throwable +) : CancellableEvent { + private var cancelled: Boolean = false + + /** + * Gets if the [Event] should be cancelled or not. + * + * @return true if the event should be cancelled + */ + override fun isCancelled(): Boolean { + return cancelled + } + + /** + * Marks the [Event] as cancelled or not. + * + * @param cancel true if the event should be cancelled, false otherwise + */ + override fun setCancelled(cancel: Boolean) { + this.cancelled = cancel + } +} diff --git a/mccoroutine-minestom-api/src/main/java/com/github/shynixn/mccoroutine/minestom/ShutdownStrategy.kt b/mccoroutine-minestom-api/src/main/java/com/github/shynixn/mccoroutine/minestom/ShutdownStrategy.kt new file mode 100644 index 00000000..eb39a7d6 --- /dev/null +++ b/mccoroutine-minestom-api/src/main/java/com/github/shynixn/mccoroutine/minestom/ShutdownStrategy.kt @@ -0,0 +1,12 @@ +package com.github.shynixn.mccoroutine.minestom + +/** + * See https://shynixn.github.io/MCCoroutine/wiki/site/plugindisable for more details. + */ +enum class ShutdownStrategy { + /** + * Default shutdown strategy. + * The coroutine session needs to be explicitly disposed. + */ + MANUAL +} diff --git a/mccoroutine-minestom-core/build.gradle.kts b/mccoroutine-minestom-core/build.gradle.kts new file mode 100644 index 00000000..6ddb61c4 --- /dev/null +++ b/mccoroutine-minestom-core/build.gradle.kts @@ -0,0 +1,21 @@ +repositories { + maven { + url = uri("https://jitpack.io") + } +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +dependencies { + implementation(project(":mccoroutine-minestom-api")) + + 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:8eb089bf3e") + testImplementation("com.github.Minestom:Minestom:8eb089bf3e") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") +} diff --git a/mccoroutine-minestom-core/src/main/java/com/github/shynixn/mccoroutine/minestom/dispatcher/AsyncCoroutineDispatcher.kt b/mccoroutine-minestom-core/src/main/java/com/github/shynixn/mccoroutine/minestom/dispatcher/AsyncCoroutineDispatcher.kt new file mode 100644 index 00000000..ced06013 --- /dev/null +++ b/mccoroutine-minestom-core/src/main/java/com/github/shynixn/mccoroutine/minestom/dispatcher/AsyncCoroutineDispatcher.kt @@ -0,0 +1,28 @@ +package com.github.shynixn.mccoroutine.minestom.dispatcher + +import kotlinx.coroutines.CoroutineDispatcher +import net.minestom.server.MinecraftServer +import net.minestom.server.timer.ExecutionType +import kotlin.coroutines.CoroutineContext + +/** + * Minestom Async ThreadPool Dispatcher. Always dispatch. + */ +internal open class AsyncCoroutineDispatcher() : 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 { + return true + } + + /** + * Handles dispatching the coroutine on the correct thread. + */ + override fun dispatch(context: CoroutineContext, block: Runnable) { + MinecraftServer.getSchedulerManager().scheduleNextTick(block, ExecutionType.ASYNC) + } +} diff --git a/mccoroutine-minestom-core/src/main/java/com/github/shynixn/mccoroutine/minestom/dispatcher/MinecraftCoroutineDispatcher.kt b/mccoroutine-minestom-core/src/main/java/com/github/shynixn/mccoroutine/minestom/dispatcher/MinecraftCoroutineDispatcher.kt new file mode 100644 index 00000000..c96950da --- /dev/null +++ b/mccoroutine-minestom-core/src/main/java/com/github/shynixn/mccoroutine/minestom/dispatcher/MinecraftCoroutineDispatcher.kt @@ -0,0 +1,36 @@ +package com.github.shynixn.mccoroutine.minestom.dispatcher + +import kotlinx.coroutines.CoroutineDispatcher +import net.minestom.server.MinecraftServer +import net.minestom.server.timer.ExecutionType +import kotlin.coroutines.CoroutineContext + +/** + * Server Main Thread Dispatcher. Dispatches only if the call is not at the primary thread yet. + */ +internal open class MinecraftCoroutineDispatcher : CoroutineDispatcher() { + private var mainThreadId: Long = -1 + + init { + MinecraftServer.getSchedulerManager().scheduleNextProcess { + mainThreadId = Thread.currentThread().id + } + } + + /** + * 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 { + return Thread.currentThread().id != mainThreadId + } + + /** + * Handles dispatching the coroutine on the correct thread. + */ + override fun dispatch(context: CoroutineContext, block: Runnable) { + MinecraftServer.getSchedulerManager().scheduleNextTick(block, ExecutionType.SYNC) + } +} diff --git a/mccoroutine-minestom-core/src/main/java/com/github/shynixn/mccoroutine/minestom/impl/CoroutineSessionImpl.kt b/mccoroutine-minestom-core/src/main/java/com/github/shynixn/mccoroutine/minestom/impl/CoroutineSessionImpl.kt new file mode 100644 index 00000000..3fa0bd68 --- /dev/null +++ b/mccoroutine-minestom-core/src/main/java/com/github/shynixn/mccoroutine/minestom/impl/CoroutineSessionImpl.kt @@ -0,0 +1,78 @@ +package com.github.shynixn.mccoroutine.minestom.impl + +import com.github.shynixn.mccoroutine.minestom.CoroutineSession +import com.github.shynixn.mccoroutine.minestom.MCCoroutineConfiguration +import com.github.shynixn.mccoroutine.minestom.MCCoroutineExceptionEvent +import com.github.shynixn.mccoroutine.minestom.dispatcher.AsyncCoroutineDispatcher +import com.github.shynixn.mccoroutine.minestom.dispatcher.MinecraftCoroutineDispatcher +import kotlinx.coroutines.* +import net.minestom.server.MinecraftServer +import net.minestom.server.event.EventDispatcher +import net.minestom.server.extensions.Extension +import kotlin.coroutines.CoroutineContext + +internal class CoroutineSessionImpl( + private val extension: Any, + override val mcCoroutineConfiguration: MCCoroutineConfiguration +) : CoroutineSession { + /** + * Gets minecraft coroutine scope. + */ + override val scope: CoroutineScope + + /** + * Gets the minecraft dispatcher. + */ + override val dispatcherMinecraft: CoroutineContext by lazy { + MinecraftCoroutineDispatcher() + } + + /** + * Gets the async dispatcher. + */ + override val dispatcherAsync: CoroutineContext by lazy { + AsyncCoroutineDispatcher() + } + + init { + // Root Exception Handler. All Exception which are not consumed by the caller end up here. + val exceptionHandler = CoroutineExceptionHandler { _, e -> + val mcCoroutineExceptionEvent = if (extension is Extension) { + MCCoroutineExceptionEvent(extension, e) + } else { + MCCoroutineExceptionEvent(null, e) + } + + MinecraftServer.getSchedulerManager().scheduleNextTick { + EventDispatcher.call(mcCoroutineExceptionEvent) + if (!mcCoroutineExceptionEvent.isCancelled) { + if (extension is Extension) { + extension.logger.error( + "This is not an error of MCCoroutine! See sub exception for details.", + e + ) + } else { + MinecraftServer.LOGGER.error( + "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 supervisor job (e.g. children of a supervisor job can fail independently). + scope = rootCoroutineScope + SupervisorJob() + dispatcherMinecraft + } + + /** + * Disposes the session. + */ + fun dispose() { + scope.coroutineContext.cancelChildren() + scope.cancel() + } +} diff --git a/mccoroutine-minestom-core/src/main/java/com/github/shynixn/mccoroutine/minestom/impl/MCCoroutineConfigurationImpl.kt b/mccoroutine-minestom-core/src/main/java/com/github/shynixn/mccoroutine/minestom/impl/MCCoroutineConfigurationImpl.kt new file mode 100644 index 00000000..2b970330 --- /dev/null +++ b/mccoroutine-minestom-core/src/main/java/com/github/shynixn/mccoroutine/minestom/impl/MCCoroutineConfigurationImpl.kt @@ -0,0 +1,27 @@ +package com.github.shynixn.mccoroutine.minestom.impl + +import com.github.shynixn.mccoroutine.minestom.MCCoroutine +import com.github.shynixn.mccoroutine.minestom.MCCoroutineConfiguration +import com.github.shynixn.mccoroutine.minestom.ShutdownStrategy +import net.minestom.server.MinecraftServer +import net.minestom.server.extensions.Extension + +internal class MCCoroutineConfigurationImpl(private val extension: Any, private val mcCoroutine: MCCoroutine) : + MCCoroutineConfiguration { + /** + * Strategy handling how MCCoroutine is disposed. + * Defaults to ShutdownStrategy.MANUAL. + */ + override var shutdownStrategy: ShutdownStrategy = ShutdownStrategy.MANUAL + + /** + * Manually disposes the MCCoroutine session for the given plugin. + */ + override fun disposePluginSession() { + if (extension is MinecraftServer) { + mcCoroutine.disable(extension) + } else if (extension is Extension) { + mcCoroutine.disable(extension) + } + } +} diff --git a/mccoroutine-minestom-core/src/main/java/com/github/shynixn/mccoroutine/minestom/impl/MCCoroutineImpl.kt b/mccoroutine-minestom-core/src/main/java/com/github/shynixn/mccoroutine/minestom/impl/MCCoroutineImpl.kt new file mode 100644 index 00000000..84158942 --- /dev/null +++ b/mccoroutine-minestom-core/src/main/java/com/github/shynixn/mccoroutine/minestom/impl/MCCoroutineImpl.kt @@ -0,0 +1,68 @@ +package com.github.shynixn.mccoroutine.minestom.impl + +import com.github.shynixn.mccoroutine.minestom.CoroutineSession +import com.github.shynixn.mccoroutine.minestom.MCCoroutine +import net.minestom.server.MinecraftServer +import net.minestom.server.extensions.Extension + +class MCCoroutineImpl : MCCoroutine { + private val items = HashMap() + + /** + * Get coroutine session for the given extension. + * When using an extension, coroutine scope is bound to the lifetime of the extension. + */ + override fun getCoroutineSession(extension: Extension): CoroutineSession { + if (!items.containsKey(extension)) { + startCoroutineSession(extension) + } + + return items[extension]!! + } + + /** + * Get coroutine session for the given extension. + * When using an extension, coroutine scope is bound to the lifetime of the entire server. + */ + override fun getCoroutineSession(minecraftServer: MinecraftServer): CoroutineSession { + if (!items.containsKey(minecraftServer)) { + startCoroutineSession(minecraftServer) + } + + return items[minecraftServer]!! + } + + /** + * Disposes the given extension coroutine session. + */ + override fun disable(extension: Extension) { + if (!items.containsKey(extension)) { + return + } + + val session = items[extension]!! + session.dispose() + items.remove(extension) + } + + /** + * Disposes the given server coroutine session. + */ + override fun disable(minecraftServer: MinecraftServer) { + if (!items.containsKey(minecraftServer)) { + return + } + + val session = items[minecraftServer]!! + session.dispose() + items.remove(minecraftServer) + } + + /** + * Starts a new coroutine session. + */ + private fun startCoroutineSession(extension: Any) { + val mcCoroutineConfiguration = MCCoroutineConfigurationImpl(extension, this) + items[extension] = CoroutineSessionImpl(extension, mcCoroutineConfiguration) + } +} diff --git a/mccoroutine-minestom-core/src/test/java/helper/MockedMinestomServer.kt b/mccoroutine-minestom-core/src/test/java/helper/MockedMinestomServer.kt new file mode 100644 index 00000000..5866bec2 --- /dev/null +++ b/mccoroutine-minestom-core/src/test/java/helper/MockedMinestomServer.kt @@ -0,0 +1,41 @@ +package helper + +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import net.minestom.server.MinecraftServer +import net.minestom.server.extensions.Extension + +class MockedMinestomServer { + fun boot(mlogger: ComponentLogger? = null): Extension { + val extension = MockedExtension(mlogger) + MinecraftServer.init() + Thread { + while (true) { + MinecraftServer.getSchedulerManager().processTick() + Thread.sleep(50) + } + }.start() + return extension + } + + class MockedExtension(private val logger: ComponentLogger?) : Extension() { + override fun initialize() { + } + + override fun terminate() { + } + + + /** + * Gets the logger for the extension + * + * @return The logger for the extension + */ + override fun getLogger(): ComponentLogger { + if (logger != null) { + return logger + } + + return super.getLogger() + } + } +} diff --git a/mccoroutine-minestom-core/src/test/java/integrationtest/MinestomCommandTest.kt b/mccoroutine-minestom-core/src/test/java/integrationtest/MinestomCommandTest.kt new file mode 100644 index 00000000..615f75eb --- /dev/null +++ b/mccoroutine-minestom-core/src/test/java/integrationtest/MinestomCommandTest.kt @@ -0,0 +1,65 @@ +package integrationtest + +import com.github.shynixn.mccoroutine.minestom.setSuspendingDefaultExecutor +import helper.MockedMinestomServer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import net.minestom.server.MinecraftServer +import net.minestom.server.command.builder.Command +import net.minestom.server.extensions.Extension +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test + +class MinestomCommandTest { + /** + * Given + * a call of a simple suspending command + * When + * executeServerCommand is called from any context + * Then + * the command should be called on the correct threads. + */ + @Test + fun dispatchCommand_SimpleSuspendingCommandExecutor_ShouldCallOnCorrectThreads() { + val server = MockedMinestomServer() + val extension = server.boot() + var unitTestThreadId: Long + val testCommandExecutor = TestCommandExecutor(extension) + + runBlocking { + unitTestThreadId = Thread.currentThread().id + MinecraftServer.getCommandManager().register(testCommandExecutor) + MinecraftServer.getCommandManager().executeServerCommand("unittest") + } + + Thread.sleep(250) + + 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") { + var callThreadId = 0L + var asyncThreadId = 0L + var leaveThreadId = 0L + + init { + this.setSuspendingDefaultExecutor(extension) { sender, context -> + callThreadId = Thread.currentThread().id + + withContext(Dispatchers.IO) { + asyncThreadId = Thread.currentThread().id + Thread.sleep(50) + } + + leaveThreadId = Thread.currentThread().id + } + + } + } +} diff --git a/mccoroutine-minestom-core/src/test/java/integrationtest/MinestomEventTest.kt b/mccoroutine-minestom-core/src/test/java/integrationtest/MinestomEventTest.kt new file mode 100644 index 00000000..1301d7cc --- /dev/null +++ b/mccoroutine-minestom-core/src/test/java/integrationtest/MinestomEventTest.kt @@ -0,0 +1,100 @@ +package integrationtest + +import com.github.shynixn.mccoroutine.minestom.addSuspendingListener +import helper.MockedMinestomServer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import net.minestom.server.MinecraftServer +import net.minestom.server.entity.Player +import net.minestom.server.event.player.PlayerDisconnectEvent +import net.minestom.server.event.player.PlayerLoginEvent +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.mockito.Mockito + +class MinestomEventTest { + /** + * Given a test listener + * When the test listener is register and join event is called + * then the join event should be called on the correct thread. + */ + @Test + fun registerSuspendListener_PlayerJoinEvent_ShouldCallEventWithCorrectThread() { + // Arrange + val server = MockedMinestomServer() + val extension = server.boot() + val testListener = TestListener() + var unitTestThreadId: Long + + // Act + runBlocking { + unitTestThreadId = Thread.currentThread().id + MinecraftServer.getGlobalEventHandler() + .addSuspendingListener(extension, PlayerLoginEvent::class.java) { e -> + testListener.onPlayerJoinEvent(e) + } + MinecraftServer.getGlobalEventHandler().call(PlayerLoginEvent(Mockito.mock(Player::class.java))) + } + + Thread.sleep(500) + + // Assert + Assertions.assertNotEquals(unitTestThreadId, testListener.joinEventCalledId) + Assertions.assertNotEquals(unitTestThreadId, testListener.asyncChatEventCalledId) + Assertions.assertNotEquals(unitTestThreadId, testListener.leaveThreadId) + Assertions.assertNotEquals(testListener.asyncChatEventCalledId, testListener.leaveThreadId) + } + + /** + * Given a test listener + * When the test listener is register and quit event is called + * then the quit event should be called on the correct thread. + */ + @Test + fun registerSuspendListener_PlayerQuitEvent_ShouldCallEventWithCorrectThread() { + // Arrange + val server = MockedMinestomServer() + val extension = server.boot() + val testListener = TestListener() + var unitTestThreadId: Long + + // Act + runBlocking { + unitTestThreadId = Thread.currentThread().id + MinecraftServer.getGlobalEventHandler() + .addSuspendingListener(extension, PlayerDisconnectEvent::class.java) { e -> + testListener.onPlayerQuitEvent(e) + } + MinecraftServer.getGlobalEventHandler().call(PlayerLoginEvent(Mockito.mock(Player::class.java))) + } + + Thread.sleep(500) + + // Assert + Assertions.assertNotEquals(unitTestThreadId, testListener.quitEventCalledId) + } + + class TestListener( + var joinEventCalledId: Long = 0L, + var quitEventCalledId: Long = 0L, + var asyncChatEventCalledId: Long = 0L, + var leaveThreadId: Long = 0L + ) { + + suspend fun onPlayerJoinEvent(event: PlayerLoginEvent) { + joinEventCalledId = Thread.currentThread().id + + withContext(Dispatchers.IO) { + Thread.sleep(100) + asyncChatEventCalledId = Thread.currentThread().id + } + + leaveThreadId = Thread.currentThread().id + } + + fun onPlayerQuitEvent(event: PlayerDisconnectEvent) { + quitEventCalledId = Thread.currentThread().id + } + } +} diff --git a/mccoroutine-minestom-core/src/test/java/integrationtest/MinestomExceptionTest.kt b/mccoroutine-minestom-core/src/test/java/integrationtest/MinestomExceptionTest.kt new file mode 100644 index 00000000..f56e9e60 --- /dev/null +++ b/mccoroutine-minestom-core/src/test/java/integrationtest/MinestomExceptionTest.kt @@ -0,0 +1,58 @@ +package integrationtest + +import com.github.shynixn.mccoroutine.minestom.launch +import helper.MockedMinestomServer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import net.kyori.adventure.text.logger.slf4j.ComponentLogger +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.mockito.Mockito + +class MinestomExceptionTest { + /** + * Given + * multiple extension.launch() operations + * When + * 2 launches fail and one is successful + * The + * extension scope should not fail and the 2 failures be logged. + */ + @Test + fun extensionLaunch_MultipleFailingCoroutineScopes_ShouldBeCaughtInRootScopeAndKeepExtensionScopeRunning() { + // Arrange + val testServer = MockedMinestomServer() + val logger = Mockito.mock(ComponentLogger::class.java) + var logMessageCounter = 0 + Mockito.`when`( + logger.error(Mockito.anyString(), Mockito.any()) + ).thenAnswer { + logMessageCounter++ + } + val extension = testServer.boot(logger) + var actualThreadId = 0L + var ioThreadId: Long + + // Act + runBlocking(Dispatchers.IO) { + ioThreadId = Thread.currentThread().id + + extension.launch { + throw IllegalArgumentException("UnitTestFailure!") + } + + extension.launch { + throw IllegalArgumentException("Another UnitTestFailure!") + } + + extension.launch { + actualThreadId = Thread.currentThread().id + } + } + Thread.sleep(2000) + + // Assert + Assertions.assertNotEquals(ioThreadId, actualThreadId) + Assertions.assertEquals(2, logMessageCounter) + } +} diff --git a/mccoroutine-minestom-sample/build.gradle.kts b/mccoroutine-minestom-sample/build.gradle.kts new file mode 100644 index 00000000..a8350e50 --- /dev/null +++ b/mccoroutine-minestom-sample/build.gradle.kts @@ -0,0 +1,44 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +plugins { + id("com.github.johnrengelman.shadow") version ("2.0.4") +} + +repositories { + maven { + url = uri("https://jitpack.io") + } +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } +} + +dependencies { + implementation(project(":mccoroutine-minestom-api")) + implementation(project(":mccoroutine-minestom-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") + + implementation("com.github.Minestom:Minestom:8eb089bf3e") +} + +tasks.withType { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + manifest { + attributes["Main-Class"] = "com.github.shynixn.mccoroutine.minestom.sample.MCoroutineSampleServerKt" + } +} + +tasks.withType { + dependsOn("jar") + classifier = "shadowJar" + archiveName = "$baseName.$extension" + + // Change the output folder of the plugin. + // destinationDir = File("..\\extensions") +} diff --git a/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/MCCoroutineSampleExtension.kt b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/MCCoroutineSampleExtension.kt new file mode 100644 index 00000000..95e7c29c --- /dev/null +++ b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/MCCoroutineSampleExtension.kt @@ -0,0 +1,76 @@ +package com.github.shynixn.mccoroutine.minestom.sample.extension + +import com.github.shynixn.mccoroutine.minestom.MCCoroutineExceptionEvent +import com.github.shynixn.mccoroutine.minestom.addSuspendingListener +import com.github.shynixn.mccoroutine.minestom.launch +import com.github.shynixn.mccoroutine.minestom.mcCoroutineConfiguration +import com.github.shynixn.mccoroutine.minestom.sample.extension.commandexecutor.AdminCommandExecutor +import com.github.shynixn.mccoroutine.minestom.sample.extension.impl.FakeDatabase +import com.github.shynixn.mccoroutine.minestom.sample.extension.impl.UserDataCache +import com.github.shynixn.mccoroutine.minestom.sample.extension.listener.PlayerConnectListener +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import net.minestom.server.MinecraftServer +import net.minestom.server.event.EventNode +import net.minestom.server.event.player.PlayerDisconnectEvent +import net.minestom.server.event.player.PlayerEntityInteractEvent +import net.minestom.server.event.player.PlayerLoginEvent +import net.minestom.server.extensions.Extension + +/** + * Minestom can either be customized on server level or on extension level. MCCoroutine + * implements scope handling for both types. This is the MCCoroutine Extension Scope. + */ +class MCCoroutineSampleExtension : Extension() { + /** + * Gets called on extension load. + */ + override fun initialize() { + println("[MCCoroutineSampleExtension/main] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + + // Switches into suspendable scope on startup. + this.launch { + println("[MCCoroutineSampleExtension/main] MainThread 1 Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + delay(2000) + println("[MCCoroutineSampleExtension/main] MainThread 2 Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + + withContext(Dispatchers.IO) { + println("[MCCoroutineSampleExtension/main] Simulating data load Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + Thread.sleep(500) + } + println("[MCCoroutineSampleExtension/main] MainThread 3 Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + } + + val database = FakeDatabase() + val cache = UserDataCache(this, database) + + // Extension to traditional registration. + val playerConnectListener = PlayerConnectListener(this, cache) + val rootEventNode = MinecraftServer.getGlobalEventHandler() + rootEventNode.addSuspendingListener(this, PlayerLoginEvent::class.java) { e -> + playerConnectListener.onPlayerJoinEvent(e) + } + rootEventNode.addSuspendingListener(this, PlayerDisconnectEvent::class.java) { e -> + playerConnectListener.onPlayerQuitEvent(e) + } + rootEventNode.addSuspendingListener(this, MCCoroutineExceptionEvent::class.java) { e -> + playerConnectListener.onCoroutineException(e) + } + + MinecraftServer.getCommandManager().register( + AdminCommandExecutor( + cache, + this + ) + ) + } + + /** + * Gets called on extension disable. + */ + override fun terminate() { + // Minestom requires manual disposing. + this.mcCoroutineConfiguration.disposePluginSession() + } +} diff --git a/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/commandexecutor/AdminCommandExecutor.kt b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/commandexecutor/AdminCommandExecutor.kt new file mode 100644 index 00000000..361eaec4 --- /dev/null +++ b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/commandexecutor/AdminCommandExecutor.kt @@ -0,0 +1,39 @@ +package com.github.shynixn.mccoroutine.minestom.sample.extension.commandexecutor + +import com.github.shynixn.mccoroutine.minestom.launch +import com.github.shynixn.mccoroutine.minestom.sample.extension.impl.UserDataCache +import com.github.shynixn.mccoroutine.minestom.setSuspendingDefaultExecutor +import kotlinx.coroutines.delay +import net.minestom.server.command.builder.Command +import net.minestom.server.command.builder.arguments.ArgumentType +import net.minestom.server.entity.Player +import net.minestom.server.extensions.Extension + +class AdminCommandExecutor(private val userDataCache: UserDataCache, private val extension: Extension) : + Command("mccor2") { + init { + setSuspendingDefaultExecutor(extension) { sender, context -> + println("Say hello in 1 second") + delay(1000L) + sender.sendMessage("/mccor set ") + sender.sendMessage("/mccor leave") + sender.sendMessage("/mccor exception") + } + + val killsArgument = ArgumentType.Integer("kills"); + + addSyntax({ sender, context -> + extension.launch { + if (sender is Player) { + val kills: Int = context.get(killsArgument) + println("[AdmingCommandExecutor/onCommand] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + val userData = userDataCache.getUserDataFromPlayerAsync(sender).await() + userData.amountOfPlayerKills = kills + userDataCache.saveUserData(sender) + println("[AdmingCommandExecutor/onCommand] Is ending on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + sender.sendMessage("Done!") + } + } + }) + } +} diff --git a/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/entity/UserData.kt b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/entity/UserData.kt new file mode 100644 index 00000000..caf4ceec --- /dev/null +++ b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/entity/UserData.kt @@ -0,0 +1,6 @@ +package com.github.shynixn.mccoroutine.minestom.sample.extension.entity + +class UserData { + var amountOfPlayerKills: Int = 0 + var amountOfEntityKills: Int = 0 +} diff --git a/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/impl/FakeDatabase.kt b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/impl/FakeDatabase.kt new file mode 100644 index 00000000..a910e9dc --- /dev/null +++ b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/impl/FakeDatabase.kt @@ -0,0 +1,24 @@ +package com.github.shynixn.mccoroutine.minestom.sample.extension.impl + +import com.github.shynixn.mccoroutine.minestom.sample.extension.entity.UserData +import net.minestom.server.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-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/impl/UserDataCache.kt b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/impl/UserDataCache.kt new file mode 100644 index 00000000..e6d3b9bd --- /dev/null +++ b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/impl/UserDataCache.kt @@ -0,0 +1,63 @@ +package com.github.shynixn.mccoroutine.minestom.sample.extension.impl + +import com.github.shynixn.mccoroutine.minestom.sample.extension.entity.UserData +import com.github.shynixn.mccoroutine.minestom.scope +import kotlinx.coroutines.* +import kotlinx.coroutines.future.future +import net.minestom.server.entity.Player +import net.minestom.server.extensions.Extension +import java.util.concurrent.CompletionStage + +class UserDataCache(private val extension: Extension, 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(Dispatchers.IO) { + 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(Dispatchers.IO) { + 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 extension.scope.future { + getUserDataFromPlayerAsync(player).await() + } + } +} diff --git a/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/listener/PlayerConnectListener.kt b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/listener/PlayerConnectListener.kt new file mode 100644 index 00000000..1557532c --- /dev/null +++ b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/extension/listener/PlayerConnectListener.kt @@ -0,0 +1,48 @@ +package com.github.shynixn.mccoroutine.minestom.sample.extension.listener + +import com.github.shynixn.mccoroutine.minestom.MCCoroutineExceptionEvent +import com.github.shynixn.mccoroutine.minestom.sample.extension.impl.UserDataCache +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import net.minestom.server.MinecraftServer +import net.minestom.server.event.player.PlayerDisconnectEvent +import net.minestom.server.event.player.PlayerLoginEvent +import net.minestom.server.extensions.Extension + +class PlayerConnectListener(private val extension: Extension, private val userDataCache: UserDataCache) { + /** + * Gets called on player join event. + */ + suspend fun onPlayerJoinEvent(playerJoinEvent: PlayerLoginEvent) { + 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. + */ + suspend fun onPlayerQuitEvent(playerQuitEvent: PlayerDisconnectEvent) { + println("[PlayerConnectListener/onPlayerQuitEvent] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + + val apple = withContext(Dispatchers.IO) { + println("[PlayerConnectListener/onPlayerQuitEvent] Simulate data save on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + Thread.sleep(500) + "Apple" + } + + userDataCache.clearCache(playerQuitEvent.player) + println("[PlayerConnectListener/onPlayerQuitEvent] Is ending on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + } + + fun onCoroutineException(event: MCCoroutineExceptionEvent) { + if (event.extension != extension) { + // Other extension, we do not care. + return + } + + // Print Exception + event.exception.printStackTrace() + event.isCancelled = true + } +} diff --git a/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/MCoroutineSampleServer.kt b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/MCoroutineSampleServer.kt new file mode 100644 index 00000000..946d4c17 --- /dev/null +++ b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/MCoroutineSampleServer.kt @@ -0,0 +1,88 @@ +package com.github.shynixn.mccoroutine.minestom.sample.server + +import com.github.shynixn.mccoroutine.minestom.MCCoroutineExceptionEvent +import com.github.shynixn.mccoroutine.minestom.addSuspendingListener +import com.github.shynixn.mccoroutine.minestom.launch +import com.github.shynixn.mccoroutine.minestom.sample.server.commandexecutor.AdminCommandExecutor +import com.github.shynixn.mccoroutine.minestom.sample.server.impl.FakeDatabase +import com.github.shynixn.mccoroutine.minestom.sample.server.impl.UserDataCache +import com.github.shynixn.mccoroutine.minestom.sample.server.listener.PlayerConnectListener +import com.github.shynixn.mccoroutine.minestom.suspendingHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import net.minestom.server.MinecraftServer +import net.minestom.server.coordinate.Pos +import net.minestom.server.entity.Player +import net.minestom.server.event.player.PlayerDisconnectEvent +import net.minestom.server.event.player.PlayerLoginEvent +import net.minestom.server.instance.InstanceContainer +import net.minestom.server.instance.block.Block +import net.minestom.server.instance.generator.GenerationUnit +import java.util.* + + +/** + * Minestom can either be customized on server level or on extension level. MCCoroutine + * implements scope handling for both types. This is the MCCoroutine Server Scope. + */ +fun main(args: Array) { + val minecraftServer = MinecraftServer.init() + println("[MCCoroutineSampleServer/main] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + + // Switches into suspendable scope on startup. + minecraftServer.launch { + println("[MCCoroutineSampleServer/main] MainThread 1 Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + delay(2000) + println("[MCCoroutineSampleServer/main] MainThread 2 Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + + withContext(Dispatchers.IO) { + println("[MCCoroutineSampleServer/main] Simulating data load Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + Thread.sleep(500) + } + println("[MCCoroutineSampleServer/main] MainThread 3 Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + } + + // Build a very basic instance + val instanceContainer: InstanceContainer = MinecraftServer.getInstanceManager().createInstanceContainer() + instanceContainer.setGenerator { unit: GenerationUnit -> + unit.modifier().fillHeight(0, 40, Block.STONE) + } + val globalEventHandler = MinecraftServer.getGlobalEventHandler() + globalEventHandler.addListener( + PlayerLoginEvent::class.java + ) { event: PlayerLoginEvent -> + val player: Player = event.player + player.setPermissionLevel(2) + event.setSpawningInstance(instanceContainer) + player.setRespawnPoint(Pos(0.0, 42.0, 0.0)) + } + + val database = FakeDatabase() + val cache = UserDataCache(minecraftServer, database) + + // Extension to traditional registration. + val playerConnectListener = PlayerConnectListener(minecraftServer, cache) + val rootEventNode = MinecraftServer.getGlobalEventHandler() + rootEventNode.addSuspendingListener(minecraftServer, PlayerLoginEvent::class.java) { e -> + playerConnectListener.onPlayerJoinEvent(e) + } + rootEventNode.addListener( + net.minestom.server.event.EventListener.builder(PlayerDisconnectEvent::class.java) + .suspendingHandler(minecraftServer) { e -> + playerConnectListener.onPlayerQuitEvent(e) + }.build() + ) + rootEventNode.addSuspendingListener(minecraftServer, MCCoroutineExceptionEvent::class.java) { e -> + playerConnectListener.onCoroutineException(e) + } + + + MinecraftServer.getCommandManager().register(AdminCommandExecutor(cache, minecraftServer)) + + println(MinecraftServer.VERSION_NAME) + println(UUID.randomUUID().toString()) + + minecraftServer.start("0.0.0.0", 25565) +} + diff --git a/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/commandexecutor/AdminCommandExecutor.kt b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/commandexecutor/AdminCommandExecutor.kt new file mode 100644 index 00000000..df885d42 --- /dev/null +++ b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/commandexecutor/AdminCommandExecutor.kt @@ -0,0 +1,39 @@ +package com.github.shynixn.mccoroutine.minestom.sample.server.commandexecutor + +import com.github.shynixn.mccoroutine.minestom.launch +import com.github.shynixn.mccoroutine.minestom.sample.server.impl.UserDataCache +import com.github.shynixn.mccoroutine.minestom.setSuspendingDefaultExecutor +import kotlinx.coroutines.delay +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 AdminCommandExecutor(private val userDataCache: UserDataCache, private val server: MinecraftServer) : + Command("mccor") { + init { + setSuspendingDefaultExecutor(server) { sender, context -> + println("Say hello in 1 second") + delay(1000L) + sender.sendMessage("/mccor set ") + sender.sendMessage("/mccor leave") + sender.sendMessage("/mccor exception") + } + + val killsArgument = ArgumentType.Integer("kills"); + + addSyntax({ sender, context -> + server.launch { + if (sender is Player) { + val kills: Int = context.get(killsArgument) + println("[AdmingCommandExecutor/onCommand] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + val userData = userDataCache.getUserDataFromPlayerAsync(sender).await() + userData.amountOfPlayerKills = kills + userDataCache.saveUserData(sender) + println("[AdmingCommandExecutor/onCommand] Is ending on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + sender.sendMessage("Done!") + } + } + }) + } +} diff --git a/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/entity/UserData.kt b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/entity/UserData.kt new file mode 100644 index 00000000..2c8e36fc --- /dev/null +++ b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/entity/UserData.kt @@ -0,0 +1,6 @@ +package com.github.shynixn.mccoroutine.minestom.sample.server.entity + +class UserData { + var amountOfPlayerKills: Int = 0 + var amountOfEntityKills: Int = 0 +} diff --git a/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/impl/FakeDatabase.kt b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/impl/FakeDatabase.kt new file mode 100644 index 00000000..417d9797 --- /dev/null +++ b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/impl/FakeDatabase.kt @@ -0,0 +1,24 @@ +package com.github.shynixn.mccoroutine.minestom.sample.server.impl + +import com.github.shynixn.mccoroutine.minestom.sample.server.entity.UserData +import net.minestom.server.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-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/impl/UserDataCache.kt b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/impl/UserDataCache.kt new file mode 100644 index 00000000..d07dbd61 --- /dev/null +++ b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/impl/UserDataCache.kt @@ -0,0 +1,63 @@ +package com.github.shynixn.mccoroutine.minestom.sample.server.impl + +import com.github.shynixn.mccoroutine.minestom.sample.server.entity.UserData +import com.github.shynixn.mccoroutine.minestom.scope +import kotlinx.coroutines.* +import kotlinx.coroutines.future.future +import net.minestom.server.MinecraftServer +import net.minestom.server.entity.Player +import java.util.concurrent.CompletionStage + +class UserDataCache(private val server: MinecraftServer, 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(Dispatchers.IO) { + 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(Dispatchers.IO) { + 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 server.scope.future { + getUserDataFromPlayerAsync(player).await() + } + } +} diff --git a/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/listener/PlayerConnectListener.kt b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/listener/PlayerConnectListener.kt new file mode 100644 index 00000000..3ccc2cb2 --- /dev/null +++ b/mccoroutine-minestom-sample/src/main/java/com/github/shynixn/mccoroutine/minestom/sample/server/listener/PlayerConnectListener.kt @@ -0,0 +1,42 @@ +package com.github.shynixn.mccoroutine.minestom.sample.server.listener + +import com.github.shynixn.mccoroutine.minestom.MCCoroutineExceptionEvent +import com.github.shynixn.mccoroutine.minestom.sample.server.impl.UserDataCache +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import net.minestom.server.MinecraftServer +import net.minestom.server.event.player.PlayerDisconnectEvent +import net.minestom.server.event.player.PlayerLoginEvent + +class PlayerConnectListener(private val server: MinecraftServer, private val userDataCache: UserDataCache) { + /** + * Gets called on player join event. + */ + suspend fun onPlayerJoinEvent(playerJoinEvent: PlayerLoginEvent) { + 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. + */ + suspend fun onPlayerQuitEvent(playerQuitEvent: PlayerDisconnectEvent) { + println("[PlayerConnectListener/onPlayerQuitEvent] Is starting on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + + val apple = withContext(Dispatchers.IO) { + println("[PlayerConnectListener/onPlayerQuitEvent] Simulate data save on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + Thread.sleep(500) + "Apple" + } + + userDataCache.clearCache(playerQuitEvent.player) + println("[PlayerConnectListener/onPlayerQuitEvent] Is ending on Thread:${Thread.currentThread().name}/${Thread.currentThread().id}") + } + + fun onCoroutineException(event: MCCoroutineExceptionEvent) { + // Print Exception + event.exception.printStackTrace() + event.isCancelled = true + } +} diff --git a/mccoroutine-minestom-sample/src/main/resources/extension.json b/mccoroutine-minestom-sample/src/main/resources/extension.json new file mode 100644 index 00000000..b67b934d --- /dev/null +++ b/mccoroutine-minestom-sample/src/main/resources/extension.json @@ -0,0 +1,5 @@ +{ + "entrypoint": "com.github.shynixn.mccoroutine.minestom.sample.extension.MCCoroutineSampleExtension", + "name": "MCCoroutineSampleExtension", + "version": "2.10.0" +} diff --git a/mccoroutine-sponge-api/build.gradle.kts b/mccoroutine-sponge-api/build.gradle.kts index b3e4d990..bceacc21 100644 --- a/mccoroutine-sponge-api/build.gradle.kts +++ b/mccoroutine-sponge-api/build.gradle.kts @@ -7,5 +7,5 @@ repositories { dependencies { compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") compileOnly("org.spongepowered:spongeapi:7.2.0") - testCompile("org.spongepowered:spongeapi:7.2.0") + testImplementation("org.spongepowered:spongeapi:7.2.0") } diff --git a/mccoroutine-sponge-core/build.gradle.kts b/mccoroutine-sponge-core/build.gradle.kts index 3d144737..f8c8a581 100644 --- a/mccoroutine-sponge-core/build.gradle.kts +++ b/mccoroutine-sponge-core/build.gradle.kts @@ -9,11 +9,11 @@ dependencies { compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") - testCompile("org.apache.logging.log4j:log4j-api:2.17.2") - testCompile("it.unimi.dsi:fastutil:7.0.13") - testCompile("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") - testCompile(files("lib/SpongeCommon.jar")) + testImplementation("org.apache.logging.log4j:log4j-api:2.17.2") + testImplementation("it.unimi.dsi:fastutil:7.0.13") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") + testImplementation(files("lib/SpongeCommon.jar")) compileOnly("org.spongepowered:spongeapi:7.2.0") - testCompile("org.spongepowered:spongeapi:7.2.0") + testImplementation("org.spongepowered:spongeapi:7.2.0") } diff --git a/mccoroutine-sponge-core/src/test/java/integrationtest/SpongeEventPriorityTest.kt b/mccoroutine-sponge-core/src/test/java/integrationtest/SpongeEventPriorityTest.kt index 27c9fe4b..9cff25d8 100644 --- a/mccoroutine-sponge-core/src/test/java/integrationtest/SpongeEventPriorityTest.kt +++ b/mccoroutine-sponge-core/src/test/java/integrationtest/SpongeEventPriorityTest.kt @@ -9,13 +9,13 @@ import helper.MockedSpongeServer import kotlinx.coroutines.delay import kotlinx.coroutines.joinAll import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.mockito.Mockito import org.spongepowered.api.Sponge import org.spongepowered.api.event.Listener import org.spongepowered.api.event.Order import org.spongepowered.api.event.network.ClientConnectionEvent -import kotlin.test.assertEquals class SpongeEventPriorityTest { /** @@ -46,9 +46,9 @@ class SpongeEventPriorityTest { val actualResult = classUnderTest.resultList // Assert - assertEquals(2, actualResult[0]) - assertEquals(3, actualResult[1]) - assertEquals(1, actualResult[2]) + Assertions.assertEquals(2, actualResult[0]) + Assertions.assertEquals(3, actualResult[1]) + Assertions.assertEquals(1, actualResult[2]) } /** @@ -71,7 +71,8 @@ class SpongeEventPriorityTest { // Act runBlocking { try { - Sponge.getEventManager().postSuspending(clientConnectionEvent, plugin, EventExecutionType.Consecutive).joinAll() + Sponge.getEventManager().postSuspending(clientConnectionEvent, plugin, EventExecutionType.Consecutive) + .joinAll() } catch (e: Exception) { e.toString() } @@ -79,9 +80,9 @@ class SpongeEventPriorityTest { val actualResult = classUnderTest.resultList // Assert - assertEquals(1, actualResult[0]) - assertEquals(2, actualResult[1]) - assertEquals(3, actualResult[2]) + Assertions.assertEquals(1, actualResult[0]) + Assertions.assertEquals(2, actualResult[1]) + Assertions.assertEquals(3, actualResult[2]) } private class TestEventListener { diff --git a/mccoroutine-sponge-core/src/test/java/integrationtest/SpongeEventTest.kt b/mccoroutine-sponge-core/src/test/java/integrationtest/SpongeEventTest.kt index 96203a43..b4d3f699 100644 --- a/mccoroutine-sponge-core/src/test/java/integrationtest/SpongeEventTest.kt +++ b/mccoroutine-sponge-core/src/test/java/integrationtest/SpongeEventTest.kt @@ -6,13 +6,12 @@ import helper.MockedSpongeServer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.mockito.Mockito import org.spongepowered.api.Sponge import org.spongepowered.api.event.Listener import org.spongepowered.api.event.network.ClientConnectionEvent -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals class SpongeEventTest { /** @@ -34,9 +33,9 @@ class SpongeEventTest { } // Assert - assertEquals(server.mainThreadId, testListener.joinEventCalledId) - assertNotEquals(server.mainThreadId, testListener.asyncCalledId) - assertEquals(server.mainThreadId, testListener.leaveId) + Assertions.assertEquals(server.mainThreadId, testListener.joinEventCalledId) + Assertions.assertNotEquals(server.mainThreadId, testListener.asyncCalledId) + Assertions.assertEquals(server.mainThreadId, testListener.leaveId) } /** @@ -58,7 +57,7 @@ class SpongeEventTest { } // Assert - assertEquals(server.mainThreadId, testListener.quitEventCalledId) + Assertions.assertEquals(server.mainThreadId, testListener.quitEventCalledId) } class TestListener( diff --git a/mccoroutine-sponge-core/src/test/java/unittest/SpongeMCCoroutineTest.kt b/mccoroutine-sponge-core/src/test/java/unittest/SpongeMCCoroutineTest.kt index cba7be91..5b814379 100644 --- a/mccoroutine-sponge-core/src/test/java/unittest/SpongeMCCoroutineTest.kt +++ b/mccoroutine-sponge-core/src/test/java/unittest/SpongeMCCoroutineTest.kt @@ -1,10 +1,10 @@ package unittest import com.github.shynixn.mccoroutine.sponge.impl.MCCoroutineImpl +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.mockito.Mockito import org.spongepowered.api.plugin.PluginContainer -import kotlin.test.assertEquals class SpongeMCCoroutineTest { /** @@ -23,7 +23,7 @@ class SpongeMCCoroutineTest { val session2 = classUnderTest.getCoroutineSession(plugin) // Assert - assertEquals(session1, session2) + Assertions.assertEquals(session1, session2) } private fun createWithDependencies(): MCCoroutineImpl { diff --git a/mccoroutine-sponge-sample/build.gradle.kts b/mccoroutine-sponge-sample/build.gradle.kts index 338e711f..40ee0114 100644 --- a/mccoroutine-sponge-sample/build.gradle.kts +++ b/mccoroutine-sponge-sample/build.gradle.kts @@ -35,5 +35,5 @@ dependencies { compileOnly("com.google.guava:guava:23.0") compileOnly("org.spongepowered:spongeapi:7.2.0") - testCompile("org.spongepowered:spongeapi:7.2.0") + testImplementation("org.spongepowered:spongeapi:7.2.0") } diff --git a/mccoroutine-sponge-sample/src/main/resources/mcmod.info b/mccoroutine-sponge-sample/src/main/resources/mcmod.info index fb1d05c3..0d43db31 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.9.0", + "version": "2.10.0", "description": "MCCoroutineSample is sample plugin to use MCCoroutine in Sponge.", "authorList": [ "Shynixn" diff --git a/mccoroutine-velocity-api/build.gradle.kts b/mccoroutine-velocity-api/build.gradle.kts index 83253acf..080d79bc 100644 --- a/mccoroutine-velocity-api/build.gradle.kts +++ b/mccoroutine-velocity-api/build.gradle.kts @@ -7,5 +7,5 @@ repositories { dependencies { compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") compileOnly("com.velocitypowered:velocity-api:3.0.1") - testCompile("com.velocitypowered:velocity-api:3.0.1") + testImplementation("com.velocitypowered:velocity-api:3.0.1") } diff --git a/mccoroutine-velocity-core/build.gradle b/mccoroutine-velocity-core/build.gradle index a836aaac..de7e4fff 100644 --- a/mccoroutine-velocity-core/build.gradle +++ b/mccoroutine-velocity-core/build.gradle @@ -19,7 +19,7 @@ dependencies { compileOnly("com.velocitypowered:velocity-api:3.0.1") compileOnly("org.apache.logging.log4j:log4j-core:2.17.2") - testCompile("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") - testCompile(files("lib/velocity.jar")) - testCompile("com.velocitypowered:velocity-api:3.0.1") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9") + testImplementation(files("lib/velocity.jar")) + testImplementation("com.velocitypowered:velocity-api:3.0.1") } diff --git a/mccoroutine-velocity-sample/build.gradle b/mccoroutine-velocity-sample/build.gradle index b17b335e..128912c2 100644 --- a/mccoroutine-velocity-sample/build.gradle +++ b/mccoroutine-velocity-sample/build.gradle @@ -37,5 +37,5 @@ dependencies { compileOnly("com.velocitypowered:velocity-api:3.0.1") kapt("com.velocitypowered:velocity-api:3.0.1") - testCompile("com.velocitypowered:velocity-api:3.0.1") + testImplementation("com.velocitypowered:velocity-api:3.0.1") } diff --git a/settings.gradle.kts b/settings.gradle.kts index fa9b3ba1..81b4acfa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,6 +9,10 @@ include("mccoroutine-bungeecord-api") include("mccoroutine-bungeecord-core") include("mccoroutine-bungeecord-sample") +include("mccoroutine-minestom-api") +include("mccoroutine-minestom-core") +include("mccoroutine-minestom-sample") + include("mccoroutine-sponge-api") include("mccoroutine-sponge-core") include("mccoroutine-sponge-sample")