9.4 KiB
Plugin Development
Plugin Structure
A Hytale plugin consists of:
- A main class extending
JavaPlugin - A
manifest.jsonfile with plugin metadata - Optional asset packs for client-side resources
The Plugin Manifest
The manifest.json file must be placed at the root of your JAR:
{
"Group": "com.example",
"Name": "MyPlugin",
"Version": "1.0.0",
"Description": "A description of what this plugin does",
"Authors": ["Author1", "Author2"],
"Website": "https://example.com",
"Main": "com.example.MyPlugin",
"ServerVersion": ">=1.0.0",
"Dependencies": {
"com.other:OtherPlugin": ">=2.0.0"
},
"OptionalDependencies": {
"com.soft:SoftDep": ">=1.0.0"
},
"LoadBefore": ["com.load:BeforeThis"],
"DisabledByDefault": false,
"IncludesAssetPack": true
}
Manifest Fields
| Field | Type | Required | Description |
|---|---|---|---|
Group |
String | Yes | Maven-style group ID (e.g., com.example) |
Name |
String | Yes | Plugin name (unique identifier within group) |
Version |
String | Yes | Semantic version (e.g., 1.0.0) |
Description |
String | No | Brief description of the plugin |
Authors |
String[] | No | List of author names |
Website |
String | No | Plugin website or repository URL |
Main |
String | Yes | Fully qualified main class name |
ServerVersion |
String | No | Required server version range |
Dependencies |
Object | No | Required plugin dependencies with version ranges |
OptionalDependencies |
Object | No | Optional plugin dependencies |
LoadBefore |
String[] | No | Plugins that should load after this one |
DisabledByDefault |
Boolean | No | If true, plugin must be explicitly enabled |
IncludesAssetPack |
Boolean | No | If true, plugin includes client assets |
Plugin Identifier
Plugins are identified by Group:Name format (e.g., com.example:MyPlugin). This identifier is used for:
- Dependency resolution
- Permission namespacing (
com.example.myplugin.*) - Configuration keys
The JavaPlugin Class
package com.example;
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
import com.hypixel.hytale.server.core.plugin.java.JavaPluginInit;
public class MyPlugin extends JavaPlugin {
private Config<MyConfig> config;
public MyPlugin(JavaPluginInit init) {
super(init);
// Initialize config BEFORE setup() is called
this.config = withConfig(MyConfig.CODEC);
}
@Override
protected void setup() {
// Register all components during setup
registerCommands();
registerEvents();
registerEntities();
}
@Override
protected void start() {
// Plugin is now active
// Access other plugins, start services, etc.
}
@Override
protected void shutdown() {
// Clean up resources
// Save data, close connections, etc.
}
private void registerCommands() {
getCommandRegistry().registerCommand(new MyCommand());
}
private void registerEvents() {
getEventRegistry().register(PlayerConnectEvent.class, this::onPlayerConnect);
}
private void registerEntities() {
getEntityRegistry().register("customEntity", CustomEntity.class, CustomEntity::new);
}
private void onPlayerConnect(PlayerConnectEvent event) {
getLogger().info("Player connected: " + event.getPlayer().getName());
}
}
Plugin Lifecycle States
NONE -> SETUP -> START -> ENABLED -> SHUTDOWN -> DISABLED
| State | Description |
|---|---|
NONE |
Initial state before any lifecycle methods |
SETUP |
setup() is executing; register components here |
START |
start() is executing; plugin becoming active |
ENABLED |
Plugin is fully operational and handling events |
SHUTDOWN |
shutdown() is executing; cleanup in progress |
DISABLED |
Plugin is fully disabled and unloaded |
Configuration
Defining a Configuration Class
public class MyConfig {
public static final Codec<MyConfig> CODEC = BuilderCodec.of(MyConfig::new)
.with("enabled", Codec.BOOLEAN, c -> c.enabled, true)
.with("maxPlayers", Codec.INTEGER, c -> c.maxPlayers, 100)
.with("welcomeMessage", Codec.STRING, c -> c.welcomeMessage, "Welcome!")
.build();
public final boolean enabled;
public final int maxPlayers;
public final String welcomeMessage;
private MyConfig(boolean enabled, int maxPlayers, String welcomeMessage) {
this.enabled = enabled;
this.maxPlayers = maxPlayers;
this.welcomeMessage = welcomeMessage;
}
}
Using Configuration
public class MyPlugin extends JavaPlugin {
private Config<MyConfig> config;
public MyPlugin(JavaPluginInit init) {
super(init);
this.config = withConfig(MyConfig.CODEC);
}
@Override
protected void setup() {
MyConfig cfg = config.get();
if (cfg.enabled) {
getLogger().info("Max players: " + cfg.maxPlayers);
}
}
}
Configuration is stored in the server's config under your plugin identifier.
Available Registries
CommandRegistry
Register custom commands:
getCommandRegistry().registerCommand(new MyCommand());
See Command System for details.
EventRegistry
Register event listeners:
// Simple registration
getEventRegistry().register(PlayerConnectEvent.class, this::onConnect);
// With priority
getEventRegistry().register(EventPriority.EARLY, PlayerConnectEvent.class, this::onConnect);
// Global listener (all events of type)
getEventRegistry().registerGlobal(EntityEvent.class, this::onAnyEntity);
// Async event
getEventRegistry().registerAsync(AsyncEvent.class, this::onAsync);
See Event System for details.
EntityRegistry
Register custom entity types:
getEntityRegistry().register("myEntity", MyEntity.class, MyEntity::new);
// With serialization codec
getEntityRegistry().register("myEntity", MyEntity.class, MyEntity::new, MyEntity.CODEC);
BlockStateRegistry
Register custom block states:
getBlockStateRegistry().register("myBlock", MyBlockState.class);
TaskRegistry
Schedule recurring tasks:
getTaskRegistry().register(new MyTask());
AssetRegistry
Register custom asset types:
getAssetRegistry().register(MyAsset.class, MyAsset.CODEC);
ClientFeatureRegistry
Register client-side features:
getClientFeatureRegistry().register("myFeature", MyFeature.class);
Permissions
Plugins automatically receive a base permission derived from their identifier:
{group}.{name} -> com.example.myplugin
Commands registered by your plugin will have permissions under:
{basePermission}.command.{commandName}
For example, if your plugin is com.example:MyPlugin and you register a command spawn:
- Base permission:
com.example.myplugin - Command permission:
com.example.myplugin.command.spawn
Logging
Use the built-in logger:
getLogger().info("Information message");
getLogger().warn("Warning message");
getLogger().error("Error message");
getLogger().debug("Debug message");
Dependencies
Hard Dependencies
Hard dependencies must be present for your plugin to load:
{
"Dependencies": {
"com.example:RequiredPlugin": ">=1.0.0"
}
}
Optional Dependencies
Optional dependencies are loaded if present but not required:
{
"OptionalDependencies": {
"com.example:OptionalPlugin": ">=1.0.0"
}
}
Check if optional dependency is loaded:
@Override
protected void setup() {
if (getPluginManager().isPluginLoaded("com.example:OptionalPlugin")) {
// Optional plugin is available
}
}
Load Order
Use LoadBefore to ensure specific plugins load after yours:
{
"LoadBefore": ["com.example:LoadAfterMe"]
}
Asset Packs
If your plugin includes client-side assets, set IncludesAssetPack to true:
{
"IncludesAssetPack": true
}
Place assets in your JAR under the assets/ directory following Hytale's asset structure.
Best Practices
- Register everything in
setup()- Commands, events, entities should all be registered during setup - Use
start()for initialization logic - Access other plugins, connect to databases, etc. - Clean up in
shutdown()- Close connections, save data, cancel tasks - Use configuration - Make your plugin configurable instead of hardcoding values
- Handle errors gracefully - Log errors but don't crash the server
- Respect the lifecycle - Don't access other plugins during setup, wait for start()
- Use meaningful permissions - Follow the automatic permission naming convention