diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts
index 98505ff0..13be76b2 100644
--- a/build-logic/build.gradle.kts
+++ b/build-logic/build.gradle.kts
@@ -11,16 +11,16 @@ dependencies {
compileOnly(gradleApi())
compileOnly("org.codehaus.groovy:groovy-all:3.0.9")
- implementation("org.yaml:snakeyaml:1.29")
+ implementation("org.yaml:snakeyaml:2.0")
- compileOnly("org.projectlombok:lombok:1.18.32")
- annotationProcessor("org.projectlombok:lombok:1.18.32")
+ compileOnly("org.projectlombok:lombok:1.18.40")
+ annotationProcessor("org.projectlombok:lombok:1.18.40")
compileOnly("org.jetbrains:annotations:23.0.0")
annotationProcessor("org.jetbrains:annotations:23.0.0")
implementation("com.google.code.gson:gson:2.8.9")
- implementation("com.google.guava:guava:32.0.0-android")
- implementation("org.apache.commons:commons-lang3:3.12.0")
- implementation("org.ow2.asm:asm:9.7")
- implementation("org.ow2.asm:asm-commons:9.7")
+ implementation("com.google.guava:guava:32.0.1-android")
+ implementation("org.apache.commons:commons-lang3:3.18.0")
+ implementation("org.ow2.asm:asm:9.9.1")
+ implementation("org.ow2.asm:asm-commons:9.9.1")
}
\ No newline at end of file
diff --git a/build-logic/src/main/java/io/fairyproject/gradle/ProjectTransformPlugin.java b/build-logic/src/main/java/io/fairyproject/gradle/ProjectTransformPlugin.java
index e6b5aef0..a69b669d 100644
--- a/build-logic/src/main/java/io/fairyproject/gradle/ProjectTransformPlugin.java
+++ b/build-logic/src/main/java/io/fairyproject/gradle/ProjectTransformPlugin.java
@@ -19,7 +19,7 @@ public void apply(Project project) {
}
private void configurePlugin(Project project, String language) {
- sourceSets.all(sourceSet -> {
+ sourceSets.configureEach(sourceSet -> {
project.getTasks().named(sourceSet.getCompileTaskName(language), AbstractCompile.class, compile -> {
ModuleCompilerAction action = project.getObjects().newInstance(ModuleCompilerAction.class);
compile.doLast("extraCompile", action);
diff --git a/build-logic/src/main/kotlin/io.fairyproject.bootstrap.gradle.kts b/build-logic/src/main/kotlin/io.fairyproject.bootstrap.gradle.kts
index c4cd556a..ead0ba95 100644
--- a/build-logic/src/main/kotlin/io.fairyproject.bootstrap.gradle.kts
+++ b/build-logic/src/main/kotlin/io.fairyproject.bootstrap.gradle.kts
@@ -4,11 +4,6 @@ plugins {
id("io.fairyproject.publish")
}
-configurations {
- compileOnly {
- isCanBeResolved = true
- }
-}
//sourceSets {
// test.get().compileClasspath += configurations.compileOnly.get()
diff --git a/build-logic/src/main/kotlin/io.fairyproject.common.gradle.kts b/build-logic/src/main/kotlin/io.fairyproject.common.gradle.kts
index 2b3a84eb..4bbfeda5 100644
--- a/build-logic/src/main/kotlin/io.fairyproject.common.gradle.kts
+++ b/build-logic/src/main/kotlin/io.fairyproject.common.gradle.kts
@@ -70,16 +70,16 @@ repositories {
dependencies {
compileOnly("org.jetbrains:annotations:24.1.0")
- compileOnly("org.projectlombok:lombok:1.18.32")
- annotationProcessor("org.projectlombok:lombok:1.18.32")
+ compileOnly("org.projectlombok:lombok:1.18.40")
+ annotationProcessor("org.projectlombok:lombok:1.18.40")
testCompileOnly("org.jetbrains:annotations:24.1.0")
testImplementation("org.mockito:mockito-core:4.2.0")
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.2")
- testCompileOnly("org.projectlombok:lombok:1.18.32")
- testAnnotationProcessor("org.projectlombok:lombok:1.18.32")
+ testCompileOnly("org.projectlombok:lombok:1.18.40")
+ testAnnotationProcessor("org.projectlombok:lombok:1.18.40")
}
java {
diff --git a/build-logic/src/main/kotlin/io.fairyproject.module.hytale.gradle.kts b/build-logic/src/main/kotlin/io.fairyproject.module.hytale.gradle.kts
new file mode 100644
index 00000000..96664679
--- /dev/null
+++ b/build-logic/src/main/kotlin/io.fairyproject.module.hytale.gradle.kts
@@ -0,0 +1,22 @@
+plugins {
+ id("io.fairyproject.module")
+}
+
+dependencies {
+ compileOnly("dev.imanity.hytale:HytaleServer:2026.01.17-2")
+}
+
+repositories {
+ maven ("https://repo.imanity.dev/imanity-libraries/")
+}
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(25))
+ }
+}
+
+tasks.withType(JavaCompile::class.java).configureEach {
+ options.encoding = "UTF-8"
+ options.release = 25
+}
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..d2232d94
--- /dev/null
+++ b/framework/bootstraps/hytale-bootstrap/build.gradle.kts
@@ -0,0 +1,19 @@
+plugins {
+ id("io.fairyproject.bootstrap")
+}
+
+dependencies {
+ api(project(":core-bootstrap"))
+ compileOnly("io.fairyproject:hytale-platform")
+}
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(25))
+ }
+}
+
+tasks.withType(JavaCompile::class.java).configureEach {
+ options.encoding = "UTF-8"
+ options.release = 25
+}
\ 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/modules/bukkit/bukkit-xseries/build.gradle.kts b/framework/modules/bukkit/bukkit-xseries/build.gradle.kts
index 31d6f662..3dbe2106 100644
--- a/framework/modules/bukkit/bukkit-xseries/build.gradle.kts
+++ b/framework/modules/bukkit/bukkit-xseries/build.gradle.kts
@@ -12,7 +12,7 @@ dependencies {
}
tasks {
- withType(JavaCompile::class.java) {
+ withType(JavaCompile::class.java).configureEach {
options.encoding = "UTF-8"
sourceCompatibility = "8"
targetCompatibility = "8"
diff --git a/framework/modules/hytale/build.gradle.kts b/framework/modules/hytale/build.gradle.kts
new file mode 100644
index 00000000..3d670ad0
--- /dev/null
+++ b/framework/modules/hytale/build.gradle.kts
@@ -0,0 +1,3 @@
+plugins {
+ id("io.fairyproject.composite.subprojects")
+}
diff --git a/framework/modules/hytale/hytale-bom/build.gradle.kts b/framework/modules/hytale/hytale-bom/build.gradle.kts
new file mode 100644
index 00000000..59cb3028
--- /dev/null
+++ b/framework/modules/hytale/hytale-bom/build.gradle.kts
@@ -0,0 +1,14 @@
+plugins {
+ id("io.fairyproject.bom")
+}
+
+description = "Fairy Hytale Modules BOM (Bill of materials)"
+
+dependencies {
+ constraints {
+ rootProject.subprojects.forEach {
+ if (it != project)
+ api(project(it.path))
+ }
+ }
+}
diff --git a/framework/modules/hytale/hytale-command/build.gradle.kts b/framework/modules/hytale/hytale-command/build.gradle.kts
new file mode 100644
index 00000000..5da85615
--- /dev/null
+++ b/framework/modules/hytale/hytale-command/build.gradle.kts
@@ -0,0 +1,7 @@
+plugins {
+ id("io.fairyproject.module.hytale")
+}
+
+dependencies {
+ api("io.fairyproject:core-command")
+}
diff --git a/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/HytaleCommandExecutor.java b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/HytaleCommandExecutor.java
new file mode 100644
index 00000000..6b75edd7
--- /dev/null
+++ b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/HytaleCommandExecutor.java
@@ -0,0 +1,92 @@
+/*
+ * 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.command;
+
+import com.hypixel.hytale.server.core.Message;
+import com.hypixel.hytale.server.core.command.system.CommandContext;
+import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
+import io.fairyproject.command.BaseCommand;
+import io.fairyproject.hytale.command.event.HytaleCommandContext;
+import lombok.Getter;
+
+/**
+ * Hytale command executor that bridges Fairy's BaseCommand to Hytale's command system.
+ */
+@Getter
+public class HytaleCommandExecutor extends CommandBase {
+
+ private final BaseCommand fairyCommand;
+
+ public HytaleCommandExecutor(BaseCommand fairyCommand) {
+ super(fairyCommand.getCommandNames()[0], fairyCommand.getDescription());
+ this.fairyCommand = fairyCommand;
+
+ // Add aliases
+ String[] commandNames = fairyCommand.getCommandNames();
+ if (commandNames.length > 1) {
+ String[] aliases = new String[commandNames.length - 1];
+ System.arraycopy(commandNames, 1, aliases, 0, aliases.length);
+ this.addAliases(aliases);
+ }
+
+ // Allow extra arguments since Fairy handles its own argument parsing
+ this.setAllowsExtraArguments(true);
+ }
+
+ @Override
+ protected void executeSync(CommandContext hytaleContext) {
+ // Extract the remaining arguments from the input
+ String inputString = hytaleContext.getInputString();
+ String[] args = extractArgs(inputString);
+
+ io.fairyproject.command.CommandContext fairyContext = new HytaleCommandContext(hytaleContext.sender(), args);
+ try {
+ this.fairyCommand.execute(fairyContext);
+ } catch (Throwable throwable) {
+ this.fairyCommand.onError(fairyContext, throwable);
+ hytaleContext.sendMessage(Message.raw("An error occurred while executing the command.").color("#FF5555"));
+ }
+ }
+
+ /**
+ * Extract arguments from the input string.
+ * The input string contains the full command, so we need to skip the command name.
+ */
+ private String[] extractArgs(String inputString) {
+ if (inputString == null || inputString.isEmpty()) {
+ return new String[0];
+ }
+
+ String[] parts = inputString.split(" ");
+ if (parts.length <= 1) {
+ return new String[0];
+ }
+
+ // Skip the command name and return the rest as arguments
+ String[] args = new String[parts.length - 1];
+ System.arraycopy(parts, 1, args, 0, args.length);
+ return args;
+ }
+}
diff --git a/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/HytaleCommandListener.java b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/HytaleCommandListener.java
new file mode 100644
index 00000000..01fca8a2
--- /dev/null
+++ b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/HytaleCommandListener.java
@@ -0,0 +1,62 @@
+/*
+ * 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.command;
+
+import io.fairyproject.command.BaseCommand;
+import io.fairyproject.command.CommandListener;
+import io.fairyproject.command.CommandService;
+import io.fairyproject.container.InjectableComponent;
+import io.fairyproject.container.PostDestroy;
+import io.fairyproject.container.PreInitialize;
+import io.fairyproject.hytale.command.map.HytaleCommandMap;
+import lombok.RequiredArgsConstructor;
+
+@InjectableComponent
+@RequiredArgsConstructor
+public class HytaleCommandListener implements CommandListener {
+
+ private final CommandService commandService;
+ private final HytaleCommandMap hytaleCommandMap;
+
+ @PreInitialize
+ public void init() {
+ commandService.addCommandListener(this);
+ }
+
+ @PostDestroy
+ public void destroy() {
+ commandService.removeCommandListener(this);
+ }
+
+ @Override
+ public void onCommandInitial(BaseCommand command, String[] aliases) {
+ hytaleCommandMap.register(command);
+ }
+
+ @Override
+ public void onCommandRemoval(BaseCommand command) {
+ hytaleCommandMap.unregister(command);
+ }
+}
diff --git a/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/HytaleCommandModule.java b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/HytaleCommandModule.java
new file mode 100644
index 00000000..9a8ab51e
--- /dev/null
+++ b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/HytaleCommandModule.java
@@ -0,0 +1,50 @@
+/*
+ * 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.command;
+
+import io.fairyproject.command.CommandService;
+import io.fairyproject.container.InjectableComponent;
+import io.fairyproject.container.PreInitialize;
+import io.fairyproject.hytale.command.presence.DefaultPresenceProvider;
+import io.fairyproject.hytale.command.presence.PlayerPresenceProvider;
+import lombok.RequiredArgsConstructor;
+
+@InjectableComponent
+@RequiredArgsConstructor
+public class HytaleCommandModule {
+
+ private final CommandService commandService;
+
+ @PreInitialize
+ public void onPreInitialize() {
+ // Register presence providers for Hytale commands
+ // The command framework uses exact type matching, so we need to register
+ // separate providers for each context type:
+ // - DefaultPresenceProvider for HytaleCommandContext (any sender)
+ // - PlayerPresenceProvider for HytalePlayerCommandContext (player-only)
+ commandService.registerDefaultPresenceProvider(new DefaultPresenceProvider());
+ commandService.registerDefaultPresenceProvider(new PlayerPresenceProvider());
+ }
+}
diff --git a/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/HytaleMixedCommandExecutor.java b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/HytaleMixedCommandExecutor.java
new file mode 100644
index 00000000..01bd5320
--- /dev/null
+++ b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/HytaleMixedCommandExecutor.java
@@ -0,0 +1,172 @@
+/*
+ * 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.command;
+
+import com.hypixel.hytale.component.Ref;
+import com.hypixel.hytale.component.Store;
+import com.hypixel.hytale.server.core.Message;
+import com.hypixel.hytale.server.core.command.system.CommandContext;
+import com.hypixel.hytale.server.core.command.system.basecommands.AbstractAsyncCommand;
+import com.hypixel.hytale.server.core.universe.PlayerRef;
+import com.hypixel.hytale.server.core.universe.world.World;
+import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
+import io.fairyproject.command.BaseCommand;
+import io.fairyproject.hytale.command.event.HytaleCommandContext;
+import io.fairyproject.hytale.command.event.HytalePlayerCommandContext;
+import lombok.Getter;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Hytale mixed command executor that supports both player and console senders.
+ *
+ *
This executor extends {@link AbstractAsyncCommand} which accepts any sender type, allowing
+ * both players and console to execute commands. When the sender is a player, it schedules
+ * execution on the correct world thread and creates a {@link HytalePlayerCommandContext} with
+ * full player data. When the sender is not a player (e.g., console), it creates a
+ * {@link HytaleCommandContext} and executes immediately.
+ *
+ * This allows commands to have some sub-commands that require player context and others
+ * that work with any sender. The Fairy command framework handles context type validation
+ * at the method level:
+ *
+ * - Methods using {@link HytaleCommandContext} can be executed by any sender (player or console)
+ * - Methods using {@link HytalePlayerCommandContext} can only be executed by players
+ *
+ *
+ * @see HytaleCommandExecutor for commands where all methods use HytaleCommandContext
+ * @see HytalePlayerCommandExecutor for commands where all methods use HytalePlayerCommandContext
+ */
+@Getter
+public class HytaleMixedCommandExecutor extends AbstractAsyncCommand {
+
+ private static final Message MESSAGE_PLAYER_NOT_IN_WORLD = Message.raw("You must be in a world to execute this command.").color("#FF5555");
+
+ private final BaseCommand fairyCommand;
+
+ public HytaleMixedCommandExecutor(BaseCommand fairyCommand) {
+ super(fairyCommand.getCommandNames()[0], fairyCommand.getDescription());
+ this.fairyCommand = fairyCommand;
+
+ // Add aliases
+ String[] commandNames = fairyCommand.getCommandNames();
+ if (commandNames.length > 1) {
+ String[] aliases = new String[commandNames.length - 1];
+ System.arraycopy(commandNames, 1, aliases, 0, aliases.length);
+ this.addAliases(aliases);
+ }
+
+ // Allow extra arguments since Fairy handles its own argument parsing
+ this.setAllowsExtraArguments(true);
+ }
+
+ @Override
+ protected CompletableFuture executeAsync(CommandContext hytaleContext) {
+ // Check if sender is a player
+ if (hytaleContext.isPlayer()) {
+ // Get player reference from context
+ Ref ref = hytaleContext.senderAsPlayerRef();
+
+ if (ref == null || !ref.isValid()) {
+ hytaleContext.sendMessage(MESSAGE_PLAYER_NOT_IN_WORLD);
+ return CompletableFuture.completedFuture(null);
+ }
+
+ // Get Store and World (these don't require thread safety)
+ Store store = ref.getStore();
+ EntityStore entityStore = (EntityStore) store.getExternalData();
+ World world = entityStore.getWorld();
+
+ // Schedule execution on the correct world thread
+ return runAsync(hytaleContext, () -> executeForPlayer(hytaleContext, store, ref, world), world);
+ } else {
+ // Non-player sender (console, etc.) - execute immediately
+ executeForConsole(hytaleContext);
+ return CompletableFuture.completedFuture(null);
+ }
+ }
+
+ /**
+ * Execute command for player on the correct world thread.
+ */
+ private void executeForPlayer(CommandContext hytaleContext, Store store, Ref ref, World world) {
+ String[] args = extractArgs(hytaleContext.getInputString());
+
+ // Now we're on the correct thread, safe to call store.getComponent()
+ PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
+
+ HytalePlayerCommandContext fairyContext = new HytalePlayerCommandContext(
+ hytaleContext.sender(),
+ args,
+ store,
+ ref,
+ playerRef,
+ world
+ );
+
+ try {
+ this.fairyCommand.execute(fairyContext);
+ } catch (Throwable throwable) {
+ this.fairyCommand.onError(fairyContext, throwable);
+ hytaleContext.sendMessage(Message.raw("An error occurred while executing the command.").color("#FF5555"));
+ }
+ }
+
+ /**
+ * Execute command for console/non-player sender.
+ */
+ private void executeForConsole(CommandContext hytaleContext) {
+ String[] args = extractArgs(hytaleContext.getInputString());
+
+ HytaleCommandContext fairyContext = new HytaleCommandContext(hytaleContext.sender(), args);
+
+ try {
+ this.fairyCommand.execute(fairyContext);
+ } catch (Throwable throwable) {
+ this.fairyCommand.onError(fairyContext, throwable);
+ hytaleContext.sendMessage(Message.raw("An error occurred while executing the command.").color("#FF5555"));
+ }
+ }
+
+ /**
+ * Extract arguments from the input string.
+ * The input string contains the full command, so we need to skip the command name.
+ */
+ private String[] extractArgs(String inputString) {
+ if (inputString == null || inputString.isEmpty()) {
+ return new String[0];
+ }
+
+ String[] parts = inputString.split(" ");
+ if (parts.length <= 1) {
+ return new String[0];
+ }
+
+ // Skip the command name and return the rest as arguments
+ String[] args = new String[parts.length - 1];
+ System.arraycopy(parts, 1, args, 0, args.length);
+ return args;
+ }
+}
diff --git a/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/HytalePlayerCommandExecutor.java b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/HytalePlayerCommandExecutor.java
new file mode 100644
index 00000000..d125d8d4
--- /dev/null
+++ b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/HytalePlayerCommandExecutor.java
@@ -0,0 +1,125 @@
+/*
+ * 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.command;
+
+import com.hypixel.hytale.component.Ref;
+import com.hypixel.hytale.component.Store;
+import com.hypixel.hytale.server.core.Message;
+import com.hypixel.hytale.server.core.command.system.CommandContext;
+import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
+import com.hypixel.hytale.server.core.universe.PlayerRef;
+import com.hypixel.hytale.server.core.universe.world.World;
+import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
+import io.fairyproject.command.BaseCommand;
+import io.fairyproject.hytale.command.event.HytalePlayerCommandContext;
+import lombok.Getter;
+
+/**
+ * Hytale player command executor that bridges Fairy's BaseCommand to Hytale's command system.
+ *
+ * This executor extends {@link AbstractPlayerCommand} which handles multi-threading properly.
+ * The execute method is called on the correct thread for the player's world, ensuring thread safety
+ * when accessing player and world data.
+ *
+ * The command execution flow:
+ *
+ * - Player executes command (may be on any thread)
+ * - AbstractPlayerCommand.executeAsync is called
+ * - Hytale schedules execution on the correct world thread
+ * - execute() is called with player context on the correct thread
+ * - Fairy's BaseCommand.execute() is invoked with HytalePlayerCommandContext
+ *
+ */
+@Getter
+public class HytalePlayerCommandExecutor extends AbstractPlayerCommand {
+
+ private final BaseCommand fairyCommand;
+
+ public HytalePlayerCommandExecutor(BaseCommand fairyCommand) {
+ super(fairyCommand.getCommandNames()[0], fairyCommand.getDescription());
+ this.fairyCommand = fairyCommand;
+
+ // Add aliases
+ String[] commandNames = fairyCommand.getCommandNames();
+ if (commandNames.length > 1) {
+ String[] aliases = new String[commandNames.length - 1];
+ System.arraycopy(commandNames, 1, aliases, 0, aliases.length);
+ this.addAliases(aliases);
+ }
+
+ // Allow extra arguments since Fairy handles its own argument parsing
+ this.setAllowsExtraArguments(true);
+ }
+
+ @Override
+ protected void execute(
+ CommandContext hytaleContext,
+ Store store,
+ Ref ref,
+ PlayerRef playerRef,
+ World world
+ ) {
+ // Extract the remaining arguments from the input
+ String inputString = hytaleContext.getInputString();
+ String[] args = extractArgs(inputString);
+
+ // Create player-specific context with all the player/world information
+ HytalePlayerCommandContext fairyContext = new HytalePlayerCommandContext(
+ hytaleContext.sender(),
+ args,
+ store,
+ ref,
+ playerRef,
+ world
+ );
+
+ try {
+ this.fairyCommand.execute(fairyContext);
+ } catch (Throwable throwable) {
+ this.fairyCommand.onError(fairyContext, throwable);
+ hytaleContext.sendMessage(Message.raw("An error occurred while executing the command.").color("#FF5555"));
+ }
+ }
+
+ /**
+ * Extract arguments from the input string.
+ * The input string contains the full command, so we need to skip the command name.
+ */
+ private String[] extractArgs(String inputString) {
+ if (inputString == null || inputString.isEmpty()) {
+ return new String[0];
+ }
+
+ String[] parts = inputString.split(" ");
+ if (parts.length <= 1) {
+ return new String[0];
+ }
+
+ // Skip the command name and return the rest as arguments
+ String[] args = new String[parts.length - 1];
+ System.arraycopy(parts, 1, args, 0, args.length);
+ return args;
+ }
+}
diff --git a/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/event/HytaleCommandContext.java b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/event/HytaleCommandContext.java
new file mode 100644
index 00000000..62e56657
--- /dev/null
+++ b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/event/HytaleCommandContext.java
@@ -0,0 +1,55 @@
+/*
+ * 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.command.event;
+
+import com.hypixel.hytale.server.core.command.system.CommandSender;
+import io.fairyproject.command.CommandContext;
+
+/**
+ * Hytale command context implementation.
+ */
+public class HytaleCommandContext extends CommandContext {
+
+ private final CommandSender sender;
+
+ public HytaleCommandContext(CommandSender sender, String[] args) {
+ super(args);
+ this.sender = sender;
+ }
+
+ @Override
+ public String name() {
+ return sender.getDisplayName();
+ }
+
+ public CommandSender getSender() {
+ return this.sender;
+ }
+
+ @Override
+ public boolean hasPermission(String permission) {
+ return this.sender.hasPermission(permission);
+ }
+}
diff --git a/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/event/HytalePlayerCommandContext.java b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/event/HytalePlayerCommandContext.java
new file mode 100644
index 00000000..fa28de1d
--- /dev/null
+++ b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/event/HytalePlayerCommandContext.java
@@ -0,0 +1,148 @@
+/*
+ * 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.command.event;
+
+import com.hypixel.hytale.component.Ref;
+import com.hypixel.hytale.component.Store;
+import com.hypixel.hytale.server.core.command.system.CommandSender;
+import com.hypixel.hytale.server.core.entity.entities.Player;
+import com.hypixel.hytale.server.core.universe.PlayerRef;
+import com.hypixel.hytale.server.core.universe.world.World;
+import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
+
+import java.util.UUID;
+
+/**
+ * Hytale player command context implementation.
+ * This context is used for commands executed by players and provides access to
+ * player-specific data like the player reference, world, and entity store.
+ *
+ * Note: Hytale is multi-threaded, so command execution may occur on different threads.
+ * This context captures the player's state at the time of command execution.
+ */
+public class HytalePlayerCommandContext extends HytaleCommandContext {
+
+ private final Store store;
+ private final Ref ref;
+ private final PlayerRef playerRef;
+ private final World world;
+
+ public HytalePlayerCommandContext(
+ CommandSender sender,
+ String[] args,
+ Store store,
+ Ref ref,
+ PlayerRef playerRef,
+ World world
+ ) {
+ super(sender, args);
+ this.store = store;
+ this.ref = ref;
+ this.playerRef = playerRef;
+ this.world = world;
+ }
+
+ @Override
+ public String name() {
+ return playerRef.getUsername();
+ }
+
+ /**
+ * Get the entity store for the player.
+ *
+ * @return the entity store
+ */
+ public Store getStore() {
+ return store;
+ }
+
+ /**
+ * Get the entity reference for the player.
+ *
+ * @return the entity reference
+ */
+ public Ref getRef() {
+ return ref;
+ }
+
+ /**
+ * Get the player reference.
+ *
+ * @return the player reference
+ */
+ public PlayerRef getPlayerRef() {
+ return playerRef;
+ }
+
+ public Player getPlayerComponent() {
+ return store.getComponent(ref, Player.getComponentType());
+ }
+
+ /**
+ * Get the world the player is in.
+ *
+ * @return the world
+ */
+ public World getWorld() {
+ return world;
+ }
+
+ /**
+ * Get the player's UUID.
+ *
+ * @return the player's UUID
+ */
+ public UUID getPlayerUuid() {
+ return playerRef.getUuid();
+ }
+
+ /**
+ * Get the player's username.
+ *
+ * @return the player's username
+ */
+ public String getPlayerName() {
+ return playerRef.getUsername();
+ }
+
+ /**
+ * Check if the player reference is still valid.
+ * In a multi-threaded environment, the player may have disconnected.
+ *
+ * @return true if the player reference is still valid
+ */
+ public boolean isPlayerValid() {
+ return playerRef.isValid() && ref.isValid();
+ }
+
+ /**
+ * Check if we are currently on the correct thread for this store.
+ *
+ * @return true if on the correct thread
+ */
+ public boolean isInStoreThread() {
+ return store.isInThread();
+ }
+}
diff --git a/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/map/DefaultHytaleCommandMap.java b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/map/DefaultHytaleCommandMap.java
new file mode 100644
index 00000000..ee536f26
--- /dev/null
+++ b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/map/DefaultHytaleCommandMap.java
@@ -0,0 +1,170 @@
+/*
+ * 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.command.map;
+
+import com.hypixel.hytale.server.core.command.system.AbstractCommand;
+import com.hypixel.hytale.server.core.command.system.CommandRegistration;
+import com.hypixel.hytale.server.core.command.system.CommandRegistry;
+import io.fairyproject.command.BaseCommand;
+import io.fairyproject.command.annotation.Command;
+import io.fairyproject.container.InjectableComponent;
+import io.fairyproject.data.MetaKey;
+import io.fairyproject.data.MetaStorage;
+import io.fairyproject.hytale.FairyHytalePlatform;
+import io.fairyproject.hytale.command.HytaleCommandExecutor;
+import io.fairyproject.hytale.command.HytaleMixedCommandExecutor;
+import io.fairyproject.hytale.command.HytalePlayerCommandExecutor;
+import io.fairyproject.hytale.command.event.HytaleCommandContext;
+import io.fairyproject.hytale.command.event.HytalePlayerCommandContext;
+
+import java.lang.reflect.Method;
+
+/**
+ * Default implementation of HytaleCommandMap that registers commands with Hytale's CommandRegistry.
+ *
+ * This implementation supports three types of command executors:
+ *
+ * - {@link HytaleCommandExecutor} - For commands where all methods use HytaleCommandContext (any sender)
+ * - {@link HytalePlayerCommandExecutor} - For commands where all methods use HytalePlayerCommandContext (player-only, thread-safe)
+ * - {@link HytaleMixedCommandExecutor} - For commands with mixed context types (supports both player and console)
+ *
+ *
+ * The executor type is automatically detected by analyzing the command method parameters:
+ *
+ * - If all methods use HytalePlayerCommandContext → HytalePlayerCommandExecutor
+ * - If all methods use HytaleCommandContext → HytaleCommandExecutor
+ * - If methods use both types → HytaleMixedCommandExecutor
+ *
+ */
+@InjectableComponent
+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);
+
+ @Override
+ public void register(BaseCommand command) {
+ if (this.isRegistered(command)) {
+ throw new IllegalArgumentException("Command already registered: " + command.getCommandNames()[0]);
+ }
+
+ // Analyze command methods to determine executor type
+ ContextTypeInfo contextInfo = analyzeContextTypes(command.getClass());
+
+ AbstractCommand commandExecutor;
+ if (contextInfo.hasOnlyPlayerContext()) {
+ // All methods require player context - use player-only executor (thread-safe)
+ commandExecutor = new HytalePlayerCommandExecutor(command);
+ } else if (contextInfo.hasOnlyGeneralContext()) {
+ // All methods use general context - use general executor (any sender)
+ commandExecutor = new HytaleCommandExecutor(command);
+ } else {
+ // Mixed context types - use mixed executor (supports both player and console)
+ commandExecutor = new HytaleMixedCommandExecutor(command);
+ }
+
+ CommandRegistry commandRegistry = getCommandRegistry();
+ CommandRegistration registration = commandRegistry.registerCommand(commandExecutor);
+
+ command.getMetaStorage().put(EXECUTOR_KEY, commandExecutor);
+ command.getMetaStorage().put(REGISTRATION_KEY, registration);
+ }
+
+ /**
+ * Analyzes the command class to determine what context types are used by @Command methods.
+ */
+ private ContextTypeInfo analyzeContextTypes(Class> clazz) {
+ boolean hasPlayerContext = false;
+ boolean hasGeneralContext = false;
+
+ for (Method method : clazz.getDeclaredMethods()) {
+ if (!method.isAnnotationPresent(Command.class)) {
+ continue;
+ }
+
+ Class>[] paramTypes = method.getParameterTypes();
+ if (paramTypes.length == 0) {
+ continue;
+ }
+
+ Class> firstParam = paramTypes[0];
+ if (HytalePlayerCommandContext.class.isAssignableFrom(firstParam)) {
+ hasPlayerContext = true;
+ } else if (HytaleCommandContext.class.isAssignableFrom(firstParam)) {
+ hasGeneralContext = true;
+ }
+ }
+
+ // Also check superclass methods
+ Class> superclass = clazz.getSuperclass();
+ if (superclass != null && superclass != Object.class) {
+ ContextTypeInfo superInfo = analyzeContextTypes(superclass);
+ hasPlayerContext = hasPlayerContext || superInfo.hasPlayerContext;
+ hasGeneralContext = hasGeneralContext || superInfo.hasGeneralContext;
+ }
+
+ return new ContextTypeInfo(hasPlayerContext, hasGeneralContext);
+ }
+
+ private static class ContextTypeInfo {
+ final boolean hasPlayerContext;
+ final boolean hasGeneralContext;
+
+ ContextTypeInfo(boolean hasPlayerContext, boolean hasGeneralContext) {
+ this.hasPlayerContext = hasPlayerContext;
+ this.hasGeneralContext = hasGeneralContext;
+ }
+
+ boolean hasOnlyPlayerContext() {
+ return hasPlayerContext && !hasGeneralContext;
+ }
+
+ boolean hasOnlyGeneralContext() {
+ return hasGeneralContext && !hasPlayerContext;
+ }
+ }
+
+ @Override
+ public void unregister(BaseCommand command) {
+ MetaStorage metaStorage = command.getMetaStorage();
+ if (!this.isRegistered(command)) {
+ throw new IllegalArgumentException("Command not registered: " + command.getCommandNames()[0]);
+ }
+
+ // Note: Hytale's CommandRegistry may not support unregistration
+ // Remove from meta storage to mark as unregistered
+ metaStorage.remove(EXECUTOR_KEY);
+ metaStorage.remove(REGISTRATION_KEY);
+ }
+
+ @Override
+ public boolean isRegistered(BaseCommand command) {
+ return command.getMetaStorage().contains(EXECUTOR_KEY);
+ }
+
+ private CommandRegistry getCommandRegistry() {
+ return FairyHytalePlatform.PLUGIN.getCommandRegistry();
+ }
+}
diff --git a/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/map/HytaleCommandMap.java b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/map/HytaleCommandMap.java
new file mode 100644
index 00000000..5e54ebb9
--- /dev/null
+++ b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/map/HytaleCommandMap.java
@@ -0,0 +1,36 @@
+/*
+ * 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.command.map;
+
+import io.fairyproject.command.BaseCommand;
+
+public interface HytaleCommandMap {
+
+ void register(BaseCommand command);
+
+ void unregister(BaseCommand command);
+
+ boolean isRegistered(BaseCommand command);
+}
diff --git a/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/presence/DefaultPresenceProvider.java b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/presence/DefaultPresenceProvider.java
new file mode 100644
index 00000000..79d9f683
--- /dev/null
+++ b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/presence/DefaultPresenceProvider.java
@@ -0,0 +1,66 @@
+/*
+ * 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.command.presence;
+
+import com.hypixel.hytale.server.core.Message;
+import io.fairyproject.command.MessageType;
+import io.fairyproject.command.PresenceProvider;
+import io.fairyproject.hytale.command.event.HytaleCommandContext;
+
+/**
+ * Default presence provider for Hytale commands using Hytale's Message API.
+ */
+public class DefaultPresenceProvider implements PresenceProvider {
+
+ private static final String COLOR_INFO = "#55FFFF"; // Aqua
+ private static final String COLOR_WARN = "#FFAA00"; // Gold
+ private static final String COLOR_ERROR = "#FF5555"; // Red
+
+ @Override
+ public Class type() {
+ return HytaleCommandContext.class;
+ }
+
+ @Override
+ public void sendMessage(HytaleCommandContext commandContext, MessageType messageType, String... messages) {
+ String color;
+ switch (messageType) {
+ case WARN:
+ color = COLOR_WARN;
+ break;
+ case ERROR:
+ color = COLOR_ERROR;
+ break;
+ default:
+ color = COLOR_INFO;
+ break;
+ }
+
+ for (String message : messages) {
+ Message hytaleMessage = Message.raw(message).color(color);
+ commandContext.getSender().sendMessage(hytaleMessage);
+ }
+ }
+}
diff --git a/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/presence/PlayerPresenceProvider.java b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/presence/PlayerPresenceProvider.java
new file mode 100644
index 00000000..a638ead4
--- /dev/null
+++ b/framework/modules/hytale/hytale-command/src/main/java/io/fairyproject/hytale/command/presence/PlayerPresenceProvider.java
@@ -0,0 +1,76 @@
+/*
+ * 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.command.presence;
+
+import com.hypixel.hytale.server.core.Message;
+import io.fairyproject.command.MessageType;
+import io.fairyproject.command.PresenceProvider;
+import io.fairyproject.hytale.command.event.HytaleCommandContext;
+import io.fairyproject.hytale.command.event.HytalePlayerCommandContext;
+
+/**
+ * Presence provider for Hytale player commands.
+ *
+ * This provider is registered for {@link HytalePlayerCommandContext} type but accepts
+ * the base {@link HytaleCommandContext} in sendMessage(). This is necessary because when
+ * console tries to execute a player-only command, the framework sends an error message
+ * using this provider, but the actual context is HytaleCommandContext (not player context).
+ */
+public class PlayerPresenceProvider implements PresenceProvider {
+
+ private static final String COLOR_INFO = "#55FFFF"; // Aqua
+ private static final String COLOR_WARN = "#FFAA00"; // Gold
+ private static final String COLOR_ERROR = "#FF5555"; // Red
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Class type() {
+ // Return HytalePlayerCommandContext.class for registration purposes
+ // This ensures the framework registers this provider for player context methods
+ // The unchecked cast is safe because we accept HytaleCommandContext in sendMessage()
+ return (Class) (Class>) HytalePlayerCommandContext.class;
+ }
+
+ @Override
+ public void sendMessage(HytaleCommandContext commandContext, MessageType messageType, String... messages) {
+ String color;
+ switch (messageType) {
+ case WARN:
+ color = COLOR_WARN;
+ break;
+ case ERROR:
+ color = COLOR_ERROR;
+ break;
+ default:
+ color = COLOR_INFO;
+ break;
+ }
+
+ for (String message : messages) {
+ Message hytaleMessage = Message.raw(message).color(color);
+ commandContext.getSender().sendMessage(hytaleMessage);
+ }
+ }
+}
diff --git a/framework/modules/hytale/settings.gradle.kts b/framework/modules/hytale/settings.gradle.kts
new file mode 100644
index 00000000..9d9ae216
--- /dev/null
+++ b/framework/modules/hytale/settings.gradle.kts
@@ -0,0 +1,13 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ }
+}
+
+includeBuild("../../../build-logic")
+includeBuild("../../tests")
+includeBuild("../../platforms")
+includeBuild("..")
+
+include(":hytale-bom")
+include(":hytale-command")
diff --git a/framework/modules/modules-bom/build.gradle.kts b/framework/modules/modules-bom/build.gradle.kts
index 3ab26436..2eb74cda 100644
--- a/framework/modules/modules-bom/build.gradle.kts
+++ b/framework/modules/modules-bom/build.gradle.kts
@@ -7,5 +7,6 @@ description = "Fairy DevTools BOM (Bill of materials)"
dependencies {
api(platform("io.fairyproject:core-bom"))
api(platform("io.fairyproject:bukkit-bom"))
+ api(platform("io.fairyproject:hytale-bom"))
api(platform("io.fairyproject:mc-bom"))
}
\ No newline at end of file
diff --git a/framework/modules/settings.gradle.kts b/framework/modules/settings.gradle.kts
index 8d5b210c..a1efc69a 100644
--- a/framework/modules/settings.gradle.kts
+++ b/framework/modules/settings.gradle.kts
@@ -7,5 +7,6 @@ pluginManagement {
includeBuild("core")
includeBuild("bukkit")
includeBuild("mc")
+includeBuild("hytale")
include(":modules-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/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/framework/platforms/core-platform/src/main/java/io/fairyproject/library/relocate/RelocationHandlerImpl.java b/framework/platforms/core-platform/src/main/java/io/fairyproject/library/relocate/RelocationHandlerImpl.java
index 78d683c3..3cc988f0 100644
--- a/framework/platforms/core-platform/src/main/java/io/fairyproject/library/relocate/RelocationHandlerImpl.java
+++ b/framework/platforms/core-platform/src/main/java/io/fairyproject/library/relocate/RelocationHandlerImpl.java
@@ -44,13 +44,13 @@ public class RelocationHandlerImpl implements RelocationHandler {
Library.builder()
.groupId("org.ow2.asm")
.artifactId("asm")
- .version("9.7")
+ .version("9.9.1")
.build(),
// asm-commons
Library.builder()
.groupId("org.ow2.asm")
.artifactId("asm-commons")
- .version("9.7")
+ .version("9.9.1")
.build(),
// jar-relocator
Library.builder()
diff --git a/framework/platforms/hytale-platform/build.gradle.kts b/framework/platforms/hytale-platform/build.gradle.kts
new file mode 100644
index 00000000..226f0189
--- /dev/null
+++ b/framework/platforms/hytale-platform/build.gradle.kts
@@ -0,0 +1,19 @@
+plugins {
+ id("io.fairyproject.platform")
+}
+
+dependencies {
+ api(project(":core-platform"))
+ compileOnlyApi("dev.imanity.hytale:HytaleServer:2026.01.17-2")
+}
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(25))
+ }
+}
+
+tasks.withType(JavaCompile::class.java).configureEach {
+ options.encoding = "UTF-8"
+ options.release = 25
+}
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..b0e8c238
--- /dev/null
+++ b/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/FairyHytalePlatform.java
@@ -0,0 +1,147 @@
+/*
+ * 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.component.system.ISystem;
+import com.hypixel.hytale.server.core.plugin.PluginBase;
+import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
+import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
+import io.fairyproject.Debug;
+import io.fairyproject.FairyPlatform;
+import io.fairyproject.PlatformType;
+import io.fairyproject.container.PreInitialize;
+import io.fairyproject.container.collection.ContainerObjCollector;
+import io.fairyproject.hytale.entity.RegisterAsChunkSystem;
+import io.fairyproject.hytale.entity.RegisterAsEntitySystem;
+import io.fairyproject.hytale.logger.HytaleILogger;
+import io.fairyproject.hytale.plugin.HytalePluginHandler;
+import io.fairyproject.log.Log;
+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 final URLClassLoaderAccess classLoader;
+ private final File dataFolder;
+ private final CompositeTerminable compositeTerminable;
+ private final Runnable shutdownCallback;
+
+ public FairyHytalePlatform(PluginBase plugin, Runnable shutdownCallback, File dataFolder) {
+ FairyPlatform.INSTANCE = this;
+ this.shutdownCallback = shutdownCallback;
+ setPlugin(plugin);
+
+ 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());
+ if (!Debug.UNIT_TEST) {
+ Log.set(new HytaleILogger());
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ @PreInitialize
+ public void onPreInitialize() {
+ this.getContainerContext().objectCollectorRegistry().add(ContainerObjCollector.create()
+ .withFilter(ContainerObjCollector.inherits(ISystem.class))
+ .withAddHandler(ContainerObjCollector.warpInstance(ISystem.class, system -> {
+ if (system.getClass().isAnnotationPresent(RegisterAsEntitySystem.class)) {
+ EntityStore.REGISTRY.registerSystem((ISystem) system);
+ } else if (system.getClass().isAnnotationPresent(RegisterAsChunkSystem.class)) {
+ ChunkStore.REGISTRY.registerSystem((ISystem) system);
+ }
+ }))
+ .withRemoveHandler(ContainerObjCollector.warpInstance(ISystem.class, system -> {
+ if (system.getClass().isAnnotationPresent(RegisterAsEntitySystem.class)) {
+ EntityStore.REGISTRY.unregisterSystem(
+ (Class extends ISystem>) system.getClass()
+ );
+ } else if (system.getClass().isAnnotationPresent(RegisterAsChunkSystem.class)) {
+ ChunkStore.REGISTRY.unregisterSystem(
+ (Class extends ISystem>) system.getClass()
+ );
+ }
+ })));
+ }
+
+ @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 (this.shutdownCallback != null) {
+ this.shutdownCallback.run();
+ }
+ }
+
+ private static synchronized void setPlugin(PluginBase plugin) {
+ PLUGIN = plugin;
+ }
+
+ @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/entity/RegisterAsChunkSystem.java b/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/entity/RegisterAsChunkSystem.java
new file mode 100644
index 00000000..52e6ab43
--- /dev/null
+++ b/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/entity/RegisterAsChunkSystem.java
@@ -0,0 +1,39 @@
+/*
+ * 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.entity;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a class to be automatically registered as a chunk system
+ * using {@code ChunkStore.REGISTRY.registerSystem}.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RegisterAsChunkSystem {
+}
diff --git a/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/entity/RegisterAsEntitySystem.java b/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/entity/RegisterAsEntitySystem.java
new file mode 100644
index 00000000..632b5480
--- /dev/null
+++ b/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/entity/RegisterAsEntitySystem.java
@@ -0,0 +1,39 @@
+/*
+ * 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.entity;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a class to be automatically registered as an entity system
+ * using {@code EntityStore.REGISTRY.registerSystem}.
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface RegisterAsEntitySystem {
+}
diff --git a/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/logger/HytaleILogger.java b/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/logger/HytaleILogger.java
new file mode 100644
index 00000000..20606d54
--- /dev/null
+++ b/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/logger/HytaleILogger.java
@@ -0,0 +1,99 @@
+/*
+ * 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.logger;
+
+import com.hypixel.hytale.logger.HytaleLogger;
+import io.fairyproject.log.ILogger;
+
+import java.util.logging.Level;
+
+public class HytaleILogger implements ILogger {
+
+ private final HytaleLogger logger;
+
+ public HytaleILogger() {
+ this.logger = HytaleLogger.get("Fairy");
+ }
+
+ @Override
+ public void info(String message, Object... replace) {
+ logger.at(Level.INFO).log(String.format(message, replace));
+ }
+
+ @Override
+ public void debug(String message, Object... replace) {
+ logger.at(Level.FINE).log(String.format(message, replace));
+ }
+
+ @Override
+ public void warn(String message, Object... replace) {
+ logger.at(Level.WARNING).log(String.format(message, replace));
+ }
+
+ @Override
+ public void error(String message, Object... replace) {
+ logger.at(Level.SEVERE).log(String.format(message, replace));
+ }
+
+ @Override
+ public void info(String message, Throwable throwable, Object... replace) {
+ logger.at(Level.INFO).withCause(throwable).log(String.format(message, replace));
+ }
+
+ @Override
+ public void debug(String message, Throwable throwable, Object... replace) {
+ logger.at(Level.FINE).withCause(throwable).log(String.format(message, replace));
+ }
+
+ @Override
+ public void warn(String message, Throwable throwable, Object... replace) {
+ logger.at(Level.WARNING).withCause(throwable).log(String.format(message, replace));
+ }
+
+ @Override
+ public void error(String message, Throwable throwable, Object... replace) {
+ logger.at(Level.SEVERE).withCause(throwable).log(String.format(message, replace));
+ }
+
+ @Override
+ public void info(Throwable throwable) {
+ logger.at(Level.INFO).withCause(throwable).log("");
+ }
+
+ @Override
+ public void debug(Throwable throwable) {
+ logger.at(Level.FINE).withCause(throwable).log("");
+ }
+
+ @Override
+ public void warn(Throwable throwable) {
+ logger.at(Level.WARNING).withCause(throwable).log("");
+ }
+
+ @Override
+ public void error(Throwable throwable) {
+ logger.at(Level.SEVERE).withCause(throwable).log("");
+ }
+}
diff --git a/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/network/PacketListener.java b/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/network/PacketListener.java
new file mode 100644
index 00000000..96419162
--- /dev/null
+++ b/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/network/PacketListener.java
@@ -0,0 +1,10 @@
+package io.fairyproject.hytale.network;
+
+import com.hypixel.hytale.server.core.universe.PlayerRef;
+import org.jetbrains.annotations.NotNull;
+
+public interface PacketListener {
+
+ void handle(@NotNull PlayerRef playerRef, @NotNull T packet);
+
+}
diff --git a/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/network/PacketListenerManager.java b/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/network/PacketListenerManager.java
new file mode 100644
index 00000000..f5dd8d72
--- /dev/null
+++ b/framework/platforms/hytale-platform/src/main/java/io/fairyproject/hytale/network/PacketListenerManager.java
@@ -0,0 +1,10 @@
+package io.fairyproject.hytale.network;
+
+import io.fairyproject.container.InjectableComponent;
+
+@InjectableComponent
+public class PacketListenerManager {
+
+
+
+}
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/framework/tests/hytale-tests/build.gradle.kts b/framework/tests/hytale-tests/build.gradle.kts
new file mode 100644
index 00000000..d335017f
--- /dev/null
+++ b/framework/tests/hytale-tests/build.gradle.kts
@@ -0,0 +1,20 @@
+plugins {
+ id("io.fairyproject.versioned")
+ id("io.fairyproject.publish")
+}
+
+dependencies {
+ compileOnly("io.fairyproject:hytale-platform")
+ api(project(":core-tests"))
+}
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(25))
+ }
+}
+
+tasks.withType(JavaCompile::class.java).configureEach {
+ options.encoding = "UTF-8"
+ options.release = 25
+}
diff --git a/framework/tests/hytale-tests/src/main/java/io/fairyproject/tests/hytale/HytaleTestingHandle.java b/framework/tests/hytale-tests/src/main/java/io/fairyproject/tests/hytale/HytaleTestingHandle.java
new file mode 100644
index 00000000..37837d0d
--- /dev/null
+++ b/framework/tests/hytale-tests/src/main/java/io/fairyproject/tests/hytale/HytaleTestingHandle.java
@@ -0,0 +1,39 @@
+/*
+ * 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.tests.hytale;
+
+import io.fairyproject.tests.TestingHandle;
+
+/**
+ * This is the Hytale implementation for testing handle.
+ * You can use this to create custom server mock.
+ */
+public interface HytaleTestingHandle extends TestingHandle {
+
+ @Override
+ default void onPreInitialization() {
+ // Hytale testing initialization
+ }
+}
diff --git a/framework/tests/settings.gradle.kts b/framework/tests/settings.gradle.kts
index 3cc4b21c..1d9d7938 100644
--- a/framework/tests/settings.gradle.kts
+++ b/framework/tests/settings.gradle.kts
@@ -11,4 +11,5 @@ include(":core-tests")
include(":app-tests")
include(":mc-tests")
include(":bukkit-tests")
+include(":hytale-tests")
include(":tests-bom")
diff --git a/global.properties b/global.properties
index a88e3bff..f3e5801f 100644
--- a/global.properties
+++ b/global.properties
@@ -1 +1 @@
-version = 0.8.4b1-SNAPSHOT
\ No newline at end of file
+version = 0.8.5b1-hytale8-SNAPSHOT
\ No newline at end of file
diff --git a/gradle-plugin/build.gradle.kts b/gradle-plugin/build.gradle.kts
index 669cae57..7869702f 100644
--- a/gradle-plugin/build.gradle.kts
+++ b/gradle-plugin/build.gradle.kts
@@ -1,9 +1,10 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.util.*
plugins {
- kotlin("jvm") version "1.9.10"
- id("com.gradle.plugin-publish") version "1.0.0"
+ kotlin("jvm") version "2.1.21"
+ id("com.gradle.plugin-publish") version "1.3.1"
id("io.fairyproject.common")
id("io.fairyproject.publish")
`java-gradle-plugin`
@@ -38,13 +39,13 @@ repositories {
}
dependencies {
- implementation("io.spring.gradle:dependency-management-plugin:1.1.0")
+ implementation("io.spring.gradle:dependency-management-plugin:1.1.7")
implementation(kotlin("stdlib-jdk8"))
implementation("org.json:json:20231013")
implementation("org.apache.maven:maven-plugin-api:3.8.5")
- implementation("org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.7.22")
- implementation("org.ow2.asm:asm:9.7")
- implementation("org.ow2.asm:asm-commons:9.7")
+ implementation("org.jetbrains.kotlin:kotlin-gradle-plugin-api:2.1.21")
+ implementation("org.ow2.asm:asm:9.9.1")
+ implementation("org.ow2.asm:asm-commons:9.9.1")
implementation("com.google.code.gson:gson:2.10")
implementation("io.github.toolfactory:narcissus:1.0.7")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0")
@@ -61,12 +62,14 @@ gradlePlugin {
}
}
-tasks.withType {
+tasks.withType().configureEach {
manifest {
attributes["Implementation-Version"] = project.version
}
}
-tasks.withType {
- kotlinOptions.jvmTarget = "1.8"
+tasks.withType().configureEach {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ }
}
\ No newline at end of file
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..1579ef5c 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") }
@@ -42,7 +44,7 @@ class FairyGradlePlugin : Plugin {
}
private fun configurePlugin(project: Project, language: String) {
- sourceSets.all { sourceSet ->
+ sourceSets.configureEach { sourceSet ->
project.tasks.named(sourceSet.getCompileTaskName(language)) {
val action = project.objects.newInstance(FairyCompilerAction::class.java)
it.doLast("fairyCompile", action)
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..65a68735 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))
@@ -143,8 +150,9 @@ abstract class FairyResourceAction @Inject constructor() : Action {
}
private fun pushClassToMapping(classInfo: ClassInfo, classMapper: MutableMap) {
- ClassType.values().forEach { classType ->
- if (!classInfo.name.contains("module-info") && classType.names.contains(classInfo.name.substringAfterLast("/"))) {
+ ClassType.entries.forEach { classType ->
+ val className = classInfo.name.substringAfterLast("/")
+ if (!classInfo.name.contains("module-info") && classType.names.contains(className)) {
// Duplicated class types
if (classMapper.contains(classType))
throw IllegalStateException("a project are not suppose to have 2 or more classes that are $classType")
@@ -156,6 +164,8 @@ 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
+ if (jarEntry.name.equals("fairy.json")) return true
return false
}
@@ -176,7 +186,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..aadb6c92
--- /dev/null
+++ b/gradle-plugin/src/main/kotlin/io/fairyproject/gradle/resource/impl/FairyResourceHytaleMeta.kt
@@ -0,0 +1,106 @@
+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"] = convertToValidSemver(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