This commit is contained in:
Britakee
2026-01-25 21:47:55 +01:00
3 changed files with 628 additions and 59 deletions

398
README.md
View File

@@ -1,101 +1,381 @@
# 🛠️ Hytale Plugin Template
# Hytale Plugin Template
Welcome to the **Hytale Plugin Template**! This project is a pre-configured foundation for building **Java Plugins**. It streamlines the development process by handling classpath setup, server execution, and asset bundling.
A minimal, ready-to-use template for creating Hytale plugins with modern build tools and automated testing.
> **⚠️ Early Access Warning**
> Hytale is currently in Early Access. Features, APIs, and this template are subject to frequent changes. Please ensure you are using the latest version of the template for the best experience.
> **✨ Builds immediately without any changes!** Clone and run `./gradlew shadowJar` to get a working plugin JAR.
## Features
**Modern Build System** - Gradle with Kotlin DSL
**Automated Testing** - Custom Gradle plugin for one-command server testing
**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
---
## 📋 Prerequisites
## Quick Start
Before you begin, ensure your environment is ready:
### Prerequisites
* **Hytale Launcher**: Installed and updated.
* **Java 25 SDK**: Required for modern Hytale development.
* **IntelliJ IDEA**: (Community or Ultimate) Recommended for full feature support.
- **Java 25 JDK** - [Download here](https://www.oracle.com/java/technologies/downloads/)
- **IntelliJ IDEA** - [Download here](https://www.jetbrains.com/idea/download/) (Community Edition is fine)
- **Git** - [Download here](https://git-scm.com/)
---
## 🚀 Quick Start Installation
### 1. Initial Setup (Before Importing)
To avoid IDE caching issues, configure these files **before** you open the project in IntelliJ:
* **`settings.gradle`**: Set your unique project name.
```gradle
rootProject.name = 'MyAwesomePlugin'
### 1. Clone or Download
```bash
git clone https://github.com/yourusername/hytale-plugin-template.git
cd hytale-plugin-template
```
**The template builds immediately without any changes!**
You can customize it later when you're ready to develop your plugin.
* **`gradle.properties`**: Set your `maven_group` (e.g., `com.yourname`) and starting version.
* **`src/main/resources/manifest.json`**: Update your plugin metadata.
* **CRITICAL:** Ensure the `"Main"` property points exactly to your entry-point class.
### 2. Build Immediately (No Changes Needed!)
The template works out-of-the-box:
```bash
# Windows
gradlew.bat shadowJar
### 2. Importing the Project
# Linux/Mac
./gradlew shadowJar
```
1. Open IntelliJ IDEA and select **Open**.
2. Navigate to the template folder and click **OK**.
3. Wait for the Gradle sync to finish. This will automatically download dependencies, create a `./run` folder, and generate the **HytaleServer** run configuration.
Your plugin JAR will be in: `build/libs/TemplatePlugin-1.0.0.jar`
### 3. Authenticating your Test Server
### 3. Customize Your Plugin (Optional)
You **must** authenticate your local server to connect to it:
When ready to customize, edit these files:
1. Launch the **HytaleServer** configuration in IDEA.
2. In the terminal, run: `auth login device`.
3. Follow the printed URL to log in via your Hytale account.
4. Once verified, run: `auth persistence Encrypted`.
**`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",
"Name": "YourPluginName",
"Main": "com.yourname.yourplugin.YourPlugin"
}
```
**Rename the main plugin class:**
- Rename `src/main/java/com/example/templateplugin/TemplatePlugin.java`
- Update package name to match your `pluginGroup`
### 4. Build Your Plugin
```bash
# Windows
gradlew.bat shadowJar
# Linux/Mac
./gradlew shadowJar
```
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
- Storage
- Utilities
See our [documentation](../Documentation/) for examples and patterns.
### 6. Test Your Plugin (Automated!)
```bash
# Windows
gradlew.bat runServer
# Linux/Mac
./gradlew runServer
```
This will:
1. Download the Hytale server (cached for future runs)
2. Build your plugin
3. Copy it to the server's mods folder
4. Start the server with interactive console
---
## 🎮 Developing & Testing
## Project Structure
### Running the Server
```
TemplatePlugin/
├── .github/workflows/
│ └── build.yml # CI/CD workflow
├── buildSrc/
│ ├── build.gradle.kts # Custom plugin configuration
│ └── src/main/kotlin/
│ └── RunHytalePlugin.kt # Automated server testing
├── src/main/
│ ├── java/com/example/templateplugin/
│ │ └── TemplatePlugin.java # Minimal main class (example)
│ └── resources/
│ └── manifest.json # Plugin metadata
├── .gitignore # Git ignore rules
├── build.gradle.kts # Build configuration
├── gradle.properties # Project properties
├── settings.gradle.kts # Project settings
├── LICENSE # MIT License
└── README.md # This file
```
If you do not see the **HytaleServer** run configuration in the top-right dropdown, click "Edit Configurations..." to unhide it. Press the **Green Play Button** to start, or the **Bug Icon** to start in Debug Mode to enable breakpoints.
**Note:** This is a minimal template. Create your own folder structure:
### Verifying the Setup
1. Launch your standard Hytale Client.
2. Connect to `Local Server` (127.0.0.1).
3. Type `/test` in-game. If it returns your plugin version, everything is working!
### Bundling Assets
You can include models and textures by placing them in `src/main/resources/Common/` or `src/main/resources/Server/`. These are editable in real-time using the in-game **Asset Editor**.
- `commands/` - For command implementations
- `listeners/` - For event listeners
- `services/` - For business logic
- `storage/` - For data persistence
- `utils/` - For utility classes
- `config/` - For configuration management
---
## 📦 Building your Plugin
## Development Workflow
To create a shareable `.jar` file for distribution:
### Building
1. Open the **Gradle Tab** on the right side of IDEA.
2. Navigate to `Tasks` -> `build` -> `build`.
3. Your compiled plugin will be in: `build/libs/your-plugin-name-1.0.0.jar`.
```bash
# Compile only
./gradlew compileJava
To install it manually, drop the JAR into `%appdata%/Hytale/UserData/Mods/`.
# Build plugin JAR
./gradlew shadowJar
# Clean and rebuild
./gradlew clean shadowJar
```
### Testing
```bash
# Run server with your plugin
./gradlew runServer
# Run unit tests
./gradlew test
# Clean test server
rm -rf run/
```
### Debugging
```bash
# Run server in debug mode
./gradlew runServer -Pdebug
# Then connect your IDE debugger to localhost:5005
```
---
## 📚 Advanced Documentation
## Customization
For detailed guides on commands, event listeners, and professional patterns, visit our full documentation:
👉 **[Hytale Modding Documentation](https://britakee-studios.gitbook.io/hytale-modding-documentation)**
### Adding Dependencies
Edit `build.gradle.kts`:
```kotlin
dependencies {
// Hytale API (provided by server)
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")
}
```
### 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 `mods/` folder.
**Usage:**
Edit `build.gradle.kts`:
```kotlin
runHytale {
jarUrl = "url to hytale server jar"
}
```
Run the server with:
```bash
# Windows
gradlew.bat runServer
# Linux/Mac
./gradlew runServer
```
**Features:**
- ✅ Automatic server JAR download and caching
- ✅ Compiles and deploys your plugin automatically
- ✅ Starts server with interactive console
- ✅ One-command workflow: `./gradlew runServer`
- ✅ Server files in `run/` directory (gitignored)
### Implementing Your Plugin
**Recommended folder structure:**
```
src/main/java/com/yourname/yourplugin/
├── YourPlugin.java # Main class
├── commands/ # Commands
├── listeners/ # Event listeners
├── services/ # Business logic
├── storage/ # Data persistence
├── config/ # Configuration
└── utils/ # Utilities
```
**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)
---
## 🆘 Troubleshooting
## CI/CD
* **Sync Fails**: Check that your Project SDK is set to **Java 25** via `File > Project Structure`.
* **Cannot Connect**: Ensure you ran the `auth` commands in the server console.
* **Plugin Not Loading**: Double-check your `manifest.json` for typos in the `"Main"` class path.
This template includes a GitHub Actions workflow that:
1. ✅ Builds your plugin on every push
2. ✅ Runs tests
3. ✅ Uploads artifacts
4. ✅ Creates releases (when you tag)
### Creating a Release
```bash
git tag v1.0.0
git push origin v1.0.0
```
GitHub Actions will automatically build and create a release with your plugin JAR.
---
**Need Help?** Visit our full guide here: **[Hytale Modding Documentation](https://britakee-studios.gitbook.io/hytale-modding-documentation)**
## Best Practices
### ✅ DO:
- Use the Service-Storage pattern for data management
- Write unit tests for your business logic
- Use structured logging (not `System.out.println`)
- Handle errors gracefully
- Document your public API
- Version your releases semantically (1.0.0, 1.1.0, etc.)
### ❌ DON'T:
- Hardcode configuration values
- Block the main thread with heavy operations
- Ignore exceptions
- Use deprecated APIs
- Commit sensitive data (API keys, passwords)
---
## Troubleshooting
### Build Fails
```bash
# Clean and rebuild
./gradlew clean build --refresh-dependencies
```
### Server Won't Start
1. Check that `jarUrl` in `build.gradle.kts` is correct
2. Verify Java 25 is installed: `java -version`
3. Check logs in `run/logs/`
### Plugin Not Loading
1. Verify `manifest.json` has correct `Main` class
2. Check server logs for errors
3. Ensure all dependencies are bundled in JAR
---
## Documentation
For detailed guides on plugin development, see:
- [Hytale Modding Documentation](https://github.com/yourusername/hytale-modding/tree/main/Documentation)
- [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)
---
## Contributing
Contributions are welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Submit a pull request
---
## License
This template is released under the MIT License. You are free to use it for any purpose.
---
## Support
- **Issues:** [GitHub Issues](https://github.com/yourusername/hytale-plugin-template/issues)
- **Documentation:** [Hytale Modding Docs](https://github.com/yourusername/hytale-modding)
- **Community:** Join the Hytale modding community
---
## Credits
Created by the Hytale modding community.
Based on best practices from production Hytale plugins.
---
**Happy Modding! 🎮**

87
build.gradle.kts Normal file
View File

@@ -0,0 +1,87 @@
plugins {
id("java-library")
id("com.gradleup.shadow") version "9.3.1"
id("run-hytale")
}
group = findProperty("pluginGroup") as String? ?: "com.example"
version = findProperty("pluginVersion") as String? ?: "1.0.0"
description = findProperty("pluginDescription") as String? ?: "A Hytale plugin template"
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
// Hytale Server API (provided by server at runtime)
compileOnly(files("./libs/HytaleServer.jar"))
// Common dependencies (will be bundled in JAR)
implementation("com.google.code.gson:gson:2.10.1")
implementation("org.jetbrains:annotations:24.1.0")
// Test dependencies
testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
// Configure server testing
runHytale {
jarUrl = "./libs/HytaleServer.jar"
assetsPath = "./libs/Assets.zip"
}
tasks {
// Configure Java compilation
compileJava {
options.encoding = Charsets.UTF_8.name()
options.release = 25
}
// Configure resource processing
processResources {
filteringCharset = Charsets.UTF_8.name()
// Replace placeholders in manifest.json
val props = mapOf(
"group" to project.group,
"version" to project.version,
"description" to project.description
)
inputs.properties(props)
filesMatching("manifest.json") {
expand(props)
}
}
// Configure ShadowJar (bundle dependencies)
shadowJar {
archiveBaseName.set(rootProject.name)
archiveClassifier.set("")
// Relocate dependencies to avoid conflicts
relocate("com.google.gson", "com.yourplugin.libs.gson")
// Minimize JAR size (removes unused classes)
minimize()
}
// Configure tests
test {
useJUnitPlatform()
}
// Make build depend on shadowJar
build {
dependsOn(shadowJar)
}
}
// Configure Java toolchain
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(25))
}
}

View File

@@ -0,0 +1,202 @@
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
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
/**
* Custom Gradle plugin for automated Hytale server testing.
*
* Usage:
* runHytale {
* jarUrl = "https://example.com/hytale-server.jar"
* assetsPath = "Assets.zip"
* }
*
* ./gradlew runServer
*/
open class RunHytalePlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create("runHytale", RunHytaleExtension::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"
}
project.tasks.findByName("shadowJar")?.let {
runTask.configure { dependsOn(it) }
}
}
}
/**
* Extension for configuring the RunHytale plugin.
*/
open class RunHytaleExtension {
var jarUrl: String = "https://example.com/hytale-server.jar"
var assetsPath: String? = null
}
/**
* Task that downloads, sets up, and runs a Hytale server with the plugin.
*/
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, "mods").apply { mkdirs() }
val jarFile = File(runDir, "server.jar")
// Cache directory for downloaded server JARs
val cacheDir = File(
project.layout.buildDirectory.asFile.get(),
"hytale-cache"
).apply { mkdirs() }
// 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(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 ${jarUri}")
try {
jarUri.toURL().openStream().use { input ->
cachedJar.outputStream().use { output ->
input.copyTo(output)
}
}
println("Server JAR downloaded and cached")
} catch (e: Exception) {
println("ERROR: Failed to download server JAR")
println("Make sure the jarUrl in build.gradle.kts is correct")
println("Error: ${e.message}")
return
}
} else {
println("Using cached server JAR")
}
// Copy server JAR to run directory
cachedJar.copyTo(jarFile, overwrite = true)
// 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)
println("Plugin copied to: ${targetFile.absolutePath}")
} ?: run {
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")
// Check if debug mode is enabled
val debugMode = project.hasProperty("debug")
val javaArgs = mutableListOf<String>()
if (debugMode) {
javaArgs.add("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005")
println("Debug mode enabled. Connect debugger to port 5005")
}
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())
.directory(runDir)
.start()
// Handle graceful shutdown
project.gradle.buildFinished {
if (process.isAlive) {
println("\nStopping server...")
process.destroy()
}
}
// 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 ->
lines.forEach {
process.outputStream.write((it + "\n").toByteArray())
process.outputStream.flush()
}
}
}.start()
// Wait for server to exit
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()
}
}