Skip to content

Commit

Permalink
Merge pull request #12 from Shynixn/development
Browse files Browse the repository at this point in the history
Merge changes to master.
  • Loading branch information
Shynixn authored Nov 12, 2020
2 parents fb2d0b1 + a202f0c commit f46fff7
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 12 deletions.
101 changes: 95 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<UserData> {
// Open a coroutine scope because we want to manage waiting with async
return coroutineScope {
// Still on bukkit primary thread.
Expand All @@ -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 a traditional Java 8 CompletionStage as return value.
@EventHandler
public void onPlayerInteractEvent(PlayerInteractAtEntityEvent event) {
CompletionStage<UserData> 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<UUID, Deferred<UserData>>()

/**
* 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<UserData> {
return plugin.scope.future {
getUserDataFromPlayerAsync(player).await()
}
}

/**
* Gets the user data from the player.
*/
suspend fun getUserDataFromPlayerAsync(player: Player): Deferred<UserData> {
return coroutineScope {
if (!cache.containsKey(player.uniqueId)) {
cache[player.uniqueId] = async(Dispatchers.async) {
fakeDatabase.getUserDataFromPlayer(player)
}
}

cache[player.uniqueId]!!
}
}
}
Expand Down Expand Up @@ -413,6 +495,12 @@ class PlaceHolderApiConnector(private val cache : UserDataCache) {
<version>1.x.x</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-jdk8</artifactId>
<version>1.x.x</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
Expand All @@ -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")
}
```
Expand Down
3 changes: 2 additions & 1 deletion mccoroutine-bukkit-sample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ tasks.withType<ShadowJar> {
archiveName = "$baseName-$version.$extension"

// Change the output folder of the plugin.
// destinationDir = File("D:\\Benutzer\\Temp\\plugins")
destinationDir = File("D:\\Benutzer\\Temp\\plugins")
}

repositories {
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Player, Deferred<UserData>>()
Expand All @@ -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<UserData> {
return coroutineScope {
if (!cache.containsKey(player)) {
cache[player] = async(plugin.asyncDispatcher) {
Expand All @@ -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<UserData> {
return plugin.scope.future {
getUserDataFromPlayerAsync(player).await()
}
}
}
Original file line number Diff line number Diff line change
@@ -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<UserData> 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;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
}
Expand Down

0 comments on commit f46fff7

Please sign in to comment.