diff --git a/README.md b/README.md index f99daa24..e1ed6d7c 100644 --- a/README.md +++ b/README.md @@ -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 --- @@ -52,11 +52,13 @@ 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 @@ -64,6 +66,7 @@ pluginDescription=Your plugin description ``` **`src/main/resources/manifest.json`:** + ```json { "Group": "YourName", @@ -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` @@ -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 @@ -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 --- @@ -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 @@ -198,11 +205,11 @@ Edit `build.gradle.kts`: ```kotlin dependencies { // Hytale API (provided by server) - compileOnly(files("libs/hytale-server.jar")) - + compileOnly(files("./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") } @@ -210,7 +217,7 @@ dependencies { ### 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:** @@ -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 @@ -242,6 +250,7 @@ gradlew.bat runServer ### Implementing Your Plugin **Recommended folder structure:** + ``` src/main/java/com/yourname/yourplugin/ ├── YourPlugin.java # Main class @@ -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) diff --git a/build.gradle.kts b/build.gradle.kts index ecfea96a..372dbb0f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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") @@ -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 { diff --git a/buildSrc/src/main/kotlin/RunHytalePlugin.kt b/buildSrc/src/main/kotlin/RunHytalePlugin.kt index e6a3b101..8ca52080 100644 --- a/buildSrc/src/main/kotlin/RunHytalePlugin.kt +++ b/buildSrc/src/main/kotlin/RunHytalePlugin.kt @@ -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 @@ -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 { 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 = 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) } } } } @@ -47,6 +42,7 @@ open class RunHytalePlugin : Plugin { */ open class RunHytaleExtension { var jarUrl: String = "https://example.com/hytale-server.jar" + var assetsPath: String? = null } /** @@ -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 @@ -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) } @@ -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) @@ -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") @@ -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()) @@ -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 -> @@ -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() + } }