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 @@
|
||||
# async-moderation
|
||||
|
||||
Listens to `PlayerChatEvent` (an `IAsyncEvent`), runs a fake 200ms moderation
|
||||
check off-thread, and on flagged messages bumps a `warnings` counter on the
|
||||
world thread + cancels the event so it never reaches other players.
|
||||
|
||||
## What it shows
|
||||
|
||||
- `eventRegistry.registerAsyncGlobal(PlayerChatEvent::class.java) { future -> … }` —
|
||||
the global overload. `registerAsync` only matches `IAsyncEvent<Void>`, and
|
||||
`PlayerChatEvent` isn't `Void`-keyed; the wrong overload compiles silently
|
||||
and never fires.
|
||||
- `scope.future { … }` from `kotlinx-coroutines-jdk8` — bridges
|
||||
`CompletableFuture` ↔ coroutines without `runBlocking`.
|
||||
- `withContext(AsyncDispatchers.HytaleIO) { … }` — for the simulated HTTP call.
|
||||
- Conditional `modify<T>(...)` + `event.isCancelled = true` — the canonical
|
||||
shape for any moderation/filter handler.
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
./gradlew shadowJar
|
||||
# → build/libs/async-moderation.jar
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
1. Drop the JAR in `mods/`, wire `ModerationStatsBinding.componentType()` to
|
||||
your registered component.
|
||||
2. Type a message containing `badword`, `spam`, or `scam` in chat.
|
||||
3. The message is suppressed; the sender's `ModerationStats.warnings`
|
||||
increments on the world thread.
|
||||
|
||||
The example uses a private `CoroutineScope` to make the supervisor
|
||||
relationship explicit. In a real plugin, `pluginScope(this)` is fine and
|
||||
flows through `Async.shutdown()` cleanly.
|
||||
@@ -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: async-moderation"
|
||||
|
||||
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 = "async-moderation"
|
||||
|
||||
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"))
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package com.mythlane.example.moderation
|
||||
|
||||
import com.hypixel.hytale.server.core.event.events.player.PlayerChatEvent
|
||||
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.installAsync
|
||||
import com.mythlane.async.binding.toEntityHandle
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.future.future
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
private val FLAG_WORDS = setOf("badword", "spam", "scam")
|
||||
|
||||
/**
|
||||
* Demonstrates the async-event flow:
|
||||
* - `registerAsyncGlobal(PlayerChatEvent)` — `registerAsync` only matches
|
||||
* `IAsyncEvent<Void>`, and `PlayerChatEvent` is `IAsyncEvent<String>`, so
|
||||
* the global overload is required.
|
||||
* - `scope.future { … }` — bridges Hytale's `CompletableFuture` contract to
|
||||
* coroutines via `kotlinx-coroutines-jdk8`.
|
||||
* - `withContext(AsyncDispatchers.HytaleIO)` for the simulated HTTP call.
|
||||
* - Conditional `modify<T>` + `event.isCancelled = true`.
|
||||
*
|
||||
* Component-side wiring is left as `TODO` — orthogonal to the demo.
|
||||
*/
|
||||
class ModerationPlugin(init: JavaPluginInit) : JavaPlugin(init) {
|
||||
|
||||
// Plugin-scoped supervisor for the chat-handling coroutines bridged from
|
||||
// CompletableFuture. Cancelled in shutdown via Async.shutdown.
|
||||
private val scope = CoroutineScope(SupervisorJob() + AsyncDispatchers.HytaleIO)
|
||||
|
||||
override fun start() {
|
||||
installAsync()
|
||||
ComponentRegistry.register<ModerationStats>(ModerationStatsBinding.componentType())
|
||||
|
||||
eventRegistry.registerAsyncGlobal(PlayerChatEvent::class.java) { future ->
|
||||
future.thenCompose { event ->
|
||||
scope.future {
|
||||
val flagged = withContext(AsyncDispatchers.HytaleIO) {
|
||||
delay(200.milliseconds) // simulated HTTP call
|
||||
FLAG_WORDS.any { it in event.content.lowercase() }
|
||||
}
|
||||
if (flagged) {
|
||||
modify<ModerationStats, Unit>(event.sender.toEntityHandle()) {
|
||||
warnings += 1
|
||||
}
|
||||
event.isCancelled = true
|
||||
}
|
||||
event
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun shutdown() {
|
||||
scope.coroutineContext[kotlinx.coroutines.Job]?.cancel()
|
||||
Async.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
/** Stub component shape — replace with your real `Component<EntityStore>` subclass. */
|
||||
class ModerationStats { var warnings: Int = 0 }
|
||||
|
||||
object ModerationStatsBinding {
|
||||
fun componentType(): Any = TODO("Return EntityStore.REGISTRY.register(ModerationStats::class.java, ...) result here.")
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"Group": "Mythlane",
|
||||
"Name": "AsyncModeration",
|
||||
"Version": "${version}",
|
||||
"Description": "${description}",
|
||||
"Authors": [
|
||||
{ "Name": "Mythlane", "Email": "contact@mythlane.com", "Url": "https://mythlane.com" }
|
||||
],
|
||||
"Website": "https://mythlane.com",
|
||||
"Main": "com.mythlane.example.moderation.ModerationPlugin",
|
||||
"ServerVersion": "${hytaleServerVersion}",
|
||||
"Dependencies": {},
|
||||
"OptionalDependencies": {},
|
||||
"DisabledByDefault": false,
|
||||
"IncludesAssetPack": false
|
||||
}
|
||||
Reference in New Issue
Block a user