diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9742329..91f87e5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,6 +13,19 @@ jobs: java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 + - name: Cache Hytale Server + uses: actions/cache@v4 + with: + path: | + hytale/libs/HytaleServer.jar + hytale/build/download/hytale-downloader-linux-amd64 + key: ${{ runner.os }}-hytale-download + restore-keys: | + ${{ runner.os }}-hytale- + - name: Download Hytale Server + env: + HYTALE_DOWNLOADER_CREDENTIALS: ${{ secrets.HYTALE_DOWNLOADER_CREDENTIALS }} + run: ./gradlew :hytale:download-server - name: Build with Gradle run: ./gradlew build - name: Test with Gradle diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml index 3e7ae00..80f0113 100644 --- a/.github/workflows/maven-publish.yml +++ b/.github/workflows/maven-publish.yml @@ -4,9 +4,6 @@ on: types: [ prereleased, released ] jobs: build: - env: - REPOSITORY_USER: ${{ secrets.REPOSITORY_USER }} - REPOSITORY_TOKEN: ${{ secrets.REPOSITORY_TOKEN }} runs-on: ubuntu-latest steps: - name: Checkout sources @@ -18,5 +15,21 @@ jobs: java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 + - name: Cache Hytale Server + uses: actions/cache@v4 + with: + path: | + hytale/libs/HytaleServer.jar + hytale/build/download/hytale-downloader-linux-amd64 + key: ${{ runner.os }}-hytale-download + restore-keys: | + ${{ runner.os }}-hytale- + - name: Download Hytale Server + env: + HYTALE_DOWNLOADER_CREDENTIALS: ${{ secrets.HYTALE_DOWNLOADER_CREDENTIALS }} + run: ./gradlew :hytale:download-server - name: Publish with Gradle to Repository + env: + REPOSITORY_USER: ${{ secrets.REPOSITORY_USER }} + REPOSITORY_TOKEN: ${{ secrets.REPOSITORY_TOKEN }} run: ./gradlew publish \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 6ded910..68b492f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.8.1 +version=0.9.0 diff --git a/hytale/.gitignore b/hytale/.gitignore new file mode 100644 index 0000000..110fd20 --- /dev/null +++ b/hytale/.gitignore @@ -0,0 +1,3 @@ +### Credentials ### +.hytale-downloader-credentials.json +libs \ No newline at end of file diff --git a/hytale/README.md b/hytale/README.md new file mode 100644 index 0000000..b4948f3 --- /dev/null +++ b/hytale/README.md @@ -0,0 +1,52 @@ +# Hytale Module + +Since the Hytale API is not public yet, and redistribution is not allowed, +you have to download the Hytale server yourself. + +## Initial Setup + +Before building this module, you need to authenticate with your Hytale account. You have two options: + +### Option 1: Using Environment Variable (Recommended for CI) + +Set the `HYTALE_DOWNLOADER_CREDENTIALS` environment variable with your Hytale authentication credentials: + +```bash +export HYTALE_DOWNLOADER_CREDENTIALS='{"your":"auth","json":"here"}' +./gradlew :hytale:download-server +``` + +This token can be obtained by running the download task without credentials once ( +see [Obtaining Hytale Authentication](#obtaining-hytale-authentication)). + +### Option 2: Using Credentials File (Recommended for Local Development) + +1. Create `.hytale-downloader-credentials.json` in the `hytale/` directory +2. Paste your Hytale authentication JSON credentials in the file +3. Run the download task: + +```bash +./gradlew :hytale:download-server +``` + +The credentials file is gitignored and won't be committed. + +## Obtaining Hytale Authentication + +To get your Hytale authentication credentials: + +1. Run the download task without credentials: + ```bash + ./gradlew :hytale:download-server + ``` +2. The Hytale downloader will prompt you to authenticate +3. After successful authentication, the credentials will be saved to + `.hytale-downloader-credentials.json` for future use + +## Updating the Server + +To update the Hytale server: + +```bash +./gradlew :hytale:update-server +``` \ No newline at end of file diff --git a/hytale/build.gradle.kts b/hytale/build.gradle.kts new file mode 100644 index 0000000..9d95123 --- /dev/null +++ b/hytale/build.gradle.kts @@ -0,0 +1,136 @@ +val moduleName by extra("dev.faststats.hytale") + +val libsDir: Directory = layout.projectDirectory.dir("libs") +val hytaleServerJar: RegularFile = libsDir.file("HytaleServer.jar") +val credentialsFile: RegularFile = layout.projectDirectory.file(".hytale-downloader-credentials.json") +val downloadDir: Provider = layout.buildDirectory.dir("download") +val hytaleZip: Provider = downloadDir.map { it.file("hytale.zip") } + +dependencies { + api(project(":core")) + compileOnly(files(hytaleServerJar)) +} + +tasks.register("download-server") { + group = "hytale" + + doLast { + if (hytaleServerJar.asFile.exists()) { + println("HytaleServer.jar already exists, skipping download") + return@doLast + } + + val downloaderZip: Provider = downloadDir.map { it.file("hytale-downloader.zip") } + + libsDir.asFile.mkdirs() + downloadDir.get().asFile.mkdirs() + + val os = org.gradle.internal.os.OperatingSystem.current() + val downloaderExecutable = when { + os.isLinux -> downloadDir.map { it.file("hytale-downloader-linux-amd64") } + os.isWindows -> downloadDir.map { it.file("hytale-downloader-windows-amd64.exe") } + else -> throw GradleException("Unsupported operating system: ${os.name}") + } + + if (!downloaderExecutable.get().asFile.exists()) { + if (!downloaderZip.get().asFile.exists()) ant.invokeMethod( + "get", mapOf( + "src" to "https://downloader.hytale.com/hytale-downloader.zip", + "dest" to downloaderZip.get().asFile.absolutePath + ) + ) else { + println("hytale-downloader.zip already exists, skipping download") + } + + copy { + from(zipTree(downloaderZip)) + include(downloaderExecutable.get().asFile.name) + into(downloadDir) + } + } else { + println("Hytale downloader binary already exists, skipping download and extraction") + } + + if (downloaderZip.get().asFile.delete()) { + println("Deleted hytale-downloader.zip after extracting binaries") + } + + downloaderExecutable.get().asFile.setExecutable(true) + + if (!hytaleZip.get().asFile.exists()) { + val credentials = System.getenv("HYTALE_DOWNLOADER_CREDENTIALS") + if (!credentials.isNullOrBlank()) { + if (!credentialsFile.asFile.exists()) { + credentialsFile.asFile.writeText(credentials) + println("Hytale downloader credentials written from environment variable to ${credentialsFile.asFile.absolutePath}") + } else { + println("Using existing credentials file at ${credentialsFile.asFile.absolutePath}") + } + } + + val processBuilder = ProcessBuilder( + downloaderExecutable.get().asFile.absolutePath, + "-download-path", + "hytale", + "-credentials-path", + credentialsFile.asFile.absolutePath + ) + processBuilder.directory(downloadDir.get().asFile) + processBuilder.redirectErrorStream(true) + val process = processBuilder.start() + + process.inputStream.bufferedReader().use { reader -> + reader.lines().forEach { line -> + println(line) + } + } + + val exitCode = process.waitFor() + if (exitCode != 0) { + throw GradleException("Hytale downloader failed with exit code: $exitCode") + } + } else { + println("hytale.zip already exists, skipping download") + } + + if (hytaleZip.get().asFile.exists()) { + val serverDir = downloadDir.map { it.dir("Server") } + copy { + from(zipTree(hytaleZip)) + include("Server/HytaleServer.jar") + into(downloadDir) + } + + val extractedJar = serverDir.map { it.file("HytaleServer.jar") } + if (extractedJar.get().asFile.exists()) { + extractedJar.get().asFile.copyTo(hytaleServerJar.asFile, overwrite = true) + serverDir.get().asFile.deleteRecursively() + } else { + throw GradleException("HytaleServer.jar was not found in Server/ subdirectory") + } + + if (!hytaleServerJar.asFile.exists()) { + throw GradleException("HytaleServer.jar was not found in hytale.zip") + } + + hytaleZip.get().asFile.delete() + println("Deleted hytale.zip after extracting HytaleServer.jar") + } else { + throw GradleException( + "hytale.zip not found at ${hytaleZip.get().asFile.absolutePath}. " + + "The downloader may not have completed successfully." + ) + } + } +} + +tasks.register("update-server") { + group = "hytale" + hytaleServerJar.asFile.delete() + hytaleZip.get().asFile.delete() + dependsOn(tasks.named("download-server")) +} + +tasks.compileJava { + dependsOn(tasks.named("download-server")) +} \ No newline at end of file diff --git a/hytale/example-plugin/build.gradle.kts b/hytale/example-plugin/build.gradle.kts new file mode 100644 index 0000000..711ae57 --- /dev/null +++ b/hytale/example-plugin/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + id("com.gradleup.shadow") version "9.3.1" +} + +val libsDir: Directory = project(":hytale").layout.projectDirectory.dir("libs") +val hytaleServerJar: RegularFile = libsDir.file("HytaleServer.jar") + +dependencies { + compileOnly(files(hytaleServerJar)) + implementation(project(":hytale")) +} + +tasks.shadowJar { + // optionally relocate faststats + relocate("dev.faststats", "com.example.utils.faststats") +} diff --git a/hytale/example-plugin/src/main/java/com/example/ExamplePlugin.java b/hytale/example-plugin/src/main/java/com/example/ExamplePlugin.java new file mode 100644 index 0000000..4b5030f --- /dev/null +++ b/hytale/example-plugin/src/main/java/com/example/ExamplePlugin.java @@ -0,0 +1,37 @@ +package com.example; + +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import dev.faststats.hytale.HytaleMetrics; +import dev.faststats.core.Metrics; +import dev.faststats.core.chart.Chart; +import com.hypixel.hytale.server.core.plugin.JavaPlugin; + +import java.net.URI; + +public class ExamplePlugin extends JavaPlugin { + private final Metrics metrics = HytaleMetrics.factory() + .url(URI.create("https://metrics.example.com/v1/collect")) // For self-hosted metrics servers only + + // Custom example charts + // For this to work you have to create a corresponding data source in your project settings first + .addChart(Chart.number("example_chart", () -> 42)) + .addChart(Chart.string("example_string", () -> "Hello, World!")) + .addChart(Chart.bool("example_boolean", () -> true)) + .addChart(Chart.stringArray("example_string_array", () -> new String[]{"Option 1", "Option 2"})) + .addChart(Chart.numberArray("example_number_array", () -> new Number[]{1, 2, 3})) + .addChart(Chart.booleanArray("example_boolean_array", () -> new Boolean[]{true, false})) + + .debug(true) // Enable debug mode for development and testing + + .token("YOUR_TOKEN_HERE") // required -> token can be found in the settings of your project + .create(this); + + public ExamplePlugin(JavaPluginInit init) { + super(init); + } + + @Override + protected void shutdown() { + metrics.shutdown(); + } +} diff --git a/hytale/example-plugin/src/main/resources/manifest.json b/hytale/example-plugin/src/main/resources/manifest.json new file mode 100644 index 0000000..8b5babd --- /dev/null +++ b/hytale/example-plugin/src/main/resources/manifest.json @@ -0,0 +1,16 @@ +{ + "group": "com.example", + "name": "Example Plugin", + "version": "1.0.0", + "description": "An example plugin for Hytale", + "authors": [ + { + "name": "Your Name", + "email": "yourname@example.com", + "url": "https://yourname.example.com" + } + ], + "website": "https://example.com/example-plugin", + "serverVersion": "*", + "main": "com.example.ExamplePlugin" +} \ No newline at end of file diff --git a/hytale/src/main/java/dev/faststats/hytale/HytaleMetrics.java b/hytale/src/main/java/dev/faststats/hytale/HytaleMetrics.java new file mode 100644 index 0000000..0e69e4c --- /dev/null +++ b/hytale/src/main/java/dev/faststats/hytale/HytaleMetrics.java @@ -0,0 +1,23 @@ +package dev.faststats.hytale; + +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import dev.faststats.core.Metrics; +import org.jetbrains.annotations.Contract; + +/** + * Hytale metrics implementation. + * + * @since 0.9.0 + */ +public sealed interface HytaleMetrics extends Metrics permits HytaleMetricsImpl { + /** + * Creates a new metrics factory for Hytale. + * + * @return the metrics factory + * @since 0.9.0 + */ + @Contract(pure = true) + static Metrics.Factory factory() { + return new HytaleMetricsImpl.Factory(); + } +} diff --git a/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java b/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java new file mode 100644 index 0000000..44c6687 --- /dev/null +++ b/hytale/src/main/java/dev/faststats/hytale/HytaleMetricsImpl.java @@ -0,0 +1,58 @@ +package dev.faststats.hytale; + +import com.google.gson.JsonObject; +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.server.core.HytaleServer; +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.universe.Universe; +import dev.faststats.core.Metrics; +import dev.faststats.core.SimpleMetrics; +import org.jetbrains.annotations.Async; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.Nullable; + +import java.nio.file.Path; +import java.util.logging.Level; + +final class HytaleMetricsImpl extends SimpleMetrics implements HytaleMetrics { + private final HytaleLogger logger; + + @Async.Schedule + @Contract(mutates = "io") + private HytaleMetricsImpl(SimpleMetrics.Factory factory, HytaleLogger logger, Path config) throws IllegalStateException { + super(factory, config); + this.logger = logger; + + startSubmitting(); + } + + @Override + protected void appendDefaultData(JsonObject charts) { + charts.addProperty("server_version", HytaleServer.get().getServerName()); + charts.addProperty("player_count", Universe.get().getPlayerCount()); + charts.addProperty("server_type", "Hytale"); + } + + @Override + protected void printError(String message, @Nullable Throwable throwable) { + logger.atSevere().log(message, throwable); + } + + @Override + protected void printInfo(String message) { + logger.atInfo().log(message); + } + + @Override + protected void printWarning(String message) { + logger.atWarning().log(message); + } + + static final class Factory extends SimpleMetrics.Factory { + @Override + public Metrics create(JavaPlugin plugin) throws IllegalStateException { + var config = plugin.getDataDirectory().resolve("faststats").resolve("config.properties"); + return new HytaleMetricsImpl(this, plugin.getLogger(), config); + } + } +} diff --git a/hytale/src/main/java/module-info.java b/hytale/src/main/java/module-info.java new file mode 100644 index 0000000..de160fd --- /dev/null +++ b/hytale/src/main/java/module-info.java @@ -0,0 +1,13 @@ +import org.jspecify.annotations.NullMarked; + +@NullMarked +module dev.faststats.hytale { + exports dev.faststats.hytale; + + requires com.google.gson; + requires dev.faststats.core; + requires java.logging; + + requires static org.jetbrains.annotations; + requires static org.jspecify; +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index e5cd200..4546526 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,6 +7,8 @@ include("bukkit") include("bukkit:example-plugin") include("bungeecord") include("core") +include("hytale") +include("hytale:example-plugin") include("minestom") include("nukkit") include("velocity") \ No newline at end of file