Hytale's server runs on Java and exposes a plugin API for extending gameplay, listening to events, registering commands, and managing permissions. Plugins are loaded from the mods/ directory and follow a well-defined lifecycle. This reference is based on Hytale server version 2026.03.26.
Plugin Lifecycle
Every plugin extends JavaPlugin, which itself extends PluginBase. The server manages plugins through a state machine:
NONE → SETUP → START → ENABLED → SHUTDOWN → DISABLED
If any stage fails, the plugin transitions to FAILED and is not loaded.
package com.example.myplugin;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
public class MyPlugin extends JavaPlugin {
public MyPlugin(JavaPluginInit init) {
super(init);
}
@Override
protected void setup() {
getLogger().info("Plugin setting up...");
// Register event listeners
getEventRegistry().register(
PlayerConnectEvent.class,
this::onPlayerConnect
);
// Register commands
getCommandRegistry().register(new SpawnCommand());
}
@Override
protected void start() {
getLogger().info("Plugin started.");
}
@Override
protected void shutdown() {
getLogger().info("Plugin shutting down.");
}
}
During setup(), you have access to several registries through PluginBase:
getEventRegistry()— register event listenersgetCommandRegistry()— register commandsgetEntityRegistry()— register custom entity typesgetTaskRegistry()— schedule recurring tasksgetAssetRegistry()— register custom assetsgetEntityStoreRegistry()— register ECS components for entitiesgetChunkStoreRegistry()— register ECS components for chunks
The PluginSetupEvent fires when a plugin initializes, which other plugins can listen for if they need to coordinate loading order.
The EventBus
Hytale's event system is built around the EventBus class. It supports both synchronous and asynchronous event handling with priority ordering.
Registering a Sync Listener
getEventRegistry().register(
PlayerConnectEvent.class,
event -> {
PlayerRef player = event.getPlayerRef();
World world = event.getWorld();
getLogger().info("Player connected to " + world);
}
);
Priority Ordering
Listeners execute in priority order. Higher-priority listeners run first, which is useful when multiple plugins need to process the same event:
getEventRegistry().register(
EventPriority.HIGH,
PlayerChatEvent.class,
event -> {
// This runs before normal-priority listeners
String message = event.getMessage();
getLogger().info("Chat: " + message);
}
);
Async Event Handling
Some events support asynchronous processing. Use registerAsync() when your handler needs to perform I/O or other blocking work without stalling the server tick:
getEventRegistry().registerAsync(
PlayerConnectEvent.class,
future -> future.thenApply(event -> {
// This runs on a separate thread
// Load player data from a database, etc.
return event;
})
);
Note: Synchronous listeners (
register) run on the server tick thread and block execution until they complete. Asynchronous listeners (registerAsync) run off the main thread and return aCompletableFuture. Use async for database lookups, HTTP calls, or anything that would otherwise lag the server.
Cancellable Events
Many gameplay events extend CancellableEcsEvent, which lets you prevent the default behavior. For example, you can stop a player from placing a block:
getEventRegistry().register(
PlaceBlockEvent.class,
event -> {
if (event.isCancelled()) return; // Another plugin may have cancelled it
Vector3i target = event.getTargetBlock();
ItemStack item = event.getItemInHand();
// Prevent placing blocks above Y=200
if (target.getY() > 200) {
event.setCancelled(true);
}
}
);
Warning: When handling a cancellable event, always check
isCancelled()at the top of your listener. A higher-priority plugin may have already cancelled the event, and processing it again could cause unexpected behavior.
Event Catalog
Here are the key server events available in version 2026.03.26:
Player Events
| Event | Cancellable | Description |
|---|---|---|
PlayerConnectEvent | No | Fired when a player connects. Provides PlayerRef, Holder<EntityStore>, and optional World. |
PlayerDisconnectEvent | No | Fired on disconnect. Includes DisconnectReason. |
PlayerReadyEvent | No | Fired when the player is fully loaded and ready to play. |
PlayerChatEvent | No | Fired when a player sends a chat message. Provides the message text. |
PlayerMouseButtonEvent | No | Fired on mouse input. Provides item in hand, target block/entity, and button type. |
PlayerInteractEvent | Yes | Fired when a player interacts with a block or entity. |
PlayerCraftEvent | No | Fired when a player crafts an item. |
Block and Item Events
| Event | Cancellable | Description |
|---|---|---|
PlaceBlockEvent | Yes | Fired when a block is placed. Provides ItemStack, target position (Vector3i), and RotationTuple. |
BreakBlockEvent | Yes | Fired when a block is broken. Provides ItemStack, target position, and BlockType. |
DamageBlockEvent | Yes | Fired when a block takes damage (before breaking). |
UseBlockEvent.Pre | Yes | Fired before a block interaction. Includes InteractionType and InteractionContext. |
UseBlockEvent.Post | No | Fired after a block interaction completes. |
DropItemEvent | Yes | Fired when an item is dropped. Subtypes: Drop and PlayerRequest. |
InteractivelyPickupItemEvent | Yes | Fired when a player picks up an item from the world. |
Crafting and Discovery Events
| Event | Cancellable | Description |
|---|---|---|
CraftRecipeEvent.Pre | Yes | Fired before a recipe is crafted. Provides CraftingRecipe and quantity. |
CraftRecipeEvent.Post | No | Fired after crafting completes. |
DiscoverZoneEvent.Display | Yes | Fired when a zone discovery UI would display. Control visibility with setDisplay(). |
Server Events
| Event | Cancellable | Description |
|---|---|---|
BootEvent | No | Fired when the server starts. |
ShutdownEvent | No | Fired when the server is shutting down. |
PrepareUniverseEvent | No | Fired when the universe is being prepared. |
PluginSetupEvent | No | Fired when a plugin completes setup. |
Permission Events
| Event | Cancellable | Description |
|---|---|---|
PlayerPermissionChangeEvent | No | Fired when a player's permissions change. |
PlayerGroupEvent.Added | No | Fired when a player is added to a permission group. |
PlayerGroupEvent.Removed | No | Fired when a player is removed from a permission group. |
GroupPermissionChangeEvent | No | Fired when a group's permissions are modified. |
Command Registration
Commands are defined by extending one of the AbstractCommand subclasses:
AbstractPlayerCommand— requires a player senderAbstractWorldCommand— requires a world contextAbstractAsyncCommand— executes asynchronouslyAbstractTargetPlayerCommand— targets another playerAbstractTargetEntityCommand— targets an entity
import com.hypixel.hytale.server.core.command.AbstractPlayerCommand;
import com.hypixel.hytale.server.core.command.CommandContext;
public class HealCommand extends AbstractPlayerCommand {
public HealCommand() {
super("heal");
setPermission("myplugin.command.heal");
setDescription("Restore full health");
}
@Override
protected void execute(CommandContext context) {
// Heal the player who ran the command
PlayerRef player = context.getPlayerRef();
// ... apply healing logic
context.sendMessage("You have been healed.");
}
}
Register the command in your plugin's setup() method:
@Override
protected void setup() {
getCommandRegistry().register(new HealCommand());
}
Commands support typed arguments, optional flags, and subcommand hierarchies. Permission strings are auto-generated from the command path (e.g., plugin.command.heal), but you can also set them explicitly.
Permissions
Hytale's permission system uses dot-notation strings under the hytale.* namespace. The PermissionProvider interface lets plugins define custom permission backends:
// Check a permission on a player
if (player.hasPermission("myplugin.feature.fly")) {
// Enable flight
}
Built-in permission namespaces include:
hytale.command.*— auto-generated from registered commandshytale.editor.*— editor tools and builder brusheshytale.camera.*— camera modes like flycamhytale.world_map.teleport.*— map teleportation
Players can be added to permission groups, and changes fire PlayerPermissionChangeEvent and PlayerGroupEvent so other plugins can react.
Putting It Together
Here's a complete example: a plugin that welcomes players on connect and provides a /spawn command.
package com.example.welcomeplugin;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent;
import com.hypixel.hytale.server.core.command.AbstractPlayerCommand;
import com.hypixel.hytale.server.core.command.CommandContext;
import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport;
import com.hypixel.hytale.math.vector.Vector3d;
import com.hypixel.hytale.math.vector.Vector3f;
public class WelcomePlugin extends JavaPlugin {
public WelcomePlugin(JavaPluginInit init) {
super(init);
}
@Override
protected void setup() {
getEventRegistry().register(
PlayerConnectEvent.class,
event -> {
getLogger().info(
"Player " + event.getPlayerRef() + " connected."
);
}
);
getCommandRegistry().register(new SpawnCommand());
}
private static class SpawnCommand extends AbstractPlayerCommand {
SpawnCommand() {
super("spawn");
setPermission("welcome.command.spawn");
setDescription("Teleport to world spawn");
}
@Override
protected void execute(CommandContext context) {
Teleport teleport = Teleport.createForPlayer(
new Vector3d(0, 64, 0),
new Vector3f(0, 0, 0)
);
// Apply teleport component to the player entity
context.sendMessage("Teleporting to spawn...");
}
}
}
Plugin development checklist
- Extend JavaPlugin and implement setup(), start(), and shutdown()
- Register event listeners in setup() via getEventRegistry()
- Register commands in setup() via getCommandRegistry()
- Check isCancelled() before processing cancellable events
- Use registerAsync() for listeners that perform I/O
- Set explicit permissions on commands
- Place your compiled JAR in the mods/ directory
- Test on a local server before deploying