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:
@@ -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.
|
||||
@@ -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) }
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
+75
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user