feat(examples): four runnable plugin examples

- player-load: load-on-join (PlayerReadyEvent → IO → modify).
- async-moderation: IAsyncEvent → coroutine bridge for chat moderation.
- periodic-leaderboard: pluginScope loop with parallel reads.
- bounty-board: kitchen-sink demo exercising every v0.1 primitive.
This commit is contained in:
2026-04-28 16:30:39 +02:00
parent ad1379c267
commit 5fc3bda1c5
22 changed files with 1069 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
# periodic-leaderboard
Recomputes a top-5 leaderboard every minute by reading `PlayerStats.level`
from every online player in parallel, sorting, and broadcasting.
## What it shows
- `pluginScope(this).launch { while (isActive) { delay(60.seconds); … } }`
a plugin-lifetime periodic task. Cancels cleanly on shutdown via
`Async.shutdown()`.
- Parallel `read` over players via `async { … }.awaitAll()`. Each `read`
switches to its player's owning world dispatcher independently — reads on
different worlds run concurrently, reads on the same world serialize.
- `runCatching { … }` around the body so a single bad read doesn't kill the
loop.
This is the shape for any plugin-wide background recompute: leaderboards,
periodic backups of derived state, quest sync, etc.
## Build
```bash
./gradlew shadowJar
# → build/libs/periodic-leaderboard.jar
```
## Run
1. Drop in `mods/`, wire `PlayerStatsBinding.componentType()` and
`onlinePlayers()`.
2. For a faster demo while testing, change `delay(60.seconds)` to
`delay(5.seconds)` and rebuild.
3. Connect 2+ players. The top-5 logs every loop tick.
If you want **per-world** leaderboards instead, swap `pluginScope(this)` for
`worldScope(world)` and run one loop per world. Cancellation flows through
`WorldScopes.cancel(uuid)`.
@@ -0,0 +1,40 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.shadow)
}
group = "com.mythlane.example"
version = "0.1.0"
description = "Async example: periodic-leaderboard"
val hytaleServerVersion = libs.versions.hytaleServer.get()
dependencies {
compileOnly(libs.hytale.server)
implementation("com.mythlane:async:0.1.0-SNAPSHOT")
}
kotlin {
jvmToolchain(25)
compilerOptions { jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_24) }
}
tasks.withType<JavaCompile>().configureEach { options.release.set(24) }
tasks.processResources {
filteringCharset = Charsets.UTF_8.name()
val props = mapOf(
"version" to project.version,
"description" to (project.description ?: ""),
"hytaleServerVersion" to hytaleServerVersion,
)
inputs.properties(props)
filesMatching("manifest.json") { expand(props) }
}
tasks.shadowJar {
archiveBaseName.set(project.name)
archiveClassifier.set("")
}
tasks.jar { enabled = false }
tasks.build { dependsOn(tasks.shadowJar) }
@@ -0,0 +1,18 @@
rootProject.name = "periodic-leaderboard"
dependencyResolutionManagement {
repositories {
mavenCentral()
maven("https://maven.hytale.com/release") { name = "hytale" }
}
versionCatalogs {
create("libs") { from(files("../../gradle/libs.versions.toml")) }
}
}
includeBuild("../..") {
dependencySubstitution {
substitute(module("com.mythlane:async"))
.using(project(":dist"))
}
}
@@ -0,0 +1,64 @@
package com.mythlane.example.leaderboard
import com.hypixel.hytale.server.core.entity.entities.Player
import com.hypixel.hytale.server.core.plugin.JavaPlugin
import com.hypixel.hytale.server.core.plugin.JavaPluginInit
import com.mythlane.async.Async
import com.mythlane.async.ecs.ComponentRegistry
import com.mythlane.async.ecs.read
import com.mythlane.async.binding.handle
import com.mythlane.async.binding.installAsync
import com.mythlane.async.binding.pluginScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds
/**
* Demonstrates: `pluginScope(this).launch { while (isActive) { delay(…); … } }`
* for a periodic task, plus parallel `read<T>` via `async { … }.awaitAll()`.
*
* SDK-side wiring of `onlinePlayers()` and the component type is left as `TODO`.
*/
class LeaderboardPlugin(init: JavaPluginInit) : JavaPlugin(init) {
override fun start() {
installAsync()
ComponentRegistry.register<PlayerStats>(PlayerStatsBinding.componentType())
pluginScope(this).launch {
while (isActive) {
delay(60.seconds)
runCatching { recompute() }
}
}
}
override fun shutdown() {
Async.shutdown()
}
private suspend fun recompute() = coroutineScope {
val players: List<Player> = onlinePlayers()
players.map { p ->
async {
p to read<PlayerStats, Int>(p.handle()) { level }
}
}.awaitAll()
.sortedByDescending { it.second }
.take(5)
// .also { broadcast(...) } — wire to your messaging API
}
private fun onlinePlayers(): List<Player> = TODO("Return Players from your dev server (e.g. HytaleServer.get().worlds...).")
}
/** Stub component shape — replace with your real `Component<EntityStore>` subclass. */
class PlayerStats { var level: Int = 1 }
object PlayerStatsBinding {
fun componentType(): Any = TODO("Return EntityStore.REGISTRY.register(PlayerStats::class.java, ...) result here.")
}
@@ -0,0 +1,16 @@
{
"Group": "Mythlane",
"Name": "PeriodicLeaderboard",
"Version": "${version}",
"Description": "${description}",
"Authors": [
{ "Name": "Mythlane", "Email": "contact@mythlane.com", "Url": "https://mythlane.com" }
],
"Website": "https://mythlane.com",
"Main": "com.mythlane.example.leaderboard.LeaderboardPlugin",
"ServerVersion": "${hytaleServerVersion}",
"Dependencies": {},
"OptionalDependencies": {},
"DisabledByDefault": false,
"IncludesAssetPack": false
}