GithubHelp home page GithubHelp logo

monun / heartbeat-coroutines Goto Github PK

View Code? Open in Web Editor NEW
25.0 2.0 4.0 149 KB

Coroutine for Bukkit

License: GNU General Public License v3.0

Kotlin 85.12% Shell 14.88%
minecraft bukkit spigot kotlin coroutines asyncronous java papermc

heartbeat-coroutines's Introduction

Heartbeat Coroutines

Kotlin Kotlin Gradle Gradle Maven Central GitHub Kotlin

두근두근❤️코루틴

Coroutine for Paper


  • Features

    • Bukkit의 mainHeartBeat(GameLoop)에서 dispatch되는 Coroutine
    • JavaPlugin 생명주기의 CoroutineScope
    • 유연한 지연작업

GameLoop 내에서 병렬 혹은 비동기 라이브러리 없이 다음 연쇄 작업을 처리하는 코드를 작성해보겠습니다.

  1. 3초간 1초마다 카운트다운 메시지를 방송
  2. 5초간 1초마다 모든 개체에게 데미지
  3. 서프라이즈~ 메시지를 방송 후 종료 악질 운영자

Thread

Runnable {
    repeat(3) {
        // 비동기 문제를 해결하기 위해 GameLoop의 Thread에서 호출 
        GameLoop.runLater {
            broadcast(3 - it)
            Thread.sleep(1000L)
        }
    }
    repeat(5) {
        GameLoop.runLater {
            damageAll()
        }
        Thread.sleep(1000L)
    }
    GameLoop.runLater {
        broadcast("surprise~")
    }
}.let {
    Thread(it).start()
}

Callback

// 비동기로 어디선가 실행해주는 함수
async({
    repeat(3) {
        GameLoop.runLater {
            broadcast(3 - it)
            Thread.sleep(1000L)
        }
    }
}) {
    async({
        repeat(5) {
            GameLoop.runLater {
                damageAll()
            }
            Thread.sleep(1000L)
        }
    }) {
        async {
            GameLoop.runLater {
                broadcast("surprise~")
            }
        }
    }
}

FSM

// 취소 가능한 태스크
class Surprise : GameLoopTask() {
    private var state = 0
    private var countdownTicks = 0
    private var damageTicks = 0

    // 1초마다 GameLoop 에서 호출
    override fun run() {
        when (state) {
            0 -> {
                val message = 3 - countdownTicks++
                broadcast(message)

                if (countdownTicks >= 3) state = 1
            }
            1 -> {
                damageAll()

                if (++damageTicks >= 5) state = 2
            }
            else -> {
                broadcast("surprise~")
                cancel()
            }
        }
    }
}

극단적인 예를 들었습니다만..

실제로 GameLoop내에서 연쇄, 순차적인 루틴을 처리하기 위해선 대부분 위 예제들과 같은 구조의 코드를 작성하게됩니다.

루틴이 복잡해질수록 비동기 문제, 복잡성으로 인해 유연성은 떨어지고 유지보수 난이도는 기하급수로 상승합니다.

Coroutine을 이용하면 GameLoop 내 연쇄 작업 코드의 복잡성을 획기적으로 줄일 수 있습니다.

아래 예제는 GameLoop내에서 동기적으로 실행되는 Coroutine 코드입니다


Coroutine

// GameLoopDispatcher = GameLoop에서 Coroutine을 실행하는 CoroutineDispatcher 
CoroutineScope(GameLoopDispatcher).launch {
    repeat(3) {
        broadcast(3 - it)
        delay(1000L)
    }
    repeat(5) {
        damageAll()
        delay(1000L)
    }
    broadcast("surprise~")
}

Thread의 코드와 비슷하지만 GameLoop 내에서 동기적으로 실행 가능한 코드입니다!

Coroutine의 동작원리는 이 문서 를 참고하세요


Heartbeat coroutines 시작하기

Gradle

repositories {
    mavenCentral()
}
dependencies {
    implementation("io.github.monun:heartbeat-coroutines:<version>")
}

Example

// JavaPlugin#onEnable()
HeartbeatScope().launch {
    val suspension = Suspension()
    repeat(10) {
        logger.info(server.isPrimaryThread)
        suspension.delay(75L)
    }
    logger.info("BOOM")
}

Dispatchers.Heartbeat

JavaPlugin 과 같은 생명주기를 가진 CoroutineDispatcher 입니다.

Coroutine을 Bukkit의 PrimaryThread에서만 실행합니다.


HeartbeatScope()

JavaPlugin 과 같은 생명주기를 가진 CoroutineScope 입니다.

Dispatchers.HeartbeatCoroutineDispatcher로 가지며 JavaPlugin 생명주기를 따라가는 SupervisorJob을 부모로 가집니다.


Suspension

누적 지연 기능을 가진 클래스입니다.

Dispatchers.Heartbeat는 Coroutine을 Bukkit의 PrimaryThread에서 실행하기 위해서 BukkitScheduler#runTask를 사용합니다.

BukkitScheduler는 1tick(50ms)마다 등록된 태스크들을 실행하며 서버 상태에 따라 지연될 수 있습니다.

Coroutine은 지연을 millisecond 단위로 제어 할 수 있으며 이는 Dispatchers.Heartbeat에서 실행될 때 결과가 기대와 다를 수 있습니다.

delay(1) 함수가 호출 될 때 Dispatchers.Heartbeat에서는 50ms 이상 늘어날 수 있습니다.

Suspension은 내부적으로 누적되는 지연 시간을 가지며 누적된 시간이 과거일 경우 yield()를 호출하고 미래일 경우 남은 시간만큼 delay를 호출합니다.

heartbeat-coroutines's People

Contributors

dayo05 avatar dytroc avatar monun avatar sul0 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

heartbeat-coroutines's Issues

README.md 오타

README의 FSM 예시 when 부분 오류

Boolean과 Int 비교

JavaPlugin 생명주기의 CoroutineScope 가 제공되지 않습니다.

문제 요약

Plugman 등을 통해서 Heartbeat Coroutines를 사용하는 플러그인을 핫리로드할 경우, Heartbeat Coroutines를 사용하는 플러그인 전부의 코루틴이 중단됩니다. 이는 Heartbeat Coroutines가 실제로는 JavaPlugin 생명주기의 CoroutineScope를 제공하는 것이 아님을 시사합니다.

문제 설명

Heartbeat Coroutines를 사용하는 플러그인을 두 개 만들어보겠습니다.

//HelloMessage1
override fun onEnable() {
    HeartbeatScope().launch {
        while(true) {
            logger.info("Hello, HeartBeat1!")
            val sus = Suspension()
            sus.delay(1000L)
        }
    }
}
//HelloMessage2
override fun onEnable() {
    HeartbeatScope().launch {
        while(true) {
            logger.info("Hello, HeartBeat2!")
            val sus = Suspension()
            sus.delay(1000L)
        }
    }
}

그리고 plugman 등으로 둘 중 하나만 리로드합니다.

[19:01:36 INFO]: [HelloMessage1] Hello, HeartBeat1!
[19:01:37 INFO]: [HelloMessage2] Hello, HeartBeat2!
[19:01:37 INFO]: [HelloMessage1] Hello, HeartBeat1!
[19:01:38 INFO]: [HelloMessage2] Hello, HeartBeat2!
[19:01:38 INFO]: [HelloMessage1] Hello, HeartBeat1!
[19:01:39 INFO]: [HelloMessage2] Hello, HeartBeat2!
[19:01:39 INFO]: [HelloMessage1] Hello, HeartBeat1!
> plugman reload HelloMessage1
[19:01:40 INFO]: [HelloMessage1] Disabling HelloMessage1 v1.0
[19:01:40 INFO]: [HelloMessage1] Loading server plugin HelloMessage1 v1.0
[19:01:40 INFO]: [HelloMessage1] Enabling HelloMessage1 v1.0
[19:01:40 INFO]: [HelloMessage1] Hello, HeartBeat1!
[19:01:40 INFO]: [PlugMan] HelloMessage1 has been reloaded.
[19:01:41 INFO]: [HelloMessage1] Hello, HeartBeat1!
[19:01:42 INFO]: [HelloMessage1] Hello, HeartBeat1!
[19:01:43 INFO]: [HelloMessage1] Hello, HeartBeat1!
[19:01:44 INFO]: [HelloMessage1] Hello, HeartBeat1!

그러면 위와 같이 HelloMessage1을 리로드하였음에 불구, HelloMessage2의 코루틴도 중단된 것을 볼 수 있습니다.
(리로드는 HelloMessage1 만 되었기 때문에, HelloMessage2의 메시지는 더 이상 나오지 않게 됩니다.)

Paper 1.19 이상에서 에러가 발생합니다.

버전 & 환경

  • OS : Arch Linux x86_64 (6.4.8-arch1-1)
  • IDE : IntelliJ IDEA Ultimate 2023.1.3
  • JDK : openjdk 20.0.2

버그 내용

코드는 아래와 같습니다. (예제 코드와 거의 같습니다.)

override fun onEnable() {
        HeartbeatScope().launch {
            val suspension = Suspension()
            suspension.delay(3000L)
            logger.info("Hello")
        }
    }

Gradle은 아래와 같고 ShadowJar로 빌드했습니다.

plugins {
    id 'java'
    id 'org.jetbrains.kotlin.jvm' version '1.9.0'
    id 'com.github.johnrengelman.shadow' version '8.1.1'
}

group = 'space.zlfn'
version = '0.1'

repositories {
    mavenCentral()
    maven {
        name = "papermc-repo"
        url = "https://repo.papermc.io/repository/maven-public/"
    }
    maven {
        name = "sonatype"
        url = "https://oss.sonatype.org/content/groups/public/"
    }
}

dependencies {
    compileOnly "io.papermc.paper:paper-api:1.20.1-R0.1-SNAPSHOT"
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2'
    implementation("io.github.monun:heartbeat-coroutines:0.0.5")
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
}

.....
전체 Gradle
plugins {
    id 'java'
    id 'org.jetbrains.kotlin.jvm' version '1.9.0'
    id 'com.github.johnrengelman.shadow' version '8.1.1'
}

group = 'space.zlfn'
version = '0.1'

repositories {
    mavenCentral()
    maven {
        name = "papermc-repo"
        url = "https://repo.papermc.io/repository/maven-public/"
    }
    maven {
        name = "sonatype"
        url = "https://oss.sonatype.org/content/groups/public/"
    }
}

dependencies {
    compileOnly "io.papermc.paper:paper-api:1.20.1-R0.1-SNAPSHOT"
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2'
    implementation("io.github.monun:heartbeat-coroutines:0.0.5")
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
}

def targetJavaVersion = 17
java {
    def javaVersion = JavaVersion.toVersion(targetJavaVersion)
    sourceCompatibility = javaVersion
    targetCompatibility = javaVersion
    if (JavaVersion.current() < javaVersion) {
        toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion)
    }
}

tasks.withType(JavaCompile).configureEach {
    if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) {
        options.release = targetJavaVersion
    }
}

processResources {
    def props = [version: version]
    inputs.properties props
    filteringCharset 'UTF-8'
    filesMatching('paper-plugin.yml') {
        expand props
    }
}
compileKotlin {
    kotlinOptions {
        jvmTarget = "17"
    }
}
compileTestKotlin {
    kotlinOptions {
        jvmTarget = "17"
    }
}

이렇게 빌드한 플러그인을 페이퍼 1.20.1#117에서 실행시켰을 때, 아래와 같은 에러가 발생합니다.

[20:01:54 ERROR]: Error occurred while enabling HelloMessage v0.1 (Is it up to date?)
java.lang.ExceptionInInitializerError: null
	at io.github.monun.heartbeat.coroutines.HeartbeatCoroutineKt.HeartbeatScope(HeartbeatCoroutine.kt:68) ~[HelloMessage-0.1-all.jar:?]
	at space.zlfn.hellomessage.HelloMessage.onEnable(HelloMessage.kt:24) ~[HelloMessage-0.1-all.jar:?]
	at org.bukkit.plugin.java.JavaPlugin.setEnabled(JavaPlugin.java:281) ~[paper-api-1.20.1-R0.1-SNAPSHOT.jar:?]
	at io.papermc.paper.plugin.manager.PaperPluginInstanceManager.enablePlugin(PaperPluginInstanceManager.java:189) ~[paper-1.20.1.jar:git-Paper-117]
	at io.papermc.paper.plugin.manager.PaperPluginManagerImpl.enablePlugin(PaperPluginManagerImpl.java:104) ~[paper-1.20.1.jar:git-Paper-117]
	at org.bukkit.plugin.SimplePluginManager.enablePlugin(SimplePluginManager.java:507) ~[paper-api-1.20.1-R0.1-SNAPSHOT.jar:?]
	at org.bukkit.craftbukkit.v1_20_R1.CraftServer.enablePlugin(CraftServer.java:640) ~[paper-1.20.1.jar:git-Paper-117]
	at org.bukkit.craftbukkit.v1_20_R1.CraftServer.enablePlugins(CraftServer.java:551) ~[paper-1.20.1.jar:git-Paper-117]
	at net.minecraft.server.MinecraftServer.loadWorld0(MinecraftServer.java:636) ~[paper-1.20.1.jar:git-Paper-117]
	at net.minecraft.server.MinecraftServer.loadLevel(MinecraftServer.java:435) ~[paper-1.20.1.jar:git-Paper-117]
	at net.minecraft.server.dedicated.DedicatedServer.initServer(DedicatedServer.java:308) ~[paper-1.20.1.jar:git-Paper-117]
	at net.minecraft.server.MinecraftServer.runServer(MinecraftServer.java:1101) ~[paper-1.20.1.jar:git-Paper-117]
	at net.minecraft.server.MinecraftServer.lambda$spin$0(MinecraftServer.java:318) ~[paper-1.20.1.jar:git-Paper-117]
	at java.lang.Thread.run(Thread.java:1623) ~[?:?]
Caused by: java.lang.ClassCastException: class io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader cannot be cast to class org.bukkit.plugin.java.PluginClassLoader (io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader and org.bukkit.plugin.java.PluginClassLoader are in unnamed module of loader java.net.URLClassLoader @67424e82)
	at io.github.monun.heartbeat.coroutines.Downstream.pullPlugin(Downstream.kt:26) ~[HelloMessage-0.1-all.jar:?]
	at io.github.monun.heartbeat.coroutines.HeartbeatCoroutine.<clinit>(HeartbeatCoroutine.kt:14) ~[HelloMessage-0.1-all.jar:?]
	... 14 more

반면 똑같은 코드를 1.18로 빌드(ShadowJar)해서 Paper 1.18.2#338에서 실행했을 때는 에러 없이 코드가 작동했습니다.

README.md 오타

Coroutine 예제에 damageAll()이 damaegAll()로 적혀 있음

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.