From 4b4abc0a1cbfb2f349669bf765536c65c686a1bb Mon Sep 17 00:00:00 2001
From: LeeGodSRC
Date: Tue, 20 Jan 2026 17:44:36 +0800
Subject: [PATCH 01/20] feat: add Hytale platform support with bootstrap,
platform module, and gradle plugin
---
.../bootstrap/type/PlatformType.java | 3 +-
.../hytale-bootstrap/build.gradle.kts | 8 +
.../hytale/HytalePlatformBootstrap.java | 60 +++++
.../bootstrap/hytale/HytalePlugin.java | 163 ++++++++++++++
.../bootstrap/hytale/HytalePluginAction.java | 58 +++++
.../hytale/HytalePluginInstance.java | 46 ++++
framework/bootstraps/settings.gradle.kts | 1 +
.../java/io/fairyproject/PlatformType.java | 3 +-
.../hytale-platform/build.gradle.kts | 9 +
.../hytale/FairyHytalePlatform.java | 105 +++++++++
.../hytale/plugin/HytalePluginHandler.java | 44 ++++
framework/platforms/settings.gradle.kts | 1 +
.../fairyproject/gradle/FairyGradlePlugin.kt | 2 +
.../gradle/extension/FairyExtension.kt | 10 +
.../gradle/extension/property/Properties.kt | 66 ++++++
.../gradle/platform/PlatformType.kt | 3 +-
.../gradle/resource/FairyResource.kt | 8 +-
.../gradle/resource/FairyResourceAction.kt | 16 +-
.../gradle/resource/FairyResourcePlugin.kt | 13 +-
.../resource/impl/FairyResourceHytaleMeta.kt | 113 ++++++++++
.../runner/hytale/HytaleServerArtifact.kt | 70 ++++++
.../runner/hytale/RunHytaleServerExtension.kt | 83 +++++++
.../runner/hytale/RunHytaleServerPlugin.kt | 208 ++++++++++++++++++
.../hytale/action/CopyHytaleSnapshotAction.kt | 62 ++++++
.../hytale/task/PrepareHytaleBuildTask.kt | 194 ++++++++++++++++
.../task/PrepareHytaleDownloaderTask.kt | 161 ++++++++++++++
.../runner/hytale/task/RunHytaleServerTask.kt | 68 ++++++
27 files changed, 1568 insertions(+), 10 deletions(-)
create mode 100644 framework/bootstraps/hytale-bootstrap/build.gradle.kts
create mode 100644 framework/bootstraps/hytale-bootstrap/src/main/java/io/fairyproject/bootstrap/hytale/HytalePlatformBootstrap.java
create mode 100644 framework/bootstraps/hytale-bootstrap/src/main/java/io/fairyproject/bootstrap/hytale/HytalePlugin.java
create mode 100644 framework/bootstraps/hytale-bootstrap/src/main/java/io/fairyproject/bootstrap/hytale/HytalePluginAction.java
create mode 100644 framework/bootstraps/hytale-bootstrap/src/main/java/io/fairyproject/bootstrap/hytale/HytalePluginInstance.java
create mode 100644 framework/platforms/hytale-platform/build.gradle.kts
create mode 100644 framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/FairyHytalePlatform.java
create mode 100644 framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/plugin/HytalePluginHandler.java
create mode 100644 gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/impl/FairyResourceHytaleMeta.kt
create mode 100644 gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/HytaleServerArtifact.kt
create mode 100644 gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/RunHytaleServerExtension.kt
create mode 100644 gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/RunHytaleServerPlugin.kt
create mode 100644 gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/action/CopyHytaleSnapshotAction.kt
create mode 100644 gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/task/PrepareHytaleBuildTask.kt
create mode 100644 gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/task/PrepareHytaleDownloaderTask.kt
create mode 100644 gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/task/RunHytaleServerTask.kt
diff --git a/framework/bootstraps/core-bootstrap/src/main/java/io/fairyproject/bootstrap/type/PlatformType.java b/framework/bootstraps/core-bootstrap/src/main/java/io/fairyproject/bootstrap/type/PlatformType.java
index b9b473a9..510568e3 100644
--- a/framework/bootstraps/core-bootstrap/src/main/java/io/fairyproject/bootstrap/type/PlatformType.java
+++ b/framework/bootstraps/core-bootstrap/src/main/java/io/fairyproject/bootstrap/type/PlatformType.java
@@ -6,6 +6,7 @@ public enum PlatformType {
BUNGEE,
VELOCITY,
NUKKIT,
- APP
+ APP,
+ HYTALE
}
diff --git a/framework/bootstraps/hytale-bootstrap/build.gradle.kts b/framework/bootstraps/hytale-bootstrap/build.gradle.kts
new file mode 100644
index 00000000..1c189acb
--- /dev/null
+++ b/framework/bootstraps/hytale-bootstrap/build.gradle.kts
@@ -0,0 +1,8 @@
+plugins {
+ id("io.fairyproject.bootstrap")
+}
+
+dependencies {
+ api(project(":core-bootstrap"))
+ compileOnly("io.fairyproject:hytale-platform")
+}
\ No newline at end of file
diff --git a/framework/bootstraps/hytale-bootstrap/src/main/java/io/fairyproject/bootstrap/hytale/HytalePlatformBootstrap.java b/framework/bootstraps/hytale-bootstrap/src/main/java/io/fairyproject/bootstrap/hytale/HytalePlatformBootstrap.java
new file mode 100644
index 00000000..7d304c2c
--- /dev/null
+++ b/framework/bootstraps/hytale-bootstrap/src/main/java/io/fairyproject/bootstrap/hytale/HytalePlatformBootstrap.java
@@ -0,0 +1,60 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Fairy Project
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.fairyproject.bootstrap.hytale;
+
+import io.fairyproject.FairyPlatform;
+import io.fairyproject.bootstrap.platform.AbstractPlatformBootstrap;
+import io.fairyproject.bootstrap.type.PlatformType;
+import io.fairyproject.hytale.FairyHytalePlatform;
+import lombok.Getter;
+import org.jetbrains.annotations.Nullable;
+
+@Getter
+class HytalePlatformBootstrap extends AbstractPlatformBootstrap {
+
+ @Override
+ protected void onFailure(@Nullable Throwable throwable) {
+ if (throwable != null) {
+ throwable.printStackTrace();
+ }
+ if (HytalePlugin.INSTANCE != null && HytalePlugin.INSTANCE.getShutdownAction() != null) {
+ HytalePlugin.INSTANCE.getShutdownAction().run();
+ }
+ }
+
+ @Override
+ protected PlatformType getPlatformType() {
+ return PlatformType.HYTALE;
+ }
+
+ @Override
+ protected FairyPlatform createPlatform() {
+ return new FairyHytalePlatform(
+ HytalePlugin.INSTANCE,
+ HytalePlugin.INSTANCE.getShutdownAction(),
+ HytalePlugin.INSTANCE.getDataDirectory().toFile()
+ );
+ }
+}
diff --git a/framework/bootstraps/hytale-bootstrap/src/main/java/io/fairyproject/bootstrap/hytale/HytalePlugin.java b/framework/bootstraps/hytale-bootstrap/src/main/java/io/fairyproject/bootstrap/hytale/HytalePlugin.java
new file mode 100644
index 00000000..e77926b7
--- /dev/null
+++ b/framework/bootstraps/hytale-bootstrap/src/main/java/io/fairyproject/bootstrap/hytale/HytalePlugin.java
@@ -0,0 +1,163 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Fairy Project
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.fairyproject.bootstrap.hytale;
+
+import com.google.gson.JsonObject;
+import com.hypixel.hytale.server.core.plugin.JavaPlugin;
+import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
+import io.fairyproject.bootstrap.PluginClassInitializerFinder;
+import io.fairyproject.bootstrap.PluginFileReader;
+import io.fairyproject.bootstrap.instance.PluginInstance;
+import io.fairyproject.bootstrap.internal.FairyInternalIdentityMeta;
+import io.fairyproject.bootstrap.platform.PlatformBootstrap;
+import lombok.AccessLevel;
+import lombok.Setter;
+import org.jetbrains.annotations.NotNull;
+
+@FairyInternalIdentityMeta
+public final class HytalePlugin extends JavaPlugin {
+
+ public static HytalePlugin INSTANCE;
+
+ private final PluginInstance instance;
+ private final PluginFileReader pluginFileReader;
+ private final PlatformBootstrap bootstrap;
+ private Runnable shutdownAction;
+
+ @Setter(AccessLevel.PACKAGE)
+ private boolean loaded;
+
+ /**
+ * Constructor with dependency injection for testing purposes.
+ *
+ * @param init the Hytale plugin init object
+ * @param instance the plugin instance
+ * @param pluginFileReader the plugin file reader
+ * @param bootstrap the platform bootstrap
+ * @param shutdownAction the action to run on shutdown (nullable, will use default if null)
+ */
+ public HytalePlugin(@NotNull JavaPluginInit init,
+ PluginInstance instance,
+ PluginFileReader pluginFileReader,
+ PlatformBootstrap bootstrap,
+ Runnable shutdownAction) {
+ super(init);
+ this.instance = instance;
+ this.pluginFileReader = pluginFileReader;
+ this.bootstrap = bootstrap;
+ this.shutdownAction = shutdownAction;
+ }
+
+ /**
+ * Default constructor for Hytale server.
+ *
+ * @param init the Hytale plugin init object
+ */
+ public HytalePlugin(@NotNull JavaPluginInit init) {
+ this(
+ init,
+ new HytalePluginInstance(PluginClassInitializerFinder.find()),
+ new PluginFileReader(),
+ new HytalePlatformBootstrap(),
+ null // will be initialized lazily
+ );
+ }
+
+ /**
+ * Get the shutdown action to be used by the platform.
+ * Lazily initializes to server stop if not set.
+ *
+ * @return the shutdown action
+ */
+ public Runnable getShutdownAction() {
+ if (this.shutdownAction == null) {
+ // Use reflection to call server stop to avoid compile-time API dependency issues
+ this.shutdownAction = () -> {
+ try {
+ Object server = this.getClass().getMethod("getServer").invoke(this);
+ if (server != null) {
+ server.getClass().getMethod("stop").invoke(server);
+ }
+ } catch (Exception e) {
+ System.err.println("[Fairy] Failed to stop server: " + e.getMessage());
+ }
+ };
+ }
+ return this.shutdownAction;
+ }
+
+ /**
+ * Called by Hytale during plugin setup phase.
+ * Maps to Bukkit's onLoad() lifecycle.
+ * Performs: preload + load + instance.onLoad
+ */
+ @Override
+ protected void setup() {
+ if (this.loaded)
+ return;
+ INSTANCE = this;
+
+ if (!this.bootstrap.preload()) {
+ System.err.println("[Fairy] Failed to boot fairy! check stacktrace for the reason of failure!");
+ this.getShutdownAction().run();
+ return;
+ }
+
+ JsonObject jsonObject = pluginFileReader.read(this.getClass());
+ this.instance.init(jsonObject);
+ this.bootstrap.load(this.instance.getPlugin());
+ this.instance.onLoad();
+
+ this.loaded = true;
+ }
+
+ /**
+ * Called by Hytale during plugin start phase.
+ * Maps to Bukkit's onEnable() lifecycle.
+ * Performs: enable + instance.onEnable
+ */
+ @Override
+ protected void start() {
+ if (!this.loaded)
+ throw new IllegalStateException("Plugin not loaded yet!");
+
+ this.bootstrap.enable();
+ this.instance.onEnable();
+ }
+
+ /**
+ * Called by Hytale during plugin shutdown phase.
+ * Maps to Bukkit's onDisable() lifecycle.
+ * Performs: instance.onDisable + disable
+ */
+ @Override
+ protected void shutdown() {
+ if (!this.loaded)
+ return;
+
+ this.instance.onDisable();
+ this.bootstrap.disable();
+ }
+}
diff --git a/framework/bootstraps/hytale-bootstrap/src/main/java/io/fairyproject/bootstrap/hytale/HytalePluginAction.java b/framework/bootstraps/hytale-bootstrap/src/main/java/io/fairyproject/bootstrap/hytale/HytalePluginAction.java
new file mode 100644
index 00000000..c9cfbf35
--- /dev/null
+++ b/framework/bootstraps/hytale-bootstrap/src/main/java/io/fairyproject/bootstrap/hytale/HytalePluginAction.java
@@ -0,0 +1,58 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Fairy Project
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.fairyproject.bootstrap.hytale;
+
+import com.hypixel.hytale.server.core.plugin.JavaPlugin;
+import io.fairyproject.plugin.PluginAction;
+
+import java.nio.file.Path;
+
+public class HytalePluginAction implements PluginAction {
+
+ private final JavaPlugin hytalePlugin;
+ private boolean closed;
+
+ public HytalePluginAction(JavaPlugin hytalePlugin) {
+ this.hytalePlugin = hytalePlugin;
+ this.closed = false;
+ }
+
+ @Override
+ public void close() {
+ this.closed = true;
+ // Hytale doesn't have a direct plugin disable mechanism like Bukkit
+ // The plugin lifecycle is managed by the server
+ }
+
+ @Override
+ public boolean isClosed() {
+ return this.closed;
+ }
+
+ @Override
+ public Path getDataFolder() {
+ return this.hytalePlugin.getDataDirectory();
+ }
+}
diff --git a/framework/bootstraps/hytale-bootstrap/src/main/java/io/fairyproject/bootstrap/hytale/HytalePluginInstance.java b/framework/bootstraps/hytale-bootstrap/src/main/java/io/fairyproject/bootstrap/hytale/HytalePluginInstance.java
new file mode 100644
index 00000000..d2c1664a
--- /dev/null
+++ b/framework/bootstraps/hytale-bootstrap/src/main/java/io/fairyproject/bootstrap/hytale/HytalePluginInstance.java
@@ -0,0 +1,46 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Fairy Project
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.fairyproject.bootstrap.hytale;
+
+import io.fairyproject.bootstrap.instance.AbstractPluginInstance;
+import io.fairyproject.plugin.PluginAction;
+import io.fairyproject.plugin.initializer.PluginClassInitializer;
+
+final class HytalePluginInstance extends AbstractPluginInstance {
+
+ public HytalePluginInstance(PluginClassInitializer initializer) {
+ super(initializer);
+ }
+
+ @Override
+ protected ClassLoader getClassLoader() {
+ return HytalePlugin.INSTANCE.getClass().getClassLoader();
+ }
+
+ @Override
+ protected PluginAction getPluginAction() {
+ return new HytalePluginAction(HytalePlugin.INSTANCE);
+ }
+}
diff --git a/framework/bootstraps/settings.gradle.kts b/framework/bootstraps/settings.gradle.kts
index 09cd1d38..94a86293 100644
--- a/framework/bootstraps/settings.gradle.kts
+++ b/framework/bootstraps/settings.gradle.kts
@@ -11,4 +11,5 @@ includeBuild("../tests")
include(":core-bootstrap")
include(":bukkit-bootstrap")
include(":app-bootstrap")
+include(":hytale-bootstrap")
include(":bootstrap-bom")
\ No newline at end of file
diff --git a/framework/platforms/core-platform/src/main/java/io/fairyproject/PlatformType.java b/framework/platforms/core-platform/src/main/java/io/fairyproject/PlatformType.java
index d6f2ad29..2738e13c 100644
--- a/framework/platforms/core-platform/src/main/java/io/fairyproject/PlatformType.java
+++ b/framework/platforms/core-platform/src/main/java/io/fairyproject/PlatformType.java
@@ -7,6 +7,7 @@ public enum PlatformType {
MC,
VELOCITY,
NUKKIT,
- APP
+ APP,
+ HYTALE
}
diff --git a/framework/platforms/hytale-platform/build.gradle.kts b/framework/platforms/hytale-platform/build.gradle.kts
new file mode 100644
index 00000000..3df2d311
--- /dev/null
+++ b/framework/platforms/hytale-platform/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ id("io.fairyproject.platform")
+}
+
+
+dependencies {
+ api(project(":core-platform"))
+ compileOnlyApi("dev.imanity.hytale:HytaleServer:2026.01.17-2")
+}
diff --git a/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/FairyHytalePlatform.java b/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/FairyHytalePlatform.java
new file mode 100644
index 00000000..cea17062
--- /dev/null
+++ b/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/FairyHytalePlatform.java
@@ -0,0 +1,105 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Fairy Project
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.fairyproject.hytale;
+
+import com.hypixel.hytale.server.core.plugin.PluginBase;
+import io.fairyproject.FairyPlatform;
+import io.fairyproject.PlatformType;
+import io.fairyproject.hytale.plugin.HytalePluginHandler;
+import io.fairyproject.plugin.PluginManager;
+import io.fairyproject.util.URLClassLoaderAccess;
+import io.fairyproject.util.terminable.Terminable;
+import io.fairyproject.util.terminable.TerminableConsumer;
+import io.fairyproject.util.terminable.composite.CompositeTerminable;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.net.URLClassLoader;
+
+public class FairyHytalePlatform extends FairyPlatform implements TerminableConsumer {
+
+ public static PluginBase PLUGIN;
+ private static Runnable shutdownCallback;
+
+ private final URLClassLoaderAccess classLoader;
+ private final File dataFolder;
+ private final CompositeTerminable compositeTerminable;
+
+ public FairyHytalePlatform(PluginBase plugin, Runnable shutdownCallback, File dataFolder) {
+ FairyPlatform.INSTANCE = this;
+ PLUGIN = plugin;
+ FairyHytalePlatform.shutdownCallback = shutdownCallback;
+
+ this.dataFolder = dataFolder;
+ this.compositeTerminable = CompositeTerminable.create();
+ ClassLoader classLoader = this.getClass().getClassLoader();
+ if (classLoader instanceof URLClassLoader) {
+ this.classLoader = URLClassLoaderAccess.create((URLClassLoader) classLoader);
+ } else {
+ this.classLoader = URLClassLoaderAccess.create(null);
+ }
+
+ PluginManager.initialize(new HytalePluginHandler());
+ }
+
+ @NotNull
+ @Override
+ public T bind(@NotNull T terminable) {
+ return this.compositeTerminable.bind(terminable);
+ }
+
+ @Override
+ public void saveResource(String name, boolean replace) {
+ // Hytale doesn't have a built-in saveResource mechanism like Bukkit
+ // This can be implemented later if needed
+ }
+
+ @Override
+ public URLClassLoaderAccess getClassloader() {
+ return this.classLoader;
+ }
+
+ @Override
+ public File getDataFolder() {
+ return this.dataFolder;
+ }
+
+ @Override
+ public void shutdown() {
+ if (shutdownCallback != null) {
+ shutdownCallback.run();
+ }
+ }
+
+ @Override
+ public boolean isRunning() {
+ return true;
+ }
+
+ @Override
+ public PlatformType getPlatformType() {
+ return PlatformType.HYTALE;
+ }
+}
diff --git a/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/plugin/HytalePluginHandler.java b/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/plugin/HytalePluginHandler.java
new file mode 100644
index 00000000..f619fe95
--- /dev/null
+++ b/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/plugin/HytalePluginHandler.java
@@ -0,0 +1,44 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Fairy Project
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.fairyproject.hytale.plugin;
+
+import io.fairyproject.hytale.FairyHytalePlatform;
+import io.fairyproject.plugin.PluginHandler;
+import org.jetbrains.annotations.Nullable;
+
+public class HytalePluginHandler implements PluginHandler {
+
+ @Override
+ public @Nullable String getPluginByClass(Class> type) {
+ // Hytale doesn't have a built-in mechanism to resolve plugin by class like Bukkit's JavaPluginLoader
+ // Return the main plugin name as fallback
+ if (FairyHytalePlatform.PLUGIN != null) {
+ // getIdentifier() returns ResourceLocation in "Group:Name" format
+ return FairyHytalePlatform.PLUGIN.getIdentifier().toString();
+ }
+ return null;
+ }
+
+}
diff --git a/framework/platforms/settings.gradle.kts b/framework/platforms/settings.gradle.kts
index f10968bd..c32bf4d8 100644
--- a/framework/platforms/settings.gradle.kts
+++ b/framework/platforms/settings.gradle.kts
@@ -11,4 +11,5 @@ include(":core-platform")
include(":app-platform")
include(":mc-platform")
include(":bukkit-platform")
+include(":hytale-platform")
include(":platforms-bom")
diff --git a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/FairyGradlePlugin.kt b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/FairyGradlePlugin.kt
index 1026e843..4b26211e 100644
--- a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/FairyGradlePlugin.kt
+++ b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/FairyGradlePlugin.kt
@@ -6,6 +6,7 @@ import io.fairyproject.gradle.dependency.DependencyManagementPluginAction
import io.fairyproject.gradle.extension.FairyExtension
import io.fairyproject.gradle.resource.FairyResourcePlugin
import io.fairyproject.gradle.runner.RunServerPlugin
+import io.fairyproject.gradle.runner.hytale.RunHytaleServerPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.GroovyPlugin
@@ -31,6 +32,7 @@ class FairyGradlePlugin : Plugin {
project.plugins.apply(JavaBasePlugin::class.java)
project.plugins.apply(FairyResourcePlugin::class.java)
project.plugins.apply(RunServerPlugin::class.java)
+ project.plugins.apply(RunHytaleServerPlugin::class.java)
sourceSets = project.extensions.getByType(JavaPluginExtension::class.java).sourceSets
project.plugins.withType(JavaPlugin::class.java) { configurePlugin(project, "java") }
diff --git a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/extension/FairyExtension.kt b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/extension/FairyExtension.kt
index 8b9a585e..a7521449 100644
--- a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/extension/FairyExtension.kt
+++ b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/extension/FairyExtension.kt
@@ -1,6 +1,7 @@
package io.fairyproject.gradle.extension
import io.fairyproject.gradle.extension.property.BukkitProperties
+import io.fairyproject.gradle.extension.property.HytaleProperties
import io.fairyproject.gradle.extension.property.Properties
import io.fairyproject.gradle.platform.PlatformType
import org.gradle.api.model.ObjectFactory
@@ -28,5 +29,14 @@ open class FairyExtension(objectFactory: ObjectFactory) {
*/
fun bukkitPropertiesRaw(): Map = this.properties.computeIfAbsent(PlatformType.BUKKIT) { BukkitProperties() }
+ /**
+ * Get properties for hytale platform
+ */
+ fun hytaleProperties(): HytaleProperties = this.properties.computeIfAbsent(PlatformType.HYTALE) { HytaleProperties() } as HytaleProperties
+
+ /**
+ * Get properties for hytale platform in raw format
+ */
+ fun hytalePropertiesRaw(): Map = this.properties.computeIfAbsent(PlatformType.HYTALE) { HytaleProperties() }
}
\ No newline at end of file
diff --git a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/extension/property/Properties.kt b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/extension/property/Properties.kt
index cab01d8e..3da95093 100644
--- a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/extension/property/Properties.kt
+++ b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/extension/property/Properties.kt
@@ -63,4 +63,70 @@ class BukkitProperties : Properties(PlatformType.BUKKIT) {
list
}
+}
+
+/**
+ * Author information for Hytale plugin.
+ */
+data class HytaleAuthor(
+ var name: String = "",
+ var email: String = "",
+ var url: String = ""
+)
+
+/**
+ * The properties of a Hytale plugin.
+ */
+class HytaleProperties : Properties(PlatformType.HYTALE) {
+
+ var group: String
+ get() = this["Group"] as? String ?: ""
+ set(value) { this["Group"] = value }
+
+ var website: String
+ get() = this["Website"] as? String ?: ""
+ set(value) { this["Website"] = value }
+
+ var serverVersion: String
+ get() = this["ServerVersion"] as? String ?: ""
+ set(value) { this["ServerVersion"] = value }
+
+ var disabledByDefault: Boolean
+ get() = this["DisabledByDefault"] as? Boolean ?: false
+ set(value) { this["DisabledByDefault"] = value }
+
+ var includesAssetPack: Boolean
+ get() = this["IncludesAssetPack"] as? Boolean ?: true
+ set(value) { this["IncludesAssetPack"] = value }
+
+ val authors: MutableList by lazy {
+ val list = mutableListOf()
+ this["Authors"] = list
+ list
+ }
+
+ val dependencies: MutableMap by lazy {
+ val map = mutableMapOf()
+ this["Dependencies"] = map
+ map
+ }
+
+ val optionalDependencies: MutableMap by lazy {
+ val map = mutableMapOf()
+ this["OptionalDependencies"] = map
+ map
+ }
+
+ val loadBefore: MutableMap by lazy {
+ val map = mutableMapOf()
+ this["LoadBefore"] = map
+ map
+ }
+
+ val subPlugins: MutableList by lazy {
+ val list = mutableListOf()
+ this["SubPlugins"] = list
+ list
+ }
+
}
\ No newline at end of file
diff --git a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/platform/PlatformType.kt b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/platform/PlatformType.kt
index b403657b..3c0764b4 100644
--- a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/platform/PlatformType.kt
+++ b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/platform/PlatformType.kt
@@ -6,7 +6,8 @@ package io.fairyproject.gradle.platform
enum class PlatformType {
BUKKIT,
APP,
- CORE;
+ CORE,
+ HYTALE;
val dependencyName: String
get() = name.lowercase()
diff --git a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/FairyResource.kt b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/FairyResource.kt
index 5bdfc28f..f821137e 100644
--- a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/FairyResource.kt
+++ b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/FairyResource.kt
@@ -1,6 +1,7 @@
package io.fairyproject.gradle.resource
import io.fairyproject.gradle.resource.impl.FairyResourceBukkitMeta
+import io.fairyproject.gradle.resource.impl.FairyResourceHytaleMeta
import io.fairyproject.gradle.resource.impl.FairyResourcePluginMeta
/**
@@ -20,7 +21,8 @@ interface FairyResource {
val ALL = arrayOf(
FairyResourcePluginMeta(),
- FairyResourceBukkitMeta()
+ FairyResourceBukkitMeta(),
+ FairyResourceHytaleMeta()
)
}
@@ -35,10 +37,12 @@ data class FairyResourceGenerateContext(
val projectVersion: String,
val projectDescription: String,
val hasBukkitPlatform: Boolean,
+ val hasHytalePlatform: Boolean,
private val _pluginName: String?,
val mainPackage: String?,
val fairyPackage: String?,
- val props: Map
+ val props: Map,
+ val hytaleProps: Map
) {
val pluginName: String
get() = _pluginName ?: projectName
diff --git a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/FairyResourceAction.kt b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/FairyResourceAction.kt
index e1a25855..71be3dae 100644
--- a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/FairyResourceAction.kt
+++ b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/FairyResourceAction.kt
@@ -31,6 +31,9 @@ abstract class FairyResourceAction @Inject constructor() : Action {
@get:Input
abstract val hasBukkitPlatform: Property
+ @get:Input
+ abstract val hasHytalePlatform: Property
+
override fun execute(task: Task) {
val jar = task as Jar
val file = jar.archiveFile.get().asFile
@@ -66,8 +69,10 @@ abstract class FairyResourceAction @Inject constructor() : Action {
return
}
- classMapper[ClassType.BUKKIT_PLUGIN] ?: run {
- println("[Fairy] Bukkit plugin class not found, no resources will be generated.")
+ val hasBukkitPluginClass = classMapper.containsKey(ClassType.BUKKIT_PLUGIN)
+ val hasHytalePluginClass = classMapper.containsKey(ClassType.HYTALE_PLUGIN)
+ if (!hasBukkitPluginClass && !hasHytalePluginClass) {
+ println("[Fairy] No platform plugin class found (BukkitPlugin or HytalePlugin), no resources will be generated.")
return
}
@@ -80,10 +85,12 @@ abstract class FairyResourceAction @Inject constructor() : Action {
info.version,
info.description,
hasBukkitPlatform.get(),
+ hasHytalePlatform.get(),
extension.get().name.orNull,
extension.get().mainPackage.orNull,
extension.get().fairyPackage.orNull,
- extension.get().bukkitPropertiesRaw()
+ extension.get().bukkitPropertiesRaw(),
+ extension.get().hytalePropertiesRaw()
), classMapper
)?.let { resource ->
output.putNextEntry(JarEntry(resource.name))
@@ -156,6 +163,7 @@ abstract class FairyResourceAction @Inject constructor() : Action {
private fun shouldExcludeFile(jarEntry: JarEntry): Boolean {
if (jarEntry.name.equals("module.json")) return true
if (jarEntry.name.equals("plugin.yml")) return true
+ if (jarEntry.name.equals("manifest.json")) return true
return false
}
@@ -176,7 +184,7 @@ abstract class FairyResourceAction @Inject constructor() : Action {
* Class type.
*/
enum class ClassType(vararg val names: String) {
- MAIN_CLASS, MAIN_CLASS_INTERFACE("Plugin", "Application"), BUKKIT_PLUGIN("BukkitPlugin");
+ MAIN_CLASS, MAIN_CLASS_INTERFACE("Plugin", "Application"), BUKKIT_PLUGIN("BukkitPlugin"), HYTALE_PLUGIN("HytalePlugin");
}
/**
diff --git a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/FairyResourcePlugin.kt b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/FairyResourcePlugin.kt
index 1642bbfd..0eaa2384 100644
--- a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/FairyResourcePlugin.kt
+++ b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/FairyResourcePlugin.kt
@@ -23,13 +23,20 @@ class FairyResourcePlugin: Plugin {
.any { it.isBukkitPlatform }
}
+ val hasHytalePlatform by lazy {
+ project.configurations
+ .flatMap { it.dependencies }
+ .any { it.isHytalePlatform }
+ }
+
val action = project.objects.newInstance(FairyResourceAction::class.java).apply {
this.extension.set(extension)
- // Set the value of hasBukkitPlatform and projectInfo after the project is evaluated
+ // Set the value of hasBukkitPlatform, hasHytalePlatform and projectInfo after the project is evaluated
project.afterEvaluate {
val projectInfo = ProjectInfo(project.name, project.version.toString(), project.description ?: "")
this.projectInfo.set(projectInfo)
this.hasBukkitPlatform.set(hasBukkitPlatform)
+ this.hasHytalePlatform.set(hasHytalePlatform)
}
}
@@ -39,4 +46,8 @@ class FairyResourcePlugin: Plugin {
private val Dependency.isBukkitPlatform: Boolean
get() = group == "io.fairyproject" &&
name in listOf("bukkit-platform", "bukkit-bundles", "bukkit-bootstrap")
+
+ private val Dependency.isHytalePlatform: Boolean
+ get() = group == "io.fairyproject" &&
+ name in listOf("hytale-platform", "hytale-bundles", "hytale-bootstrap")
}
\ No newline at end of file
diff --git a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/impl/FairyResourceHytaleMeta.kt b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/impl/FairyResourceHytaleMeta.kt
new file mode 100644
index 00000000..5cec9183
--- /dev/null
+++ b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/impl/FairyResourceHytaleMeta.kt
@@ -0,0 +1,113 @@
+package io.fairyproject.gradle.resource.impl
+
+import com.google.gson.GsonBuilder
+import io.fairyproject.gradle.extension.property.HytaleAuthor
+import io.fairyproject.gradle.resource.*
+
+/**
+ * The resource generator for Hytale manifest.json
+ */
+class FairyResourceHytaleMeta : FairyResource {
+
+ private val gson = GsonBuilder().setPrettyPrinting().create()
+
+ override fun generate(
+ context: FairyResourceGenerateContext,
+ classMapper: Map
+ ): ResourceInfo? {
+ if (!context.hasHytalePlatform)
+ return null
+
+ classMapper[ClassType.HYTALE_PLUGIN] ?: return null
+
+ val manifest = mutableMapOf()
+
+ // Set Main class - this is the HytalePlugin class that extends Hytale's JavaPlugin
+ val hytalePluginClass = classMapper[ClassType.HYTALE_PLUGIN]!!
+ manifest["Main"] = hytalePluginClass.name.replace("/", ".")
+
+ // Set Group from hytaleProps or default
+ val group = context.hytaleProps["Group"] as? String
+ manifest["Group"] = if (group.isNullOrEmpty()) "io.fairyproject" else group
+
+ // Set Name from context
+ manifest["Name"] = context.pluginName
+
+ // Set Version from context - must be valid semver format
+ manifest["Version"] = normalizeToSemver(context.projectVersion)
+
+ // Set Description from context
+ manifest["Description"] = context.projectDescription
+
+ // Set Authors
+ val authors = context.hytaleProps["Authors"]
+ manifest["Authors"] = if (authors is List<*> && authors.isNotEmpty()) {
+ authors.map { author ->
+ when (author) {
+ is HytaleAuthor -> mapOf(
+ "Name" to author.name,
+ "Email" to author.email,
+ "Url" to author.url
+ )
+ else -> mapOf("Name" to "", "Email" to "", "Url" to "")
+ }
+ }
+ } else {
+ emptyList
*/
@InjectableComponent
-@RequiredArgsConstructor
public class DefaultHytaleCommandMap implements HytaleCommandMap {
public static final MetaKey EXECUTOR_KEY = MetaKey.create("fairy:hytale-command-executor", AbstractCommand.class);
public static final MetaKey REGISTRATION_KEY = MetaKey.create("fairy:hytale-command-registration", CommandRegistration.class);
+ public DefaultHytaleCommandMap() {
+ System.out.println("DefaultHytaleCommandMap initialized");
+ }
+
@Override
public void register(BaseCommand command) {
if (this.isRegistered(command)) {
diff --git a/framework/platforms/core-platform/src/main/java/io/fairyproject/container/RootNodeLoader.java b/framework/platforms/core-platform/src/main/java/io/fairyproject/container/RootNodeLoader.java
index 3398cd85..57d2476c 100644
--- a/framework/platforms/core-platform/src/main/java/io/fairyproject/container/RootNodeLoader.java
+++ b/framework/platforms/core-platform/src/main/java/io/fairyproject/container/RootNodeLoader.java
@@ -34,6 +34,9 @@
import io.fairyproject.util.Stacktrace;
import lombok.RequiredArgsConstructor;
+import java.net.URL;
+import java.nio.file.Paths;
+
import static io.fairyproject.Debug.log;
@RequiredArgsConstructor
@@ -62,6 +65,9 @@ private void runClassScanner() {
if (!Debug.UNIT_TEST) {
classScanner.getUrls().add(this.getClass().getProtectionDomain().getCodeSource().getLocation());
classScanner.getClassLoaders().add(ContainerContext.class.getClassLoader());
+
+ // Also add URLs from devtools classpath for development mode
+ addDevtoolsClasspathUrls(classScanner);
}
classScanner.scan();
@@ -71,6 +77,30 @@ private void runClassScanner() {
}
}
+ private void addDevtoolsClasspathUrls(ContainerNodeClassScanner classScanner) {
+ String classpathProperty = System.getProperty("io.fairyproject.devtools.classpath");
+ if (classpathProperty == null || classpathProperty.isEmpty()) {
+ return;
+ }
+
+ // Parse format: name1|path1,path2:name2|path3,path4
+ String[] entries = classpathProperty.split(":");
+ for (String entry : entries) {
+ try {
+ String[] parts = entry.split("\\|");
+ if (parts.length < 2) continue;
+
+ String paths = parts[1];
+ for (String path : paths.split(",")) {
+ URL url = Paths.get(path).toUri().toURL();
+ classScanner.getUrls().add(url);
+ }
+ } catch (Exception e) {
+ // Ignore parsing errors
+ }
+ }
+ }
+
private void addPreDefinedComponents() {
ContainerObj obj = ContainerObj.create(this.context.getClass());
this.node.addObj(obj);
diff --git a/framework/platforms/core-platform/src/main/java/io/fairyproject/library/LibraryHandlerPluginListener.java b/framework/platforms/core-platform/src/main/java/io/fairyproject/library/LibraryHandlerPluginListener.java
index 9db40554..6f6ca4d6 100644
--- a/framework/platforms/core-platform/src/main/java/io/fairyproject/library/LibraryHandlerPluginListener.java
+++ b/framework/platforms/core-platform/src/main/java/io/fairyproject/library/LibraryHandlerPluginListener.java
@@ -52,7 +52,13 @@ public void onPluginPreLoaded(ClassLoader classLoader, PluginDescription descrip
@Override
public void onPluginInitial(Plugin plugin) {
- final URLClassLoaderAccess classLoader = URLClassLoaderAccess.create((URLClassLoader) plugin.getPluginClassLoader());
+ ClassLoader pluginClassLoader = plugin.getPluginClassLoader();
+ if (!(pluginClassLoader instanceof URLClassLoader)) {
+ // In Java 9+, the system classloader is not a URLClassLoader
+ // Skip library injection for non-URLClassLoader classloaders
+ return;
+ }
+ final URLClassLoaderAccess classLoader = URLClassLoaderAccess.create((URLClassLoader) pluginClassLoader);
this.libraryHandler.addClassLoader(plugin, classLoader);
}
diff --git a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/RunHytaleServerExtension.kt b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/RunHytaleServerExtension.kt
index 2d152e94..90073719 100644
--- a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/RunHytaleServerExtension.kt
+++ b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/RunHytaleServerExtension.kt
@@ -80,4 +80,19 @@ open class RunHytaleServerExtension(objectFactory: ObjectFactory) {
*/
val downloaderPath: Property = objectFactory.property(String::class.java)
+ /**
+ * Allow OP commands (default true).
+ */
+ val allowOp: Property = objectFactory.property(Boolean::class.java).convention(true)
+
+ /**
+ * Disable Sentry error reporting (default true).
+ */
+ val disableSentry: Property = objectFactory.property(Boolean::class.java).convention(true)
+
+ /**
+ * Authentication mode: "authenticated", "unauthenticated", or "mixed" (default "authenticated").
+ */
+ val authMode: Property = objectFactory.property(String::class.java).convention("authenticated")
+
}
diff --git a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/RunHytaleServerPlugin.kt b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/RunHytaleServerPlugin.kt
index d342ae82..4e37a349 100644
--- a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/RunHytaleServerPlugin.kt
+++ b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/RunHytaleServerPlugin.kt
@@ -27,12 +27,14 @@ package io.fairyproject.gradle.runner.hytale
import io.fairyproject.gradle.FairyGradlePlugin
import io.fairyproject.gradle.runner.ClasspathRegistry
import io.fairyproject.gradle.runner.hytale.action.CopyHytaleSnapshotAction
+import io.fairyproject.gradle.runner.hytale.task.GenerateHytaleManifestTask
import io.fairyproject.gradle.runner.hytale.task.PrepareHytaleBuildTask
import io.fairyproject.gradle.runner.hytale.task.PrepareHytaleDownloaderTask
import io.fairyproject.gradle.runner.hytale.task.RunHytaleServerTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.DuplicatesStrategy
+import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.Delete
import org.gradle.jvm.tasks.Jar
@@ -91,6 +93,7 @@ open class RunHytaleServerPlugin : Plugin {
configurePrepareHytaleBuild(downloaderDir, downloadsDir, workDir, artifact)
configureCopyHytaleModJar(workDir)
configureCleanHytaleServer(workDir)
+ configureGenerateHytaleManifest()
configureRunHytaleServer(artifact, workDir, snapshotDir)
}
@@ -168,6 +171,31 @@ open class RunHytaleServerPlugin : Plugin {
}
}
+ private fun configureGenerateHytaleManifest() {
+ val javaExtension = project.extensions.getByType(JavaPluginExtension::class.java)
+ val mainSourceSet = javaExtension.sourceSets.getByName("main")
+
+ project.tasks.register("generateHytaleManifest", GenerateHytaleManifestTask::class.java) {
+ it.group = group
+ it.description = "Generates Hytale manifest.json for development"
+
+ // Input: compiled classes directory
+ it.classesDir.set(mainSourceSet.output.classesDirs.singleFile)
+
+ // Input: runtime classpath (to find HytalePlugin from hytale-bootstrap)
+ it.runtimeClasspath.from(mainSourceSet.runtimeClasspath)
+
+ // Output: src/main/ so manifest.json is at root of --mods path
+ it.outputDir.set(project.file("src/main/resources"))
+
+ it.projectName.set(project.name)
+ it.projectVersion.set(project.version.toString())
+ it.projectDescription.set(project.description ?: "")
+
+ it.dependsOn("classes")
+ }
+ }
+
private fun configureRunHytaleServer(
artifact: HytaleServerArtifact,
workDir: Path,
@@ -186,8 +214,8 @@ open class RunHytaleServerPlugin : Plugin {
it.dependsOn("cleanHytaleServer")
}
it.doFirst(CopyHytaleSnapshotAction(snapshotDir, workDir))
- it.dependsOn("copyHytaleModJar")
it.dependsOn("prepareHytaleBuild")
+ it.dependsOn("generateHytaleManifest")
it.group = group
it.description = "Runs the Hytale server with mods"
diff --git a/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/task/GenerateHytaleManifestTask.kt b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/task/GenerateHytaleManifestTask.kt
new file mode 100644
index 00000000..3ebed16d
--- /dev/null
+++ b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/runner/hytale/task/GenerateHytaleManifestTask.kt
@@ -0,0 +1,326 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022 Fairy Project
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package io.fairyproject.gradle.runner.hytale.task
+
+import com.google.gson.GsonBuilder
+import io.fairyproject.gradle.constants.ClassConstants
+import io.fairyproject.gradle.extension.FairyExtension
+import io.fairyproject.gradle.extension.property.HytaleAuthor
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Classpath
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.tree.ClassNode
+import java.io.File
+import java.util.jar.JarFile
+
+/**
+ * Task for generating Hytale manifest.json into the build output directory.
+ * This allows running the server with compiled classes instead of a JAR.
+ *
+ * @since 0.7
+ * @author LeeGod
+ */
+abstract class GenerateHytaleManifestTask : DefaultTask() {
+
+ @get:InputDirectory
+ abstract val classesDir: DirectoryProperty
+
+ @get:Classpath
+ abstract val runtimeClasspath: ConfigurableFileCollection
+
+ @get:OutputDirectory
+ abstract val outputDir: DirectoryProperty
+
+ @get:Input
+ abstract val projectName: Property
+
+ @get:Input
+ abstract val projectVersion: Property
+
+ @get:Input
+ abstract val projectDescription: Property
+
+ private val gson = GsonBuilder().setPrettyPrinting().create()
+
+ @TaskAction
+ fun generate() {
+ val extension = project.extensions.findByType(FairyExtension::class.java)
+
+ // Find HytalePlugin class
+ val hytalePluginClass = findHytalePluginClass() ?: run {
+ logger.warn("[Fairy] No HytalePlugin class found, skipping manifest generation")
+ return
+ }
+
+ // Find main class (with @FairyLaunch annotation)
+ val mainClass = findMainClass() ?: run {
+ logger.warn("[Fairy] No main class with @FairyLaunch found, skipping fairy.json generation")
+ return
+ }
+
+ val outputDirectory = outputDir.get().asFile
+ outputDirectory.mkdirs()
+
+ // Generate manifest.json
+ val hytaleProps = extension?.hytalePropertiesRaw() ?: emptyMap()
+ val manifest = buildManifest(hytalePluginClass, hytaleProps, extension)
+ val manifestFile = File(outputDirectory, "manifest.json")
+ manifestFile.writeText(gson.toJson(manifest))
+ logger.lifecycle("[Fairy] Generated Hytale manifest.json at ${manifestFile.absolutePath}")
+
+ // Generate fairy.json
+ val fairyJson = buildFairyJson(mainClass, extension)
+ val fairyFile = File(outputDirectory, "fairy.json")
+ fairyFile.writeText(gson.toJson(fairyJson))
+ logger.lifecycle("[Fairy] Generated fairy.json at ${fairyFile.absolutePath}")
+ }
+
+ private fun findMainClass(): String? {
+ val classesDirectory = classesDir.get().asFile
+ if (!classesDirectory.exists()) return null
+
+ // First, find the Plugin/Application interface class from runtime classpath
+ val pluginInterfaceClass = findPluginInterfaceClass()
+
+ classesDirectory.walkTopDown()
+ .filter { it.isFile && it.extension == "class" }
+ .forEach { classFile ->
+ val className = findMainClassInFile(classFile, pluginInterfaceClass)
+ if (className != null) return className
+ }
+
+ return null
+ }
+
+ private fun findPluginInterfaceClass(): String? {
+ // Look for Plugin or Application class with @FairyInternalIdentityMeta
+ runtimeClasspath.files
+ .filter { it.isFile && it.extension == "jar" }
+ .forEach { jarFile ->
+ try {
+ JarFile(jarFile).use { jar ->
+ for (entry in jar.entries()) {
+ if (!entry.name.endsWith(".class")) continue
+ val simpleName = entry.name.substringAfterLast("/").removeSuffix(".class")
+ if (simpleName != "Plugin" && simpleName != "Application") continue
+
+ val bytes = jar.getInputStream(entry).readBytes()
+ val classReader = ClassReader(bytes)
+ val classNode = ClassNode()
+ classReader.accept(classNode, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES)
+
+ val hasInternalMeta = classNode.visibleAnnotations?.any {
+ it.desc.contains(ClassConstants.INTERNAL_META)
+ } ?: false
+
+ if (hasInternalMeta) {
+ return classNode.name
+ }
+ }
+ }
+ } catch (e: Exception) {
+ // Ignore
+ }
+ }
+ return null
+ }
+
+ private fun findMainClassInFile(classFile: File, pluginInterfaceClass: String?): String? {
+ val bytes = classFile.readBytes()
+ val classReader = ClassReader(bytes)
+ val classNode = ClassNode()
+
+ classReader.accept(
+ classNode,
+ ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES
+ )
+
+ // Skip abstract classes and interfaces
+ if (classNode.access and Opcodes.ACC_ABSTRACT != 0) return null
+ if (classNode.access and Opcodes.ACC_INTERFACE != 0) return null
+
+ // Check if this class has @FairyLaunch annotation
+ val hasFairyLaunch = classNode.visibleAnnotations?.any {
+ it.desc.contains(ClassConstants.FAIRY_LAUNCH)
+ } ?: false
+
+ // Check if this class extends Plugin or Application
+ val extendsPluginInterface = pluginInterfaceClass != null && classNode.superName == pluginInterfaceClass
+
+ if (!hasFairyLaunch && !extendsPluginInterface) return null
+
+ return classNode.name.replace("/", ".")
+ }
+
+ private fun buildFairyJson(mainClass: String, extension: FairyExtension?): Map {
+ val json = mutableMapOf()
+ json["name"] = extension?.name?.orNull ?: projectName.get()
+ json["mainClass"] = mainClass
+ extension?.mainPackage?.orNull?.let { json["shadedPackage"] = it }
+ extension?.fairyPackage?.orNull?.let { json["fairyPackage"] = it }
+ return json
+ }
+
+ private fun findHytalePluginClass(): String? {
+ // First, scan project's compiled classes
+ val classesDirectory = classesDir.get().asFile
+ if (classesDirectory.exists()) {
+ classesDirectory.walkTopDown()
+ .filter { it.isFile && it.extension == "class" }
+ .forEach { classFile ->
+ val className = findHytalePluginInClassFile(classFile)
+ if (className != null) return className
+ }
+ }
+
+ // Then, scan runtime classpath JARs (for HytalePlugin from hytale-bootstrap)
+ runtimeClasspath.files
+ .filter { it.isFile && it.extension == "jar" }
+ .forEach { jarFile ->
+ val className = findHytalePluginInJar(jarFile)
+ if (className != null) return className
+ }
+
+ return null
+ }
+
+ private fun findHytalePluginInJar(jarFile: File): String? {
+ try {
+ JarFile(jarFile).use { jar ->
+ for (entry in jar.entries()) {
+ if (!entry.name.endsWith(".class")) continue
+ if (!entry.name.endsWith("HytalePlugin.class")) continue
+
+ val bytes = jar.getInputStream(entry).readBytes()
+ val className = findHytalePluginInBytes(bytes)
+ if (className != null) return className
+ }
+ }
+ } catch (e: Exception) {
+ // Ignore JAR read errors
+ }
+ return null
+ }
+
+ private fun findHytalePluginInClassFile(classFile: File): String? {
+ return findHytalePluginInBytes(classFile.readBytes())
+ }
+
+ private fun findHytalePluginInBytes(bytes: ByteArray): String? {
+ val classReader = ClassReader(bytes)
+ val classNode = ClassNode()
+
+ classReader.accept(
+ classNode,
+ ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES
+ )
+
+ // Check if this class has @FairyInternalIdentityMeta annotation
+ val hasInternalMeta = classNode.visibleAnnotations?.any {
+ it.desc.contains(ClassConstants.INTERNAL_META)
+ } ?: false
+
+ if (!hasInternalMeta) return null
+
+ // Check if the class name ends with HytalePlugin
+ val simpleName = classNode.name.substringAfterLast("/")
+ if (simpleName != "HytalePlugin") return null
+
+ // Skip abstract classes and interfaces
+ if (classNode.access and Opcodes.ACC_ABSTRACT != 0) return null
+ if (classNode.access and Opcodes.ACC_INTERFACE != 0) return null
+
+ return classNode.name.replace("/", ".")
+ }
+
+ private fun buildManifest(
+ hytalePluginClass: String,
+ hytaleProps: Map,
+ extension: FairyExtension?
+ ): Map {
+ val manifest = mutableMapOf()
+
+ manifest["Main"] = hytalePluginClass
+ manifest["Group"] = (hytaleProps["Group"] as? String)?.takeIf { it.isNotEmpty() } ?: "io.fairyproject"
+ manifest["Name"] = extension?.name?.orNull ?: projectName.get()
+ manifest["Version"] = normalizeToSemver(projectVersion.get())
+ manifest["Description"] = projectDescription.get()
+
+ // Authors
+ val authors = hytaleProps["Authors"]
+ manifest["Authors"] = if (authors is List<*> && authors.isNotEmpty()) {
+ authors.map { author ->
+ when (author) {
+ is HytaleAuthor -> mapOf(
+ "Name" to author.name,
+ "Email" to author.email,
+ "Url" to author.url
+ )
+ else -> mapOf("Name" to "", "Email" to "", "Url" to "")
+ }
+ }
+ } else {
+ emptyList