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
+36
View File
@@ -0,0 +1,36 @@
# player-load
Loads `<plugin data dir>/players/<uuid>.json` when a player joins, hydrates a
`PlayerStats` component on the world thread.
## What it shows
- `eventRegistry.registerGlobal(PlayerReadyEvent::class.java) { … }` — the right
overload for events with a non-`Void` key type.
- `playerScope(player).launch { … }` — coroutine bound to the player. Cancels
on disconnect, so a slow disk read doesn't outlive the session.
- `withContext(AsyncDispatchers.HytaleIO) { … }` — file I/O off the world thread.
- `modify<PlayerStats>(player.handle()) { … }` — thread-safe component
mutation. The dispatcher switches happen automatically.
## Build
```bash
./gradlew shadowJar
# → build/libs/player-load.jar
```
## Run
1. Drop the JAR in your dev server's `mods/`.
2. Wire `PlayerStatsBinding.componentType()` — replace the `TODO()` with
whatever `EntityStore.REGISTRY.register(PlayerStats::class.java, …)` returns
in your setup.
3. Seed `<server>/data/Mythlane.PlayerLoad/players/<your-uuid>.json`:
```json
{"level":42,"clan":"red"}
```
4. Connect. The values land on the live `PlayerStats` component.
The JSON parser in the example is intentionally minimal — use
`kotlinx.serialization` in real plugins.
+40
View File
@@ -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: player-load"
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) }
+18
View File
@@ -0,0 +1,18 @@
rootProject.name = "player-load"
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,75 @@
package com.mythlane.example.playerload
import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent
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.dispatchers.AsyncDispatchers
import com.mythlane.async.ecs.ComponentRegistry
import com.mythlane.async.ecs.modify
import com.mythlane.async.binding.handle
import com.mythlane.async.binding.installAsync
import com.mythlane.async.binding.playerScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.nio.file.Files
import java.util.UUID
/**
* Demonstrates: `playerScope(...).launch { withContext(HytaleIO) { … }; modify<T> { … } }`.
*
* SDK-side wiring (component type registration, the actual `PlayerStats` class)
* is left as `TODO` — those pieces depend on your dev server's component layout
* and are orthogonal to what this example demonstrates.
*/
class PlayerLoadPlugin(init: JavaPluginInit) : JavaPlugin(init) {
override fun start() {
installAsync()
ComponentRegistry.register<PlayerStats>(PlayerStatsBinding.componentType())
val playersDir = dataDirectory.resolve("players").also { Files.createDirectories(it) }
eventRegistry.registerGlobal(PlayerReadyEvent::class.java) { event ->
val player = event.player
playerScope(player).launch {
val data = withContext(AsyncDispatchers.HytaleIO) {
loadFromDisk(playersDir, player.uuid!!)
}
modify<PlayerStats>(player.handle()) {
level = data.level
clan = data.clan
}
}
}
}
override fun shutdown() {
Async.shutdown()
}
// Demo-grade JSON parser — use kotlinx.serialization in real plugins.
private fun loadFromDisk(dir: java.nio.file.Path, uuid: UUID): PlayerData {
val file = dir.resolve("$uuid.json")
if (!Files.exists(file)) return PlayerData(level = 1, clan = "")
val raw = Files.readString(file).trim().removePrefix("{").removeSuffix("}")
val map = raw.split(",").associate {
val (k, v) = it.split(":", limit = 2)
k.trim().trim('"') to v.trim().trim('"')
}
return PlayerData(map["level"]?.toIntOrNull() ?: 1, map["clan"].orEmpty())
}
}
data class PlayerData(val level: Int, val clan: String)
/** Stub component shape — replace with your real `Component<EntityStore>` subclass. */
class PlayerStats {
var level: Int = 1
var clan: String = ""
}
/** Stub: returns the registered `ComponentType<EntityStore, PlayerStats>` token from your setup. */
object PlayerStatsBinding {
fun componentType(): Any = TODO("Return EntityStore.REGISTRY.register(PlayerStats::class.java, ...) result here.")
}
@@ -0,0 +1,16 @@
{
"Group": "Mythlane",
"Name": "PlayerLoad",
"Version": "${version}",
"Description": "${description}",
"Authors": [
{ "Name": "Mythlane", "Email": "contact@mythlane.com", "Url": "https://mythlane.com" }
],
"Website": "https://mythlane.com",
"Main": "com.mythlane.example.playerload.PlayerLoadPlugin",
"ServerVersion": "${hytaleServerVersion}",
"Dependencies": {},
"OptionalDependencies": {},
"DisabledByDefault": false,
"IncludesAssetPack": false
}