Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ A minimal, ready-to-use template for creating Hytale plugins with modern build t
✅ **Java 25** - Latest Java features
✅ **ShadowJar** - Automatic dependency bundling
✅ **CI/CD Ready** - GitHub Actions workflow included
✅ **Minimal Structure** - Only essential files, write your own code
✅ **Minimal Structure** - Only essential files, write your own code

---

Expand Down Expand Up @@ -52,18 +52,21 @@ Your plugin JAR will be in: `build/libs/TemplatePlugin-1.0.0.jar`
When ready to customize, edit these files:

**`settings.gradle.kts`:**

```kotlin
rootProject.name = "your-plugin-name"
```

**`gradle.properties`:**

```properties
pluginGroup=com.yourname
pluginVersion=1.0.0
pluginDescription=Your plugin description
```

**`src/main/resources/manifest.json`:**

```json
{
"Group": "YourName",
Expand All @@ -73,6 +76,7 @@ pluginDescription=Your plugin description
```

**Rename the main plugin class:**

- Rename `src/main/java/com/example/templateplugin/TemplatePlugin.java`
- Update package name to match your `pluginGroup`

Expand All @@ -91,6 +95,7 @@ Your plugin JAR will be in: `build/libs/YourPluginName-1.0.0.jar`
### 5. Implement Your Plugin

Write your plugin code in `src/main/java/`:

- Commands
- Event listeners
- Services
Expand All @@ -110,9 +115,10 @@ gradlew.bat runServer
```

This will:

1. Download the Hytale server (cached for future runs)
2. Build your plugin
3. Copy it to the server's plugins folder
3. Copy it to the server's mods folder
4. Start the server with interactive console

---
Expand Down Expand Up @@ -141,6 +147,7 @@ TemplatePlugin/
```

**Note:** This is a minimal template. Create your own folder structure:

- `commands/` - For command implementations
- `listeners/` - For event listeners
- `services/` - For business logic
Expand Down Expand Up @@ -198,19 +205,19 @@ Edit `build.gradle.kts`:
```kotlin
dependencies {
// Hytale API (provided by server)
compileOnly(files("libs/hytale-server.jar"))
compileOnly(files("./HytaleServer.jar"))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it expected to have ./HytaleServer.jar here while in other places it is at ./libs/HytaleServer.jar ?

Copy link

@jeanniardJ jeanniardJ Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that in the build.gradle.kts file, there is the following path: "./libs/HytaleServer.jar".


// Your dependencies (will be bundled)
implementation("com.google.code.gson:gson:2.10.1")

// Test dependencies
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
}
```

### Configuring Server Testing

**Run Hytale Server** - A Gradle plugin to download and run a Hytale server for development and testing purposes. The server files will be located in the `run/` directory of the project. Before starting the server it will compile (shadowJar task) and copy the plugin jar to the server's `plugins/` folder.
**Run Hytale Server** - A Gradle plugin to download and run a Hytale server for development and testing purposes. The server files will be located in the `run/` directory of the project. Before starting the server it will compile (shadowJar task) and copy the plugin jar to the server's `mods/` folder.

**Usage:**

Expand All @@ -233,6 +240,7 @@ gradlew.bat runServer
```

**Features:**

- ✅ Automatic server JAR download and caching
- ✅ Compiles and deploys your plugin automatically
- ✅ Starts server with interactive console
Expand All @@ -242,6 +250,7 @@ gradlew.bat runServer
### Implementing Your Plugin

**Recommended folder structure:**

```
src/main/java/com/yourname/yourplugin/
├── YourPlugin.java # Main class
Expand All @@ -254,6 +263,7 @@ src/main/java/com/yourname/yourplugin/
```

**See our documentation for examples:**

- [Getting Started with Plugins](../Documentation/07-getting-started-with-plugins.md)
- [Advanced Plugin Patterns](../Documentation/12-advanced-plugin-patterns.md)
- [Common Plugin Features](../Documentation/14-common-plugin-features.md)
Expand Down
7 changes: 3 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ repositories {

dependencies {
// Hytale Server API (provided by server at runtime)
compileOnly(files("libs/hytale-server.jar"))
compileOnly(files("./libs/HytaleServer.jar"))

// Common dependencies (will be bundled in JAR)
implementation("com.google.code.gson:gson:2.10.1")
Expand All @@ -28,9 +28,8 @@ dependencies {

// Configure server testing
runHytale {
// TODO: Update this URL when Hytale server is available
// Using Paper server as placeholder for testing the runServer functionality
jarUrl = "https://fill-data.papermc.io/v1/objects/d5f47f6393aa647759f101f02231fa8200e5bccd36081a3ee8b6a5fd96739057/paper-1.21.10-115.jar"
jarUrl = "./libs/HytaleServer.jar"
assetsPath = "./libs/Assets.zip"
}

tasks {
Expand Down
96 changes: 66 additions & 30 deletions buildSrc/src/main/kotlin/RunHytalePlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction
import org.gradle.api.tasks.TaskProvider
import java.io.File
import java.io.InputStream
import java.net.URI
import java.security.MessageDigest

Expand All @@ -14,30 +15,24 @@ import java.security.MessageDigest
* Usage:
* runHytale {
* jarUrl = "https://example.com/hytale-server.jar"
* assetsPath = "Assets.zip"
* }
*
* ./gradlew runServer
*/
open class RunHytalePlugin : Plugin<Project> {
override fun apply(project: Project) {
// Create extension for configuration
val extension = project.extensions.create("runHytale", RunHytaleExtension::class.java)

// Register the runServer task
val runTask: TaskProvider<RunServerTask> = project.tasks.register(
"runServer",
RunServerTask::class.java
) {
val runTask = project.tasks.register("runServer", RunServerTask::class.java) {
jarUrl.set(extension.jarUrl)
extension.assetsPath?.let { assetsPath.set(it) }
group = "hytale"
description = "Downloads and runs the Hytale server with your plugin"
}

// Make runServer depend on shadowJar (build plugin first)
project.tasks.findByName("shadowJar")?.let {
runTask.configure {
dependsOn(it)
}
runTask.configure { dependsOn(it) }
}
}
}
Expand All @@ -47,6 +42,7 @@ open class RunHytalePlugin : Plugin<Project> {
*/
open class RunHytaleExtension {
var jarUrl: String = "https://example.com/hytale-server.jar"
var assetsPath: String? = null
}

/**
Expand All @@ -56,12 +52,15 @@ open class RunServerTask : DefaultTask() {

@Input
val jarUrl = project.objects.property(String::class.java)

@Input
val assetsPath = project.objects.property(String::class.java)

@TaskAction
fun run() {
// Create directories
val runDir = File(project.projectDir, "run").apply { mkdirs() }
val pluginsDir = File(runDir, "plugins").apply { mkdirs() }
val pluginsDir = File(runDir, "mods").apply { mkdirs() }
val jarFile = File(runDir, "server.jar")

// Cache directory for downloaded server JARs
Expand All @@ -70,17 +69,26 @@ open class RunServerTask : DefaultTask() {
"hytale-cache"
).apply { mkdirs() }

// Compute hash of URL for caching
// Normalize jarUrl to URI
val jarUrlStr = jarUrl.get()
val jarUri = when {
jarUrlStr.startsWith("file://") -> URI.create(jarUrlStr)
jarUrlStr.startsWith("http://") || jarUrlStr.startsWith("https://") -> URI.create(jarUrlStr)
File(jarUrlStr).isAbsolute -> File(jarUrlStr).toURI()
else -> File(project.projectDir, jarUrlStr).toURI()
}

// Compute hash of URI for caching
val urlHash = MessageDigest.getInstance("SHA-256")
.digest(jarUrl.get().toByteArray())
.digest(jarUri.toString().toByteArray())
.joinToString("") { "%02x".format(it) }
val cachedJar = File(cacheDir, "$urlHash.jar")

// Download server JAR if not cached
if (!cachedJar.exists()) {
println("Downloading Hytale server from ${jarUrl.get()}")
println("Downloading Hytale server from ${jarUri}")
try {
URI.create(jarUrl.get()).toURL().openStream().use { input ->
jarUri.toURL().openStream().use { input ->
cachedJar.outputStream().use { output ->
input.copyTo(output)
}
Expand All @@ -99,7 +107,7 @@ open class RunServerTask : DefaultTask() {
// Copy server JAR to run directory
cachedJar.copyTo(jarFile, overwrite = true)

// Copy plugin JAR to plugins folder
// Copy plugin JAR to mods folder
project.tasks.findByName("shadowJar")?.outputs?.files?.firstOrNull()?.let { shadowJar ->
val targetFile = File(pluginsDir, shadowJar.name)
shadowJar.copyTo(targetFile, overwrite = true)
Expand All @@ -108,6 +116,29 @@ open class RunServerTask : DefaultTask() {
println("WARNING: Could not find shadowJar output")
}

// Copy assets file (mandatory)
val assetsPathStr = assetsPath.orNull
?: throw IllegalStateException(
"assetsPath is required but not set. " +
"Please configure it in build.gradle.kts: runHytale { assetsPath = \"Assets.zip\" }"
)

val sourceAssets = when {
assetsPathStr.startsWith("file://") -> File(URI.create(assetsPathStr))
File(assetsPathStr).isAbsolute -> File(assetsPathStr)
else -> File(project.projectDir, assetsPathStr)
}
if (!sourceAssets.exists()) {
throw IllegalStateException(
"Assets file not found: ${sourceAssets.absolutePath}. " +
"Please ensure assetsPath is correctly configured in build.gradle.kts"
)
}

val assetsFile = File(runDir, sourceAssets.name)
sourceAssets.copyTo(assetsFile, overwrite = true)
println("Assets copied to: ${assetsFile.absolutePath}")

println("Starting Hytale server...")
println("Press Ctrl+C to stop the server")

Expand All @@ -121,6 +152,10 @@ open class RunServerTask : DefaultTask() {
}

javaArgs.addAll(listOf("-jar", jarFile.name))

// Add assets argument (mandatory)
javaArgs.add("--assets")
javaArgs.add(assetsFile.name)

// Start the server process
val process = ProcessBuilder("java", *javaArgs.toTypedArray())
Expand All @@ -135,20 +170,10 @@ open class RunServerTask : DefaultTask() {
}
}

// Forward stdout to console
Thread {
process.inputStream.bufferedReader().useLines { lines ->
lines.forEach { println(it) }
}
}.start()

// Forward stderr to console
Thread {
process.errorStream.bufferedReader().useLines { lines ->
lines.forEach { System.err.println(it) }
}
}.start()

// Forward process streams to console
forwardStream(process.inputStream) { println(it) }
forwardStream(process.errorStream) { System.err.println(it) }

// Forward stdin to server (for commands)
Thread {
System.`in`.bufferedReader().useLines { lines ->
Expand All @@ -163,4 +188,15 @@ open class RunServerTask : DefaultTask() {
val exitCode = process.waitFor()
println("Server exited with code $exitCode")
}

/**
* Forwards an input stream to a consumer in a background thread.
*/
private fun forwardStream(inputStream: InputStream, consumer: (String) -> Unit) {
Thread {
inputStream.bufferedReader().useLines { lines ->
lines.forEach(consumer)
}
}.start()
}
}