Hytale Plugin Development
Plugins let you extend Hytale with Java code. You can add custom commands, handle events, create new game systems, and build complex features that packs alone can't do.
Hytale is in Early Access. The API may change and official documentation is incomplete. This guide is based on verified community research and official sources.
What You Need
- Java 25 - Download from adoptium.net
- IntelliJ IDEA - Free Community Edition from jetbrains.com
- Basic Java knowledge - Variables, classes, methods, etc.
Quick Start
The fastest way to start is with the official template by Darkhax & Jared:
git clone https://github.com/realBritakee/hytale-template-plugin.git
cd hytale-template-plugin
./gradlew build
The template includes:
- Latest Hytale server files in classpath
- IDE-integrated debugging with breakpoints
- Asset bundling (editable via in-game Asset Editor)
- Pre-configured Gradle build system
- Example code and assets
Project Structure
A Hytale plugin project looks like this:
manifest.json
{
"Group": "com.yourname",
"Name": "MyPlugin",
"Version": "1.0.0",
"Description": "My first Hytale plugin",
"Authors": [
{
"Name": "Your Name",
"Email": "your.email@example.com",
"Url": "https://your-website.com"
}
],
"Website": "https://your-plugin-website.com",
"ServerVersion": "*",
"Dependencies": {},
"OptionalDependencies": {},
"DisabledByDefault": false
}
Gradle Configuration
plugins {
id("java")
id("com.github.johnrengelman.shadow") version "8.1.1"
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(25))
}
}
dependencies {
compileOnly("com.hypixel.hytale:hytale-server:+")
}
tasks.shadowJar {
archiveClassifier.set("")
}
rootProject.name = 'my-plugin'
hytale_version=latest
plugin_version=1.0.0
API Packages
| Package | Purpose |
|---|---|
com.hypixel.hytale.plugin | Plugin base classes and initialization |
com.hypixel.hytale.plugin.early | Bootstrap/early plugins and class transformers |
com.hypixel.hytale.api.event | Event system and EventBus |
com.hypixel.hytale.api.command | Command system and registration |
com.hypixel.hytale.api.permission | Permission checking and management |
Main Plugin Class
Your plugin must extend JavaPlugin and implement the lifecycle methods:
package com.yourname.plugin;
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 {
public MyPlugin(@Nonnull JavaPluginInit init) {
super(init);
getLogger().info("MyPlugin loaded!");
}
@Override
public void setup() {
// Called after constructor
// Register systems and load configs here
getLogger().info("MyPlugin setup complete!");
}
@Override
public void start() {
// Called when server is ready
// Register commands and event listeners here
getLogger().info("MyPlugin started!");
}
@Override
public void shutdown() {
// Called when server stops
// Save data and cleanup here
getLogger().info("MyPlugin shutting down!");
}
}
Plugin Lifecycle
| Method | When Called | Use For |
|---|---|---|
constructor | Plugin loaded | Initial setup, load configs |
setup() | After constructor | Register systems, ECS stores |
start() | Server ready | Register events, commands |
shutdown() | Server stopping | Save data, cleanup |
Handling Events
Hytale uses an EventBus for event handling. Events are registered using getEventRegistry().
Registering Event Handlers
import com.hypixel.hytale.server.core.event.events.PlayerConnectEvent;
import com.hypixel.hytale.server.core.event.events.PlayerDisconnectEvent;
import com.hypixel.hytale.server.core.event.events.PlayerChatEvent;
@Override
public void start() {
// Listen for player connect
getEventRegistry().register(PlayerConnectEvent.class, event -> {
var player = event.getPlayer();
getLogger().info(player.getName() + " connected!");
});
// Listen for player disconnect
getEventRegistry().register(PlayerDisconnectEvent.class, event -> {
var player = event.getPlayer();
getLogger().info(player.getName() + " disconnected!");
});
// Listen for chat (async event)
getEventRegistry().registerAsync(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
String message = event.getMessage();
getLogger().info("Chat: " + message);
return event;
});
});
}
Event Priority
You can control when your handler runs relative to others:
import com.hypixel.hytale.server.core.event.EventPriority;
// Run before other handlers
getEventRegistry().register(EventPriority.FIRST, PlayerConnectEvent.class, event -> {
// This runs first
});
// Run after other handlers
getEventRegistry().register(EventPriority.LAST, PlayerConnectEvent.class, event -> {
// This runs last
});
| Priority | Value | Use For |
|---|---|---|
FIRST | -21844 | Run before everything |
EARLY | -10922 | Run early |
NORMAL | 0 | Default priority |
LATE | 10922 | Run late |
LAST | 21844 | Run after everything |
Player Events
| Event | When It Fires | Cancellable |
|---|---|---|
PlayerSetupConnectEvent | Before player connects | Yes |
PlayerConnectEvent | Player connects | No |
PlayerReadyEvent | Player fully loaded | No |
PlayerDisconnectEvent | Player disconnects | No |
PlayerChatEvent | Player sends message | Yes (async) |
PlayerInteractEvent | Player interacts | Yes |
PlayerMouseButtonEvent | Mouse button pressed | Yes |
PlayerMouseMotionEvent | Mouse moved | Yes |
PlayerCraftEvent | Player crafts item | No |
Block Events (ECS)
| Event | When It Fires | Cancellable |
|---|---|---|
BreakBlockEvent | Block is broken | Yes |
PlaceBlockEvent | Block is placed | Yes |
DamageBlockEvent | Block takes damage | Yes |
UseBlockEvent.Pre | Before using block | Yes |
UseBlockEvent.Post | After using block | No |
Cancelling Events
Events that implement ICancellable can be cancelled:
import com.hypixel.hytale.server.core.event.events.BreakBlockEvent;
getEventRegistry().register(BreakBlockEvent.class, event -> {
var block = event.getBlock();
// Prevent breaking bedrock
if (block.getType().getName().equals("bedrock")) {
event.setCancelled(true);
}
});
Creating Commands
Commands are registered using getCommandRegistry().
Player Command
import com.hypixel.hytale.server.core.command.AbstractPlayerCommand;
import com.hypixel.hytale.server.core.command.CommandContext;
import java.util.concurrent.CompletableFuture;
public class HealCommand extends AbstractPlayerCommand {
public HealCommand() {
super("heal", "Restore your health to maximum");
}
@Override
public CompletableFuture<Void> execute(CommandContext context) {
var player = context.getPlayer();
player.setHealth(player.getMaxHealth());
player.sendMessage("You have been healed!");
return CompletableFuture.completedFuture(null);
}
}
Async Command
For commands that do I/O or take time:
import com.hypixel.hytale.server.core.command.AbstractAsyncCommand;
public class StatsCommand extends AbstractAsyncCommand {
public StatsCommand() {
super("stats", "View player statistics");
}
@Override
public CompletableFuture<Void> execute(CommandContext context) {
return CompletableFuture.runAsync(() -> {
// Fetch data from database (async)
var stats = fetchPlayerStats(context.getPlayer());
context.getPlayer().sendMessage("Your stats: " + stats);
});
}
}
Registering Commands
@Override
public void start() {
getCommandRegistry().register(new HealCommand());
getCommandRegistry().register(new StatsCommand());
}
Permissions
Check player permissions before allowing actions:
if (player.hasPermission("myplugin.admin")) {
// Player has admin permission
player.sendMessage("You are an admin!");
} else {
player.sendMessage("You don't have permission for that.");
}
Permission Events
Listen for permission changes:
| Event | When It Fires |
|---|---|
PlayerPermissionChangeEvent.PermissionsAdded | Permissions added to player |
PlayerPermissionChangeEvent.PermissionsRemoved | Permissions removed from player |
PlayerPermissionChangeEvent.GroupAdded | Player added to group |
PlayerPermissionChangeEvent.GroupRemoved | Player removed from group |
Configuration Files
Load configuration in your constructor using withConfig():
public class MyPlugin extends JavaPlugin {
private MyConfig config;
public MyPlugin(@Nonnull JavaPluginInit init) {
super(init);
// Load config file
this.config = withConfig("config.json", MyConfigCodec.INSTANCE);
}
}
Data Directory
Get your plugin's data folder:
Path dataDir = getDataDirectory();
Path playerDataFile = dataDir.resolve("playerdata.json");
Task Scheduling
Schedule tasks using getTaskRegistry():
// Run a task later
getTaskRegistry().runLater(() -> {
getLogger().info("This runs after a delay!");
}, Duration.ofSeconds(5));
// Run a repeating task
getTaskRegistry().runRepeating(() -> {
getLogger().info("This runs every 10 seconds!");
}, Duration.ofSeconds(10));
Available Registries
Your plugin has access to these registries:
| Registry | Method | Use For |
|---|---|---|
| Events | getEventRegistry() | Event listeners |
| Commands | getCommandRegistry() | Chat commands |
| Tasks | getTaskRegistry() | Scheduled tasks |
| Entity Stores | getEntityStoreRegistry() | ECS systems |
Building Your Plugin
To create a JAR file:
./gradlew build
The JAR appears in build/libs/.
Installation
Copy the JAR to your mods folder:
%appdata%\Hytale\UserData\Mods\
C:\Users\YourUsername\AppData\Roaming\Hytale\UserData\Mods\
Debugging & Development Tips
The template includes a pre-configured debug setup:
- Open your project in IntelliJ IDEA
- Find the "HytaleServer" run configuration
- Click the bug icon to run with debugger
- Set breakpoints in your code
- Your breakpoints will pause execution!
Remote Debugging
Connect your IDE debugger to port 5005 for remote debugging. The server auto-reloads on file changes in development mode.
Useful Launch Arguments
| Argument | Description |
|---|---|
--disable-sentry | Disable crash reporting (useful for development) |
--assets <path> | Path to assets zip file (required) |
--bind <port> | Custom port (default 5520) |
--early-plugins <path> | Custom path for bootstrap plugins |
Use --disable-sentry during development to prevent crash reports from being sent. Logging methods: getLogger().info(), getLogger().warn(), getLogger().error().
Troubleshooting
| Problem | Solution |
|---|---|
| Gradle sync fails | Verify Java 25 is installed and configured in IntelliJ |
| Missing run config | Re-import Gradle project (File → Sync Project) |
| Plugin not loading | Check manifest.json is valid, check server logs |
| Breakpoints not working | Make sure you're running in Debug mode (bug icon) |
Bootstrap/Early Plugins
For advanced low-level modifications, Bootstrap plugins run before the main server starts. They allow bytecode injection and class transformations.
Purpose
- Class transformations and bytecode injection
- Modify classes as they load
- Runs before main server initialization
- Can even modify
HytaleServerclass
Setup
- Create
earlyplugins/folder manually in server directory - Or use
--early-plugins <path>launch argument for custom paths - Implement
ClassTransformerinterface - Create service loader file at
META-INF/services/
Creating a Class Transformer
package com.example.early;
import com.hypixel.hytale.plugin.early.ClassTransformer;
public class ExampleTransformer implements ClassTransformer {
@Override
public byte[] transform(String className, byte[] classBytes) {
// Modify bytecode here using ASM or Mixin
return classBytes;
}
@Override
public int getPriority() {
return 0; // Higher values run first
}
}
Service Loader File
Create a file at META-INF/services/com.hypixel.hytale.plugin.early.ClassTransformer:
com.example.early.ExampleTransformer
Bootstrap plugins can destabilize the game if used incorrectly. Use sparingly and only when regular plugins cannot achieve your goal. Some packages are restricted from modification. Use ASM or Mixin libraries for bytecode manipulation.