From 62b9c19c7501ebebe4f480d61b01c7561682d4f3 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Thu, 12 Nov 2020 12:33:26 +0100 Subject: [PATCH 1/2] #11 Changed to deferred async value. --- README.md | 101 ++++++++++++++++-- mccoroutine-bukkit-sample/build.gradle.kts | 1 + .../sample/MCCoroutineSamplePlugin.kt | 2 + .../commandexecutor/AdminCommandExecutor.kt | 2 +- .../mccoroutine/sample/impl/UserDataCache.kt | 28 ++++- .../listener/EntityInteractListener.java | 34 ++++++ .../sample/listener/PlayerConnectListener.kt | 3 +- 7 files changed, 160 insertions(+), 11 deletions(-) create mode 100644 mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/listener/EntityInteractListener.java diff --git a/README.md b/README.md index 2c965429..e37fd4af 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,7 @@ class UserDataCache(private val plugin: Plugin, private val fakeDatabase: FakeDa /** * Gets the user data from the player. */ - suspend fun getUserDataFromPlayer(player: Player): UserData { + suspend fun getUserDataFromPlayerAsync(player: Player): Deferred { // Open a coroutine scope because we want to manage waiting with async return coroutineScope { // Still on bukkit primary thread. @@ -353,11 +353,93 @@ class UserDataCache(private val plugin: Plugin, private val fakeDatabase: FakeDa } } - // Suspends the calling thread until the data has been loaded. - // If the data has already been loaded, it returns immediately the result without suspension. - // It is possible that the bukkit primary thread is suspended multiple times here and continuous each time once the value - // is there. - cache[player.uniqueId]!!.await() + // Returns the Deferred UserData. It may already contain the result, it may still be running + // in the background. When calling .await() on this value we can suspend the function until it is ready. + cache[player.uniqueId]!! + } + } +} + +class PlayerConnectListener(private val userDataCache: UserDataCache) : Listener { + /** + Gets called on player join. May be called multiple times for the same player + in a short time period if the player continously quits and joins the server. + + However, as the same deferred instance is returned every time we can suspend + multiple times here without problems. It also only creates only 1 single database request. + */ + @EventHandler + suspend fun onPlayerJoinEvent(playerJoinEvent: PlayerJoinEvent) { + val userData = userDataCache.getUserDataFromPlayerAsync(playerJoinEvent.player).await() + } +} + +``` + +#### How to call a suspend function from Java + +```java +public class EntityInteractListener implements Listener { + private final UserDataCache userDataCache; + + public EntityInteractListener(UserDataCache userDataCache) { + this.userDataCache = userDataCache; + } + + // We cannot call getUserDataFromPlayerAsync directly instead we assume, + // we get traditional Java 8 CompletionStage as return value. + @EventHandler + public void onPlayerInteractEvent(PlayerInteractAtEntityEvent event) { + CompletionStage future = this.userDataCache.getUserDataFromPlayer(event.getPlayer()); + future.thenAccept(useData -> { + // Got data. + }).exceptionally(throwable -> { + throwable.printStackTrace(); + return null; + }); + } +} +``` + +```kotlin +class UserDataCache(private val plugin: Plugin, private val fakeDatabase: FakeDatabase) { + private val cache = HashMap>() + + /** + * Clears the player cache. + */ + fun clearCache(player: Player) { + cache.remove(player.uniqueId) + } + + /** + * Gets the user data from the player. + * + * This method is only useful if you plan to access suspend functions from Java. It + * is not possible to call suspend functions directly from java, so we need to + * wrap it into a Java 8 CompletionStage. + * + * This might be useful if you plan to provide a Developer Api for your plugin as other + * plugins may be written in Java or if you have got Java code in your plugin. + */ + fun getUserDataFromPlayer(player: Player): CompletionStage { + return plugin.scope.future { + getUserDataFromPlayerAsync(player).await() + } + } + + /** + * Gets the user data from the player. + */ + suspend fun getUserDataFromPlayerAsync(player: Player): Deferred { + return coroutineScope { + if (!cache.containsKey(player.uniqueId)) { + cache[player.uniqueId] = async(Dispatchers.async) { + fakeDatabase.getUserDataFromPlayer(player) + } + } + + cache[player.uniqueId]!! } } } @@ -413,6 +495,12 @@ class PlaceHolderApiConnector(private val cache : UserDataCache) { 1.x.x compile + + org.jetbrains.kotlinx + kotlinx-coroutines-jdk8 + 1.x.x + compile + org.jetbrains.kotlin kotlin-reflect @@ -426,6 +514,7 @@ class PlaceHolderApiConnector(private val cache : UserDataCache) { dependencies { implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:0.0.4") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.x.x") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.x.x") implementation("org.jetbrains.kotlin:kotlin-reflect:1.x.x") } ``` diff --git a/mccoroutine-bukkit-sample/build.gradle.kts b/mccoroutine-bukkit-sample/build.gradle.kts index 3cce157e..11c57568 100644 --- a/mccoroutine-bukkit-sample/build.gradle.kts +++ b/mccoroutine-bukkit-sample/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { 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("org.jetbrains.kotlin:kotlin-reflect:1.3.72") compileOnly("org.spigotmc:spigot-api:1.16.3-R0.1-SNAPSHOT") diff --git a/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/MCCoroutineSamplePlugin.kt b/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/MCCoroutineSamplePlugin.kt index ef081587..716f081b 100644 --- a/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/MCCoroutineSamplePlugin.kt +++ b/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/MCCoroutineSamplePlugin.kt @@ -4,6 +4,7 @@ import com.github.shynixn.mccoroutine.registerSuspendingEvents import com.github.shynixn.mccoroutine.sample.commandexecutor.AdminCommandExecutor import com.github.shynixn.mccoroutine.sample.impl.FakeDatabase import com.github.shynixn.mccoroutine.sample.impl.UserDataCache +import com.github.shynixn.mccoroutine.sample.listener.EntityInteractListener import com.github.shynixn.mccoroutine.sample.listener.PlayerConnectListener import com.github.shynixn.mccoroutine.setSuspendingExecutor import org.bukkit.plugin.java.JavaPlugin @@ -19,6 +20,7 @@ class MCCoroutineSamplePlugin : JavaPlugin() { // Extension to traditional registration. server.pluginManager.registerSuspendingEvents(PlayerConnectListener(this, cache), this) + server.pluginManager.registerSuspendingEvents(EntityInteractListener(cache), this); this.getCommand("mccor")!!.setSuspendingExecutor(AdminCommandExecutor(cache)) } } diff --git a/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/commandexecutor/AdminCommandExecutor.kt b/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/commandexecutor/AdminCommandExecutor.kt index 0cc2a3a1..ef9031aa 100644 --- a/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/commandexecutor/AdminCommandExecutor.kt +++ b/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/commandexecutor/AdminCommandExecutor.kt @@ -23,7 +23,7 @@ class AdminCommandExecutor(private val userDataCache: UserDataCache) : Suspendin val otherPlayer = Bukkit.getPlayer(playerName)!! println("[AdminCommandExecutor] Is starting on Primary Thread: " + Bukkit.isPrimaryThread()) - val userData = userDataCache.getUserDataFromPlayer(otherPlayer) + val userData = userDataCache.getUserDataFromPlayerAsync(otherPlayer).await() userData.amountOfPlayerKills = playerKills userDataCache.saveUserData(otherPlayer) println("[AdminCommandExecutor] Is ending on Primary Thread: " + Bukkit.isPrimaryThread()) diff --git a/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/impl/UserDataCache.kt b/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/impl/UserDataCache.kt index 9e56634a..c640c3a4 100644 --- a/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/impl/UserDataCache.kt +++ b/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/impl/UserDataCache.kt @@ -2,10 +2,16 @@ package com.github.shynixn.mccoroutine.sample.impl import com.github.shynixn.mccoroutine.asyncDispatcher import com.github.shynixn.mccoroutine.sample.entity.UserData -import kotlinx.coroutines.* +import com.github.shynixn.mccoroutine.scope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.future.future +import kotlinx.coroutines.withContext import org.bukkit.Bukkit import org.bukkit.entity.Player import org.bukkit.plugin.Plugin +import java.util.concurrent.CompletionStage class UserDataCache(private val plugin: Plugin, private val fakeDatabase: FakeDatabase) { private val cache = HashMap>() @@ -30,7 +36,7 @@ class UserDataCache(private val plugin: Plugin, private val fakeDatabase: FakeDa /** * Gets the user data from the player. */ - suspend fun getUserDataFromPlayer(player: Player): UserData { + suspend fun getUserDataFromPlayerAsync(player: Player): Deferred { return coroutineScope { if (!cache.containsKey(player)) { cache[player] = async(plugin.asyncDispatcher) { @@ -39,7 +45,23 @@ class UserDataCache(private val plugin: Plugin, private val fakeDatabase: FakeDa } } println("[Cache] is downloading waiting on Primary Thread: " + Bukkit.isPrimaryThread()) - cache[player]!!.await() + cache[player]!! + } + } + + /** + * Gets the user data from the player. + * + * This method is only useful if you plan to access suspend functions from Java. It + * is not possible to call suspend functions directly from java, so we need to + * wrap it into a Java 8 CompletionStage. + * + * This might be useful if you plan to provide a Developer Api for your plugin as other + * plugins may be written in Java or if you have got Java code in your plugin. + */ + fun getUserDataFromPlayer(player: Player): CompletionStage { + return plugin.scope.future { + getUserDataFromPlayerAsync(player).await() } } } diff --git a/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/listener/EntityInteractListener.java b/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/listener/EntityInteractListener.java new file mode 100644 index 00000000..515fe115 --- /dev/null +++ b/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/listener/EntityInteractListener.java @@ -0,0 +1,34 @@ +package com.github.shynixn.mccoroutine.sample.listener; + +import com.github.shynixn.mccoroutine.sample.entity.UserData; +import com.github.shynixn.mccoroutine.sample.impl.UserDataCache; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerInteractAtEntityEvent; + +import java.util.concurrent.CompletionStage; + +/** + * This is a Java example how to interact with suspend functions from Java. + */ +public class EntityInteractListener implements Listener { + private final UserDataCache userDataCache; + + public EntityInteractListener(UserDataCache userDataCache) { + this.userDataCache = userDataCache; + } + + @EventHandler + public void onPlayerInteractEvent(PlayerInteractAtEntityEvent event) { + System.out.println("[EntityInteractListener] Is starting on Primary Thread: " + Bukkit.isPrimaryThread()); + + CompletionStage future = this.userDataCache.getUserDataFromPlayer(event.getPlayer()); + future.thenAccept(useData -> { + System.out.println("[EntityInteractListener] Is ending on Primary Thread: " + Bukkit.isPrimaryThread()); + }).exceptionally(throwable -> { + throwable.printStackTrace(); + return null; + }); + } +} diff --git a/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/listener/PlayerConnectListener.kt b/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/listener/PlayerConnectListener.kt index c7e50c59..a2c43877 100644 --- a/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/listener/PlayerConnectListener.kt +++ b/mccoroutine-bukkit-sample/src/main/java/com/github/shynixn/mccoroutine/sample/listener/PlayerConnectListener.kt @@ -2,6 +2,7 @@ package com.github.shynixn.mccoroutine.sample.listener import com.github.shynixn.mccoroutine.asyncDispatcher import com.github.shynixn.mccoroutine.sample.impl.UserDataCache +import kotlinx.coroutines.future.await import kotlinx.coroutines.withContext import org.bukkit.Bukkit import org.bukkit.Material @@ -19,7 +20,7 @@ class PlayerConnectListener(private val plugin: Plugin, private val userDataCach @EventHandler suspend fun onPlayerJoinEvent(playerJoinEvent: PlayerJoinEvent) { println("[PlayerConnectListener-Join] Is starting on Primary Thread: " + Bukkit.isPrimaryThread()) - val userData = userDataCache.getUserDataFromPlayer(playerJoinEvent.player) + val userData = userDataCache.getUserDataFromPlayerAsync(playerJoinEvent.player).await() println("[PlayerConnectListener-Join] " + playerJoinEvent.player.name + " joined the server. KillCount [${userData.amountOfPlayerKills}].") println("[PlayerConnectListener-Join] Is ending on Primary Thread: " + Bukkit.isPrimaryThread()) } From a202f0c07076f951b80e7a52b1b45a59eab6f430 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Thu, 12 Nov 2020 12:38:39 +0100 Subject: [PATCH 2/2] #11 Fixed typo. --- README.md | 2 +- mccoroutine-bukkit-sample/build.gradle.kts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e37fd4af..23e6536e 100644 --- a/README.md +++ b/README.md @@ -387,7 +387,7 @@ public class EntityInteractListener implements Listener { } // We cannot call getUserDataFromPlayerAsync directly instead we assume, - // we get traditional Java 8 CompletionStage as return value. + // we get a traditional Java 8 CompletionStage as return value. @EventHandler public void onPlayerInteractEvent(PlayerInteractAtEntityEvent event) { CompletionStage future = this.userDataCache.getUserDataFromPlayer(event.getPlayer()); diff --git a/mccoroutine-bukkit-sample/build.gradle.kts b/mccoroutine-bukkit-sample/build.gradle.kts index 11c57568..0d080ef7 100644 --- a/mccoroutine-bukkit-sample/build.gradle.kts +++ b/mccoroutine-bukkit-sample/build.gradle.kts @@ -15,7 +15,7 @@ tasks.withType { archiveName = "$baseName-$version.$extension" // Change the output folder of the plugin. - // destinationDir = File("D:\\Benutzer\\Temp\\plugins") + destinationDir = File("D:\\Benutzer\\Temp\\plugins") } repositories {