Skip to content

Commit

Permalink
Merge pull request #114 from Shynixn/development
Browse files Browse the repository at this point in the history
Merge changes to master --release
  • Loading branch information
Shynixn authored May 4, 2024
2 parents ca3dd7f + c120f64 commit fea39be
Show file tree
Hide file tree
Showing 21 changed files with 188 additions and 86 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ tasks.register("printVersion") {

subprojects {
group 'com.github.shynixn.mccoroutine'
version '2.15.0'
version '2.16.0'

sourceCompatibility = 1.8

Expand Down
17 changes: 12 additions & 5 deletions docs/wiki/docs/commandexecutor.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,22 @@ plugins.

class PlayerDataCommandExecutor(private val database: Database) : SuspendingCommandExecutor {
override suspend fun onCommand(sender: CommandSender, command: Command, label: String, args: Array<out String>): Boolean {
// In Folia, this will be the global region thread, or entity execution thread.
// In Bukkit, this will be the main thread.

if (sender !is Player) {
return false
}

if (args.size == 2 && args[0].equals("rename", true)) {
val name = args[1]
val playerData = database.getDataFromPlayer(sender)
playerData.name = name
database.saveData(sender, playerData)
withContext(plugin.mainDispatcher) {
// Make sure you switch to your plugin main thread before you do anything in your plugin.
val playerData = database.getDataFromPlayer(sender)
playerData.name = name
database.saveData(sender, playerData)
}

return true
}

Expand Down Expand Up @@ -312,14 +319,14 @@ plugins.
private val database = Database()

override suspend fun onEnableAsync() {
// Minecraft Main Thread
// Global Region Thread.
database.createDbIfNotExist()
server.pluginManager.registerSuspendingEvents(PlayerDataListener(database), this)
getCommand("playerdata")!!.setSuspendingExecutor(PlayerDataCommandExecutor(database))
}

override suspend fun onDisableAsync() {
// Minecraft Main Thread
// Global Region Thread.
}
}
````
Expand Down
76 changes: 48 additions & 28 deletions docs/wiki/docs/coroutine.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
# 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.
When starting with [Coroutines in Kotlin](https://kotlinlang.org/docs/coroutines-basics.html), you may wonder how
you can use them for minecraft plugins and mods. This guide introduces concepts and a production ready API you can use, to start
adding coroutines to your project.

!!! note "Important"
Make sure you have already installed MCCoroutine. See [Installation](/gettingstarted) for details.

### Starting a coroutine

In order to start coroutine You may also encounter the function
``runBlocking`` because it makes sense for certain scenarios such as unittest.
However, keep in mind to **avoid** using ``runblocking`` in any of your plugins.
In order to start a coroutine, you can use the provided ``plugin.launch {}`` extension method. This is safe to be called
anywhere in your plugin except in onDisable where you need to use ``runBlocking``. However, keep in mind to **avoid** using ``runblocking`` anywhere else in any of your plugins.

* To enter a coroutine **anywhere** in your code at any time:

Expand Down Expand Up @@ -62,29 +61,45 @@ However, keep in mind to **avoid** using ``runblocking`` in any of your plugins.

=== "Folia"

As Folia brings multithreading to Paper based servers, threading becomes more complicated and MCCoroutine requires you to think
everytime you call plugin.launch. In Bukkit based servers, MCCoroutine can assume the correct thread automatically and optimise ticking. (e.g.
not sending a task to the scheduler if you are already on the main thread).

In Folia, there are many threadpools (explained below) and we do not have a main thread.
As Folia brings multithreading to Paper based servers, threading becomes a lore more complicated for plugin developers.

!!! note "Important"
You can run mccoroutine-folia in standard Bukkit servers as well. MCCoroutine automatically falls back to the standard Bukkit
scheduler if the Folia schedulers are not found and the rules for mccoroutine-bukkit start to apply.

!!! note "Important"
If you have been using mccoroutine for Bukkit before, you have to perform some restructuring in your plugin. **Simply changing the imports is not enough.**
``plugin.launch {}`` works differently in Folia compared to Bukkit.

First, it is important to understand that Folia does not have a server main thread. In order to access minecraft resources you need to use the correct thread for
a given resource. For an entity, you need to use the currently assigned thread for that entity. MCCoroutine provides dispatchers for each of these usecases and
automatically falls back to the matching dispatchers if you are on a Bukkit server instead of a Folia server.

However, this does not solve the problem of accessing our own data in our plugins. We do not have a main thread, so we could try accessing our data on the incoming
thread. However, sometimes you have to make sure only 1 thread is accessing a resource at a time. This is important for ordering events and avoiding concurrency exceptions.
Concurrent collections can help with that but you may still need synchronize access in other places.

As a solution, MCCoroutine proposes that each plugin gets their own "main thread" and corresponding "mainDispatcher". It is intended to execute all the stuff the plugin is going to do.
For minecraft actions, like teleporting a player or manipulating an entity. You simply excute them in a sub context and return back to your personal main thread. This
concepts result into the following code.

```kotlin
import com.github.shynixn.mccoroutine.folia.launch
import org.bukkit.plugin.Plugin

fun foo(entity : Entity) {
// The plugin.entityDispatcher(entity) parameter ensures, that we end up on the scheduler for the entity in the specific region if we suspend
// inside the plugin.launch scope. (e.g. using delay)
// The CoroutineStart.UNDISPATCHED ensures, that we enter plugin.launch scope without any delay on the current thread.
// You are responsible to ensure that you are on the correct thread pool (in this case the thread pool for the entity), if you pass CoroutineStart.UNDISPATCHED.
// This is automatically the case if you use plugin.launch{} in events or commands. You can simply use CoroutineStart.UNDISPATCHED here.
// If you use CoroutineStart.DEFAULT, the plugin.launch scope is entered in the next scheduler tick.
plugin.launch(plugin.entityDispatcher(entity), CoroutineStart.UNDISPATCHED) {
// In this case this will be the correct thread for the given entity, if the thread was correct before calling plugin.launch.
plugin.launch { // or plugin.launch(plugin.mainDispatcher) {}
// Your plugin main thread. If you have already been on your plugin main thread, this scope is entered immidiatly.
// Regardless if your are on bukkit or on folia, this is your personal thread and you must not call bukkit methods on it.
// Now perform some data access on your plugin data like accessing a repository.
val storedEntityDataInDatabase = database.get()
// Apply the data on the entity thread using the entityDispatcher.
// The plugin.entityDispatcher(entity) parameter ensures, that we end up on the scheduler for the entity in the specific region.
withContext(plugin.entityDispatcher(entity)) {
// In Folia, this will be the correct thread for the given entity.
// In Bukkit, this will be the main thread.
}
}
}
```
Expand Down Expand Up @@ -306,7 +321,8 @@ A dispatcher determines what thread or threads the corresponding coroutine uses

In Folia, MCCoroutine offers 4 custom dispatchers.

* globalRegion (Allows to execute coroutines on the global region. e.g. Global Game Rules)
* mainDispatcher (Your personal plugin main thread, allows to execute coroutines on it)
* globalRegionDispatcher (Allows to execute coroutines on the global region. e.g. Global Game Rules)
* regionDispatcher (Allows to execute coroutines on a specific location in a world)
* entityDispatcher (Allows to execute coroutines on a specific entity)
* asyncDispatcher (Allows to execute coroutines on the async thread pool)
Expand All @@ -315,16 +331,20 @@ A dispatcher determines what thread or threads the corresponding coroutine uses

```kotlin
fun foo(location: Location)) {
plugin.launch(plugin.regionDispatcher(location), CoroutineStart.UNDISPATCHED) {
// The correct thread for the given location without delay, if the thread was correct before calling plugin.launch.
plugin.launch {
// Always make your you are on your personal plugin main thread.

val result = withContext(Dispatchers.IO) {
// Perform operations asynchronously.
"Playxer is Max"
val resultBlockType = withContext(plugin.regionDispatcher(location)) {
// In Folia, this will be the correct thread for the given location
// In Bukkit, this will be the main thread.
getTypeOfBlock()
}

myBlockTypeList.add(resultBlockType)

withContext(plugin.asyncDispatcher) {
// save myBlockTypeList to file.
}

// The correct thread for the given location.
println(result) // Prints 'Player is Max'
}
}
```
Expand Down
36 changes: 18 additions & 18 deletions docs/wiki/docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,62 +8,62 @@ 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.15.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:2.15.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:2.16.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:2.16.0")
}
```

=== "BungeeCord"

```groovy
dependencies {
implementation("com.github.shynixn.mccoroutine:mccoroutine-bungeecord-api:2.15.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-bungeecord-core:2.15.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-bungeecord-api:2.16.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-bungeecord-core:2.16.0")
}
```

=== "Fabric"

```groovy
dependencies {
implementation("com.github.shynixn.mccoroutine:mccoroutine-fabric-api:2.15.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-fabric-core:2.15.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-fabric-api:2.16.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-fabric-core:2.16.0")
}
```

=== "Folia"

```groovy
dependencies {
implementation("com.github.shynixn.mccoroutine:mccoroutine-folia-api:2.15.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-folia-core:2.15.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-folia-api:2.16.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-folia-core:2.16.0")
}
```

=== "Minestom"

```groovy
dependencies {
implementation("com.github.shynixn.mccoroutine:mccoroutine-minestom-api:2.15.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-minestom-core:2.15.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-minestom-api:2.16.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-minestom-core:2.16.0")
}
```

=== "Sponge"

```groovy
dependencies {
implementation("com.github.shynixn.mccoroutine:mccoroutine-sponge-api:2.15.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-sponge-core:2.15.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-sponge-api:2.16.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-sponge-core:2.16.0")
}
```

=== "Velocity"

```groovy
dependencies {
implementation("com.github.shynixn.mccoroutine:mccoroutine-velocity-api:2.15.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-velocity-core:2.15.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-velocity-api:2.16.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-velocity-core:2.16.0")
}
```

Expand All @@ -87,17 +87,17 @@ dependencies {
**plugin.yml**
```yaml
libraries:
- com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:2.15.0
- com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:2.15.0
- com.github.shynixn.mccoroutine:mccoroutine-bukkit-api:2.16.0
- com.github.shynixn.mccoroutine:mccoroutine-bukkit-core:2.16.0
```

=== "Folia"

**plugin.yml**
```yaml
libraries:
- com.github.shynixn.mccoroutine:mccoroutine-folia-api:2.15.0
- com.github.shynixn.mccoroutine:mccoroutine-folia-core:2.15.0
- com.github.shynixn.mccoroutine:mccoroutine-folia-api:2.16.0
- com.github.shynixn.mccoroutine:mccoroutine-folia-core:2.16.0
```


Expand Down
19 changes: 12 additions & 7 deletions docs/wiki/docs/listener.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,17 +110,22 @@ suspendable functions). You can mix suspendable and non suspendable functions in
class PlayerDataListener(private val database: Database) : Listener {
@EventHandler
suspend fun onPlayerJoinEvent(event: PlayerJoinEvent) {
val player = event.player
val playerData = database.getDataFromPlayer(player)
playerData.name = player.name
playerData.lastJoinDate = Date()
database.saveData(player, playerData)
// In Folia, this will be entity thread of the player.
// In Bukkit, this will be the main thread.
withContext(plugin.mainDispatcher) {
// Make sure you switch to your plugin main thread before you do anything in your plugin.
val player = event.player
val playerData = database.getDataFromPlayer(player)
playerData.name = player.name
playerData.lastJoinDate = Date()
database.saveData(player, playerData)
}
}

@EventHandler
fun onPlayerQuitEvent(event: PlayerQuitEvent) {
// Alternative way to achieve the same thing
plugin.launch(plugin.entityDispatcher(event.player)), CoroutineStart.UNDISPATCHED) {
plugin.launch {
// Make sure you switch to your plugin main thread before you do anything in your plugin.
val player = event.player
val playerData = database.getDataFromPlayer(player)
playerData.name = player.name
Expand Down
2 changes: 1 addition & 1 deletion docs/wiki/docs/unittests.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ feedback to the real environment.

```kotlin
dependencies {
testImplementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-test:2.15.0")
testImplementation("com.github.shynixn.mccoroutine:mccoroutine-bukkit-test:2.16.0")
}
```

Expand Down
2 changes: 1 addition & 1 deletion mccoroutine-bukkit-sample/src/main/resources/plugin.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: MCCoroutine-Sample
version: 2.15.0
version: 2.16.0
author: Shynixn
main: com.github.shynixn.mccoroutine.bukkit.sample.MCCoroutineSamplePlugin
commands:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: MCCoroutine-Sample
version: 2.15.0
version: 2.16.0
author: Shynixn
main: com.github.shynixn.mccoroutine.bungeecord.sample.MCCoroutineSamplePlugin
commands:
Expand Down
4 changes: 2 additions & 2 deletions mccoroutine-fabric-sample/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ repositories {
mavenLocal()
}
dependencies {
implementation("com.github.shynixn.mccoroutine:mccoroutine-fabric-api:2.15.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-fabric-core:2.15.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-fabric-api:2.16.0")
implementation("com.github.shynixn.mccoroutine:mccoroutine-fabric-core:2.16.0")

minecraft("com.mojang", "minecraft", project.extra["minecraft_version"] as String)
mappings("net.fabricmc", "yarn", project.extra["yarn_mappings"] as String, null, "v2")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ interface CoroutineSession {
*/
val dispatcherAsync: CoroutineContext

/**
* The main dispatcher represents the main thread of a plugin.
*/
val dispatcherMain : CoroutineContext

/**
* Manipulates the bukkit server heart beat on startup.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ val Plugin.asyncDispatcher: CoroutineContext
return mcCoroutine.getCoroutineSession(this).dispatcherAsync
}

/**
* Gets the plugin main dispatcher. The main dispatcher consists of a single thread dedicated for your plugin
*/
val Plugin.mainDispatcher : CoroutineContext
get() {
return mcCoroutine.getCoroutineSession(this).dispatcherMain
}

/**
* Gets the dispatcher to schedule tasks on the region that owns the entity.
* If Folia is not loaded, this falls back to the bukkit minecraftDispatcher.
Expand Down Expand Up @@ -87,16 +95,16 @@ val Plugin.scope: CoroutineScope
}

/**
* Launches a new coroutine on the current thread without blocking the current thread and returns a reference to the coroutine as a [Job].
* Launches a new coroutine on the main plugin thread without blocking the current thread and returns a reference to the coroutine as a [Job].
* The coroutine is cancelled when the resulting job is [cancelled][Job.cancel].
*
* The coroutine context is inherited from a [Plugin.scope]. Additional context elements can be specified with [context] argument.
* If the context does not have any dispatcher nor any other [ContinuationInterceptor], then Unconfined Dispatcher is used.
* The parent job is inherited from a [Plugin.scope] as well, but it can also be overridden
* with a corresponding [context] element.
*
* By default, the coroutine is immediately scheduled on the current calling thread. However, manipulating global data, entities or locations
* is not safe in this context. Use subsequent operations for this case e.g. withContext(plugin.entityDispatcher(entity)) {} or withContext(plugin.regionDispatcher(location)) {}
* By default, the coroutine is scheduled on the plugin main thread. This is not the minecraft main thread even if you are using MCCoroutine for Folia in Bukkit mode.
* Manipulating global data, entities or locations is not safe in this context. Use subsequent operations for this case e.g. withContext(plugin.entityDispatcher(entity)) {} or withContext(plugin.regionDispatcher(location)) {}
* Other start options can be specified via `start` parameter. See [CoroutineStart] for details.
* An optional [start] parameter can be set to [CoroutineStart.LAZY] to start coroutine _lazily_. In this case,
* the coroutine [Job] is created in _new_ state. It can be explicitly started with [start][Job.start] function
Expand All @@ -111,8 +119,8 @@ val Plugin.scope: CoroutineScope
* @param block the coroutine code which will be invoked in the context of the provided scope.
**/
fun Plugin.launch(
context: CoroutineContext,
start: CoroutineStart,
context: CoroutineContext = mainDispatcher,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
if (!scope.isActive) {
Expand Down
Loading

0 comments on commit fea39be

Please sign in to comment.