1

In the docs it says that coroutines are lighter than threads and so I wanted to use a kotlin coroutine instead of the BukkitRunnable.

//Defined as class field
private val scope = coroutineScope(Dispatchers.Default)

//In class method
scope.launch {/* wait some seconds and then change blockdata */}

Calling setBlockData from Dispatchers.Default thread throws an error because the spigot API is not thread safe and you can't call API stuff from a thread other than the main.

java.lang.IllegalStateException: Asynchronous block remove!

I was thinking that changing block data is the equivalent of android UI changes in Minecraft which means that the coroutine needs to be run/injected into the main thread. So it would make sense to run my coroutine in Dispatchers.Main. However, I can't find a way use Dispatchers.Main and set it to the main thread without getting an illegalStateException

I hope my logic is correct here

user9277283
  • 63
  • 1
  • 7
  • *I can't find a way use Dispatchers.Main and set it to the main thread without getting an illegalStateException* - could you please share the code that errors like this? Is your code running on Android or JavaFX or Swing? Because if not, there is no `Dispatchers.Main` – Joffrey Jul 25 '22 at 10:23
  • It s running on none of these and that is the problem. I think I need a way for the coroutine to inject the code in main but I am not running on android or javafx or swing. I need something like runTaskTimer but with coroutines – user9277283 Jul 25 '22 at 10:51
  • 1
    Could you please clarify what error you get when using `Dispatchers.Default`? And when you write *"you can't call API stuff from another thread"* - what are you talking about precisely? Another thread than what? – Joffrey Jul 25 '22 at 12:01
  • Made some edits. The spigot api is very clear that modifying the world from a different thread than the main is a big no-no. It provides its own schedulers that from what i understand run things from the main thread and I want to do the same with kotlin – user9277283 Jul 25 '22 at 12:51
  • You might want to take a look at https://github.com/okkero/Skedule - they have implemented a dispatcher backed by the Bukkit scheduler, AFAICT. It should be exactly what you need. – Joffrey Jul 25 '22 at 13:21
  • Is there a way to do this without a library – user9277283 Jul 25 '22 at 15:59
  • Well this library is extremely small. You could write the same code yourself in your project, but why? Here is their implementation of the BukkitDispatcher (~50 lines): https://github.com/okkero/Skedule/blob/master/src/main/kotlin/com/okkero/skedule/BukkitDispatcher.kt There is only one other file in the lib (~300 lines). – Joffrey Jul 26 '22 at 08:30

1 Answers1

2

If you want a simple method that is able to bridge the suspending code with the main thread (with the possibility of fetching some information from the main thread and use that on your coroutine), you can use this method:

suspend fun <T> suspendSync(plugin: Plugin, task: () -> T): T = withTimeout(10000L) {
    // Context: The current coroutine context
    suspendCancellableCoroutine { cont ->
        // Context: The current coroutine context
        Bukkit.getScheduler().runTask(plugin) {
            // Context: Bukkit MAIN thread
            // runCatching is used to forward any exception that may occur here back to
            // our coroutine, keeping the exception transparency of Kotlin coroutines
            runCatching(task).fold({ cont.resume(it) }, cont::resumeWithException)
        }
    }
}

I've commented on what context each part of the code is executed so you can visualize the context switch. suspendCancellableCoroutine is a way of getting hold of the continuation object all coroutines use under the hood, so we can manually resume it once the main thread execute our task.

The outer block withTimeout is used so that if the main thread does not complete our task within 10 seconds, our coroutine gives up instead of hanging forever.

And the use is very simple too:

val plugin = // comes from somewhere

// example coroutine scope
CoroutineScope(Dispatchers.Default).launch {
    // doing stuff async
    // oh no, I need some data from the main thread!
    val block = suspendSync(plugin) { 
        // this code runs on the MAIN thread
        Bukkit.getWorld("blah").getBlockAt(0, 0, 0) 
    }
    // back to async here, do stuff with block (just don't MODIFY it async, use more suspendSync if needed)
}

If you have any questions or think I can improve this answer, don't be afraid of letting me know.

SecretX
  • 140
  • 1
  • 14