agentsclimarketplace

Hytale plugin project setup

Skill darthmolen/hytale-claude-code-marketplace/skills/hytale-plugin-project-setup

Use when creating a new Hytale server plugin project from scratch. Covers Java/Gradle setup, plugin manifest, main class boilerplate, and project structure.From its SKILL.md

Install
npx -y skills add darthmolen/hytale-claude-code-marketplace --skill hytale-plugin-project-setup

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

16.8 KB, as published. Nobody here has run it

Hytale Plugin Project Setup

Use this skill when creating a new Hytale server plugin project from scratch.

Prerequisites

  • Java 25 - Required by Hytale server
    • Verify: java -version should show 25+
  • Gradle - Build tool (wrapper included in project)
  • Hytale installed - Via the official launcher (build.gradle auto-detects location)

Quick Start

Create a new plugin project with this structure:

my-plugin/
├── src/main/
│   ├── java/org/example/plugin/
│   │   ├── MyPlugin.java
│   │   └── MyCommand.java
│   └── resources/
│       └── manifest.json
├── build.gradle
├── settings.gradle
├── gradle.properties
└── gradle/wrapper/

Step 1: settings.gradle

rootProject.name = 'MyPlugin'

Step 2: gradle.properties

# Plugin version (semantic versioning)
version=1.0.0

# Maven group for publishing (usually your package name, not plugin Group)
maven_group=org.example

# Java version - Hytale uses Java 25
java_version=25

# Include assets as a pack (set true if you have models/textures/items)
includes_pack=false

# Hytale release channel (release or pre-release)
patchline=release

# Load mods from user's standard mods folder during development
load_user_mods=false

# Custom Hytale install location (uncomment if needed)
# hytale_home=/path/to/Hytale

Step 3: build.gradle

plugins {
    id 'java'
}

import org.gradle.internal.os.OperatingSystem

// Auto-detect Hytale installation
ext {
    if (project.hasProperty('hytale_home')) {
        hytaleHome = project.findProperty('hytale_home')
    } else {
        def os = OperatingSystem.current()
        if (os.isWindows()) {
            hytaleHome = "${System.getProperty("user.home")}/AppData/Roaming/Hytale"
        } else if (os.isMacOsX()) {
            hytaleHome = "${System.getProperty("user.home")}/Library/Application Support/Hytale"
        } else if (os.isLinux()) {
            hytaleHome = "${System.getProperty("user.home")}/.var/app/com.hypixel.HytaleLauncher/data/Hytale"
            if (!file(hytaleHome).exists()) {
                hytaleHome = "${System.getProperty("user.home")}/.local/share/Hytale"
            }
        }
    }
}

if (!project.hasProperty('hytaleHome') || !file(hytaleHome).exists()) {
    throw new GradleException("Hytale install not found. Set hytale_home in gradle.properties.")
}

java {
    toolchain.languageVersion = JavaLanguageVersion.of(java_version)
}

// Reference HytaleServer.jar from game installation
dependencies {
    implementation(files("$hytaleHome/install/$patchline/package/game/latest/Server/HytaleServer.jar"))
}

// Create run directory for local server testing
def serverRunDir = file("$projectDir/run")
if (!serverRunDir.exists()) {
    serverRunDir.mkdirs()
}

// Auto-update manifest.json version from gradle.properties
tasks.register('updatePluginManifest') {
    def manifestFile = file('src/main/resources/manifest.json')
    doLast {
        if (!manifestFile.exists()) {
            throw new GradleException("manifest.json not found!")
        }
        def manifestJson = new groovy.json.JsonSlurper().parseText(manifestFile.text)
        manifestJson.Version = version
        manifestJson.IncludesAssetPack = includes_pack.toBoolean()
        manifestFile.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(manifestJson))
    }
}

tasks.named('processResources') {
    dependsOn 'updatePluginManifest'
}

Step 4: manifest.json

Create src/main/resources/manifest.json:

{
    "Group": "Example",
    "Name": "MyPlugin",
    "Version": "1.0.0",
    "Description": "What this plugin does",
    "Authors": [
        { "Name": "Your Name" }
    ],
    "Website": "example.org",
    "ServerVersion": "*",
    "Dependencies": {},
    "OptionalDependencies": {},
    "DisabledByDefault": false,
    "Main": "org.example.plugin.MyPlugin",
    "IncludesAssetPack": false
}

Critical Notes:

  • Authors must be array of objects: [{"Name": "..."}] NOT ["..."]
  • Dependencies must be object: {} NOT []
  • Main is the fully qualified class name of your plugin class
  • Version and IncludesAssetPack are auto-updated from gradle.properties

Step 5: Main Plugin Class

Create src/main/java/org/example/plugin/MyPlugin.java:

package org.example.plugin;

import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;

import javax.annotation.Nonnull;

public class MyPlugin extends JavaPlugin {

    private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();

    public MyPlugin(@Nonnull JavaPluginInit init) {
        super(init);
        LOGGER.atInfo().log("Hello from " + this.getName() + " v" + this.getManifest().getVersion());
    }

    @Override
    protected void setup() {
        LOGGER.atInfo().log("Setting up " + this.getName());

        // Register commands
        this.getCommandRegistry().registerCommand(new MyCommand());
    }
}

Step 6: Build and Test

# Build the JAR
./gradlew build

# JAR will be in build/libs/MyPlugin-1.0.0.jar
# Copy to your Hytale server's mods/ folder

Adding Features

Register a Command (Simple)

The simplest command uses CommandBase with synchronous execution:

package org.example.plugin;

import com.hypixel.hytale.protocol.GameMode;
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 javax.annotation.Nonnull;

public class MyCommand extends CommandBase {

    public MyCommand() {
        super("hello", "Prints a hello message");
        this.setPermissionGroup(GameMode.Adventure); // Anyone can use, not just OP
    }

    @Override
    protected void executeSync(@Nonnull CommandContext ctx) {
        ctx.sendMessage(Message.raw("Hello from the plugin!"));
    }
}

Register a Command (With Arguments)

For commands with arguments, use AbstractCommand:

package org.example.plugin.commands;

import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.server.core.command.system.AbstractCommand;
import com.hypixel.hytale.server.core.command.system.CommandContext;
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgumentType;

import javax.annotation.Nonnull;
import java.util.concurrent.CompletableFuture;

public class GreetCommand extends AbstractCommand {

    @Nonnull
    private final RequiredArg<String> nameArg = this.withRequiredArg(
        "name",
        "The name to greet",
        (ArgumentType) ArgTypes.STRING
    );

    public GreetCommand() {
        super("greet", "Greet someone by name");
    }

    @Override
    protected CompletableFuture<Void> execute(@Nonnull CommandContext context) {
        String name = this.nameArg.get(context);
        context.sendMessage(Message.raw("Hello, " + name + "!"));
        return CompletableFuture.completedFuture(null);
    }
}

Command Collections (Subcommands)

For commands with subcommands like /mymod greet <name>:

import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection;

public class MyModCommands extends AbstractCommandCollection {

    public MyModCommands() {
        super("mymod", "Commands for MyMod");
        this.addSubCommand(new GreetCommand());
    }
}
// Usage: /mymod greet Alice

Register an Event Listener

import com.hypixel.hytale.server.core.universe.world.events.AddWorldEvent;

@Override
protected void setup() {
    this.getEventRegistry().registerGlobal(AddWorldEvent.class, event -> {
        LOGGER.atInfo().log("World added: %s", event.getWorld().getName());
    });
}

Register a Component (ECS)

Components store data on blocks (ChunkStore) or entities (EntityStore).

Create components/MyBlockComponent.java:

package org.example.plugin.components;

import com.hypixel.hytale.codec.Codec;
import com.hypixel.hytale.codec.KeyedCodec;
import com.hypixel.hytale.codec.builder.BuilderCodec;
import com.hypixel.hytale.component.Component;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;

import javax.annotation.Nonnull;

public class MyBlockComponent implements Component<ChunkStore> {

    public static final BuilderCodec<MyBlockComponent> CODEC = BuilderCodec
        .builder(MyBlockComponent.class, MyBlockComponent::new)
        .append(
            new KeyedCodec<>("Value", Codec.INT),
            (comp, val) -> comp.value = val,
            comp -> comp.value
        ).add()
        .build();

    private int value;

    public MyBlockComponent() {}

    public int getValue() { return this.value; }
    public void setValue(int value) { this.value = value; }

    @Nonnull
    @Override
    public Component<ChunkStore> clone() {
        MyBlockComponent copy = new MyBlockComponent();
        copy.value = this.value;
        return copy;
    }
}

Register in plugin:

import com.hypixel.hytale.component.ComponentType;

private ComponentType<ChunkStore, MyBlockComponent> myBlockComponent;

@Override
protected void setup() {
    this.myBlockComponent = this.getChunkStoreRegistry().registerComponent(
        MyBlockComponent.class,
        "MyBlockComponent",
        MyBlockComponent.CODEC
    );
}

public ComponentType<ChunkStore, MyBlockComponent> getMyBlockComponent() {
    return this.myBlockComponent;
}

Codec types: Codec.INT, Codec.FLOAT, Codec.STRING, Codec.BOOLEAN, Codec.UUID_BINARY, BuilderCodec.STRING_ARRAY

Register a System (ECS)

Systems process components. Use RefSystem for add/remove events:

package org.example.plugin.systems;

import com.hypixel.hytale.component.*;
import com.hypixel.hytale.component.query.Query;
import com.hypixel.hytale.component.system.RefSystem;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
import org.example.plugin.MyPlugin;
import org.example.plugin.components.MyBlockComponent;

import org.checkerframework.checker.nullness.compatqual.NonNullDecl;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;

public class MyBlockSystem extends RefSystem<ChunkStore> {

    @NullableDecl
    @Override
    public Query<ChunkStore> getQuery() {
        return MyPlugin.get().getMyBlockComponent();
    }

    @Override
    public void onEntityAdded(@NonNullDecl Ref<ChunkStore> ref,
                              @NonNullDecl AddReason reason,
                              @NonNullDecl Store<ChunkStore> store,
                              @NonNullDecl CommandBuffer<ChunkStore> commandBuffer) {
        // Block with component placed
    }

    @Override
    public void onEntityRemove(@NonNullDecl Ref<ChunkStore> ref,
                               @NonNullDecl RemoveReason reason,
                               @NonNullDecl Store<ChunkStore> store,
                               @NonNullDecl CommandBuffer<ChunkStore> commandBuffer) {
        if (reason == RemoveReason.UNLOAD) return; // Ignore chunk unloads
        // Block removed
    }
}

Register:

import com.hypixel.hytale.component.system.ISystem;

@Override
protected void setup() {
    // Register component first, then system
    this.myBlockComponent = this.getChunkStoreRegistry().registerComponent(...);
    this.getChunkStoreRegistry().registerSystem((ISystem) new MyBlockSystem());
}

With Asset Pack

If your plugin includes models, textures, or items:

  1. Set includes_pack=true in gradle.properties

  2. Assets go in src/main/resources/:

    src/main/resources/
    ├── manifest.json
    ├── Common/
    │   ├── Blocks/
    │   ├── BlockTextures/
    │   └── Icons/
    └── Server/
        ├── Item/Items/
        └── Languages/
    
  3. Example item at src/main/resources/Server/Item/Items/My_Item.json:

    {
        "TranslationProperties": { "Name": "server.items.my_item.name" },
        "MaxStack": 64,
        "Icon": "Icons/my_item.png"
    }
    

Alternative: Separate Asset Pack

For larger projects or when assets need to update independently from code, use a separate pack/ folder instead of bundling assets in the JAR.

When to use separate pack folder:

  • Large asset packs (many models/textures)
  • Frequent asset updates without code changes
  • Collaborative development (artists can update without Java rebuilds)
  • Easier debugging (assets visible on disk, not in JAR)

Project structure with separate pack:

my-plugin/
├── src/main/
│   ├── java/org/example/plugin/
│   └── resources/
│       └── manifest.json          # Plugin manifest only
├── pack/                          # Separate asset pack
│   ├── manifest.json              # Pack manifest (different from plugin manifest!)
│   ├── Common/
│   │   ├── Blocks/
│   │   ├── BlockTextures/
│   │   └── Icons/
│   └── Server/
│       ├── Item/Items/
│       └── Languages/
├── build.gradle
└── gradle.properties

Pack manifest (pack/manifest.json):

{
    "Group": "Example",
    "Name": "MyPlugin",
    "Version": "1.0.0",
    "Description": "Asset pack for MyPlugin",
    "Authors": [{ "Name": "Your Name" }],
    "ServerVersion": "*",
    "Dependencies": {},
    "OptionalDependencies": {},
    "DisabledByDefault": false
}

Note: Pack manifest does NOT have Main or IncludesAssetPack fields.

Plugin manifest (src/main/resources/manifest.json):

Set IncludesAssetPack to false since assets are deployed separately:

{
    "IncludesAssetPack": false
}

Deployment gradle tasks:

Add to build.gradle:

// Deploy JAR to server
tasks.register('deployJar', Copy) {
    from tasks.jar
    into '/path/to/hytale-server/mods'
}

// Deploy asset pack to server
tasks.register('deployAssets', Sync) {
    from 'pack'
    into '/path/to/hytale-server/mods/MyPlugin'
}

// Deploy both
tasks.register('deploy') {
    dependsOn 'deployJar', 'deployAssets'
}

CI/CD with GitHub Actions

Add automated builds and releases with GitHub Actions.

Create .github/workflows/build.yml:

name: Build Plugin

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout Repository
      uses: actions/checkout@v4

    - name: Setup Java 25
      uses: actions/setup-java@v4
      with:
        java-version: '25'
        distribution: 'temurin'

    - name: Cache Gradle Dependencies
      uses: actions/cache@v4
      with:
        path: ~/.gradle
        key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
        restore-keys: |
          ${{ runner.os }}-gradle-

    - name: Grant Execute Permission for Gradlew
      run: chmod +x gradlew

    - name: Build with Gradle
      run: ./gradlew build

    - name: Upload Artifact
      uses: actions/upload-artifact@v4
      with:
        name: plugin-jar
        path: build/libs/*.jar

  release:
    needs: build
    runs-on: ubuntu-latest
    if: startsWith(github.ref, 'refs/tags/v')

    steps:
    - name: Checkout Repository
      uses: actions/checkout@v4

    - name: Setup Java 25
      uses: actions/setup-java@v4
      with:
        java-version: '25'
        distribution: 'temurin'

    - name: Grant Execute Permission for Gradlew
      run: chmod +x gradlew

    - name: Build with Gradle
      run: ./gradlew build

    - name: Create Release
      uses: softprops/action-gh-release@v1
      with:
        files: build/libs/*.jar
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

To create a release:

git tag v1.0.0
git push origin v1.0.0

This triggers the release job which creates a GitHub release with the JAR attached.

Troubleshooting

"Hytale install not found"

  • Ensure Hytale is installed via official launcher
  • Or set hytale_home in gradle.properties

"Main class not found"

  • Verify Main in manifest.json matches your class's fully qualified name
  • Check package declaration matches folder structure

"Plugin failed to load"

  • Check Java version: java -version should show 25+
  • Look for errors: ./gradlew build --stacktrace

QA Checklist

  • ./gradlew build completes without errors
  • JAR appears in build/libs/
  • manifest.json has correct Main class path
  • Server starts without plugin errors

Keep looking

Skills are one crate of 628,853. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.