Files
hytale-server/docs/02-plugin-development.md

9.4 KiB

Plugin Development

Plugin Structure

A Hytale plugin consists of:

  1. A main class extending JavaPlugin
  2. A manifest.json file with plugin metadata
  3. 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

  1. Register everything in setup() - Commands, events, entities should all be registered during setup
  2. Use start() for initialization logic - Access other plugins, connect to databases, etc.
  3. Clean up in shutdown() - Close connections, save data, cancel tasks
  4. Use configuration - Make your plugin configurable instead of hardcoding values
  5. Handle errors gracefully - Log errors but don't crash the server
  6. Respect the lifecycle - Don't access other plugins during setup, wait for start()
  7. Use meaningful permissions - Follow the automatic permission naming convention