hytale-plugin-basics

Create and structure Hytale server plugins with proper lifecycle, manifest, dependencies, and registries. Use when asked to "create a Hytale plugin", "make a Hytale mod", "start a new Hytale plugin", "setup plugin structure", or "write plugin boilerplate".

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "hytale-plugin-basics" with this command: npx skills add mnkyarts/hytale-skills/mnkyarts-hytale-skills-hytale-plugin-basics

Hytale Plugin Development Basics

Complete guide for creating Hytale server plugins following the official architecture patterns.

When to use this skill

Use this skill when:

  • Creating a new Hytale plugin from scratch
  • Setting up plugin project structure
  • Configuring plugin manifest (manifest.json)
  • Understanding plugin lifecycle hooks
  • Registering commands, events, or components
  • Managing plugin dependencies

Plugin Architecture Overview

Hytale plugins are Java-based extensions that run on the server. They follow an ECS (Entity Component System) architecture with lifecycle management.

Plugin Types

TypeLocationDescription
Core PluginsRegistered programmaticallyBuilt-in server functionality
Builtin Pluginsserver.jar/../builtin/Shipped with server
External Pluginsmods/ directoryUser-installed plugins
Early Pluginsearlyplugins/Bytecode transformers (advanced)

Plugin Lifecycle States

NONE -> SETUP -> START -> ENABLED -> SHUTDOWN -> DISABLED

Project Structure

my-plugin/
├── src/
│   └── main/
│       ├── java/
│       │   └── com/example/myplugin/
│       │       ├── MyPlugin.java
│       │       ├── commands/
│       │       ├── events/
│       │       ├── components/
│       │       └── systems/
│       └── resources/
│           ├── manifest.json
│           └── assets/           # Optional asset pack
│               └── Server/
│                   └── Content/
├── build.gradle
└── settings.gradle

Plugin Manifest (manifest.json)

Required file in JAR root defining plugin metadata:

{
  "Group": "com.example",
  "Name": "MyPlugin",
  "Version": "1.0.0",
  "Description": "My awesome Hytale plugin",
  "Authors": [
    {
      "Name": "Author Name",
      "Email": "author@example.com",
      "Url": "https://example.com"
    }
  ],
  "Website": "https://example.com/myplugin",
  "Main": "com.example.myplugin.MyPlugin",
  "ServerVersion": ">=1.0.0",
  "Dependencies": {
    "Hytale:NPCPlugin": ">=1.0.0"
  },
  "OptionalDependencies": {
    "Hytale:TeleportPlugin": "*"
  },
  "LoadBefore": {
    "Hytale:SomeOtherPlugin": "*"
  },
  "DisabledByDefault": false,
  "IncludesAssetPack": true
}

Manifest Fields

FieldRequiredDescription
GroupNoOrganization/namespace identifier
NameYesPlugin name (1-64 chars)
VersionNoSemantic version string
DescriptionNoHuman-readable description
AuthorsNoArray of author info objects
MainYesFully qualified main class name
ServerVersionNoRequired server version constraint
DependenciesNoRequired plugins with version constraints
OptionalDependenciesNoOptional plugins with version constraints
LoadBeforeNoPlugins this should load before
DisabledByDefaultNoStart disabled (default: false)
IncludesAssetPackNoHas embedded assets (default: false)

Main Plugin Class

Extend JavaPlugin and implement lifecycle methods:

package com.example.myplugin;

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 {
    
    // Required constructor
    public MyPlugin(@Nonnull JavaPluginInit init) {
        super(init);
    }
    
    @Override
    protected void setup() {
        // Called after config load
        // Register: commands, events, components, systems, codecs
        getLogger().atInfo().log("MyPlugin setup complete!");
    }
    
    @Override
    protected void start() {
        // Called after all plugins complete setup
        // Safe to interact with other plugins
        getLogger().atInfo().log("MyPlugin started!");
    }
    
    @Override
    protected void shutdown() {
        // Called before disable (in reverse load order)
        // Cleanup resources
        getLogger().atInfo().log("MyPlugin shutting down!");
    }
}

Available Registries

Access through PluginBase methods:

RegistryMethodPurpose
CommandsgetCommandRegistry()Register slash commands
EventsgetEventRegistry()Register event listeners
TasksgetTaskRegistry()Register async tasks
Block StatesgetBlockStateRegistry()Register block state types
EntitiesgetEntityRegistry()Register entity types
Client FeaturesgetClientFeatureRegistry()Register client features
AssetsgetAssetRegistry()Register asset stores
Entity ComponentsgetEntityStoreRegistry()Register ECS components/systems
Chunk ComponentsgetChunkStoreRegistry()Register chunk components/systems
CodecsgetCodecRegistry(codec)Register serializable types

Command Registration

@Override
protected void setup() {
    getCommandRegistry().registerCommand(new MyCommand());
}

// Command class
public class MyCommand extends Command {
    public MyCommand() {
        super("mycommand", "My command description");
        
        // Add arguments
        addArg(EntityArg.player("target"));
        addArg(IntArg.number("amount", 1, 100));
    }
    
    @Override
    public void execute(CommandContext ctx) {
        Player target = ctx.get("target");
        int amount = ctx.get("amount");
        ctx.sendMessage("Executed on " + target.getName() + " with " + amount);
    }
}

Event Registration

@Override
protected void setup() {
    // Global listener (all events of this type)
    getEventRegistry().registerGlobal(PlayerConnectEvent.class, this::onPlayerConnect);
    
    // Keyed listener (specific world/key)
    getEventRegistry().register(AddPlayerToWorldEvent.class, "world_name", this::onPlayerAddToWorld);
    
    // Priority listener
    getEventRegistry().registerGlobal(EventPriority.FIRST, SomeEvent.class, this::onSomeEvent);
}

private void onPlayerConnect(PlayerConnectEvent event) {
    getLogger().atInfo().log("Player connected: %s", event.getPlayer().getName());
}

Component Registration (ECS)

@Override
protected void setup() {
    // Register a component type
    ComponentType<EntityStore, MyComponent> myComponentType = 
        getEntityStoreRegistry().registerComponent(
            MyComponent.class, 
            MyComponent::new
        );
    
    // Register with serialization codec
    ComponentType<EntityStore, MyComponent> myComponentType = 
        getEntityStoreRegistry().registerComponent(
            MyComponent.class, 
            "myComponentName", 
            MyComponent.CODEC
        );
    
    // Register a system
    getEntityStoreRegistry().registerSystem(new MySystem());
}

Codec Registration

@Override
protected void setup() {
    // Register custom interaction type
    getCodecRegistry(Interaction.CODEC)
        .register("MyInteraction", MyInteraction.class, MyInteraction.CODEC);
    
    // Register custom action type
    getCodecRegistry(Action.CODEC)
        .register("MyAction", MyAction.class, MyAction.CODEC);
}

Plugin Configuration

Use withConfig() for typed configuration:

public class MyPlugin extends JavaPlugin {
    private final Config<MyConfig> config;
    
    public MyPlugin(@Nonnull JavaPluginInit init) {
        super(init);
        this.config = withConfig(MyConfig.CODEC);
    }
    
    @Override
    protected void setup() {
        MyConfig cfg = config.get();
        getLogger().atInfo().log("Config value: %s", cfg.someValue());
    }
}

// Config class
public record MyConfig(
    String someValue,
    int maxPlayers,
    boolean enableFeature
) {
    public static final BuilderCodec<MyConfig> CODEC = BuilderCodec.builder(
        Codec.STRING.required().fieldOf("SomeValue"),
        Codec.INT.required().fieldOf("MaxPlayers"),
        Codec.BOOL.optionalFieldOf("EnableFeature", true)
    ).constructor(MyConfig::new);
}

Config files are stored in config/{PluginGroup}.{PluginName}/config.json.

Accessing Server Resources

// Get the server instance
HytaleServer server = HytaleServer.get();

// Get the universe (world container)
Universe universe = server.getUniverse();

// Get a specific world
World world = universe.getWorld("world_name");

// Get the event bus
IEventBus eventBus = server.getEventBus();

// Get player by name
Optional<Player> player = server.getPlayerByName("PlayerName");

// Get asset registry
AssetRegistry assetRegistry = server.getAssetRegistry();

Dependency Injection Pattern

Access other plugins safely:

@Override
protected void start() {
    // Get optional dependency
    PluginBase teleportPlugin = getPluginManager()
        .getPlugin(PluginIdentifier.of("Hytale", "TeleportPlugin"))
        .orElse(null);
    
    if (teleportPlugin != null) {
        // Use teleport plugin features
    }
}

Gradle Build Configuration

plugins {
    id 'java'
}

group = 'com.example'
version = '1.0.0'

java {
    sourceCompatibility = JavaVersion.VERSION_21
    targetCompatibility = JavaVersion.VERSION_21
}

repositories {
    mavenCentral()
    // Add Hytale repository if available
}

dependencies {
    compileOnly 'com.hypixel.hytale:hytale-server-api:1.0.0'
}

jar {
    from('src/main/resources') {
        include 'manifest.json'
        include 'assets/**'
    }
}

Best Practices

Lifecycle Management

  1. setup(): Register all components, never interact with game state
  2. start(): Safe to interact with world, players, other plugins
  3. shutdown(): Clean up resources, save data, cancel tasks

Error Handling

@Override
protected void setup() {
    try {
        getCommandRegistry().registerCommand(new MyCommand());
    } catch (Exception e) {
        getLogger().atSevere().withCause(e).log("Failed to register command");
    }
}

Logging

The Hytale server uses a fluent logging API:

// Use the built-in logger with fluent API
getLogger().atInfo().log("Information message");
getLogger().atWarning().log("Warning message");
getLogger().atSevere().log("Error message");  // or atSevere().withCause(exception).log("Error message")
getLogger().atFine().log("Debug message");

// With string formatting
getLogger().atInfo().log("Player %s connected", playerName);

// With exception
getLogger().atSevere().withCause(exception).log("Failed to process request");

Note: The logger does NOT use .info(), .warn(), .error() methods directly. Always use the fluent pattern: .atLevel().log("message").

Resource Cleanup

All registrations through plugin registries are automatically cleaned up when the plugin is disabled. For custom resources:

private ScheduledFuture<?> task;

@Override
protected void start() {
    task = scheduler.scheduleAtFixedRate(this::doWork, 0, 1, TimeUnit.SECONDS);
}

@Override
protected void shutdown() {
    if (task != null) {
        task.cancel(false);
    }
}

Troubleshooting

Plugin Not Loading

  1. Check manifest.json is in JAR root
  2. Verify Main class path is correct
  3. Check for dependency version mismatches
  4. Look for exceptions in server logs

Class Not Found

  1. Ensure dependencies are marked compileOnly
  2. Check plugin load order via Dependencies/LoadBefore
  3. Verify JAR contains all required classes

Events Not Firing

  1. Confirm registration happens in setup()
  2. Check event key matches (for keyed events)
  3. Verify event priority order
  4. Ensure event isn't being cancelled

Scaffolding Scripts

Use the provided scripts to quickly create a new plugin project with the complete directory structure and boilerplate code.

Linux/macOS

# Interactive mode (prompts for all values)
./scripts/create-plugin.sh

# With arguments
./scripts/create-plugin.sh <PluginName> [Group] [Version] [Author] [Description]

# Example
./scripts/create-plugin.sh MyAwesomePlugin com.mycompany 1.0.0 "John Doe" "A cool plugin"

Windows

:: Interactive mode (prompts for all values)
scripts\create-plugin.bat

:: With arguments
scripts\create-plugin.bat <PluginName> [Group] [Version] [Author] [Description]

:: Example
scripts\create-plugin.bat MyAwesomePlugin com.mycompany 1.0.0 "John Doe" "A cool plugin"

Generated Structure

The scripts create the following project structure:

my-plugin/
├── src/main/java/com/example/myplugin/
│   ├── MyPlugin.java              # Main plugin class with lifecycle methods
│   ├── commands/
│   │   └── ExampleCommand.java    # Example command implementation
│   ├── events/
│   │   └── PlayerEventHandler.java # Example event handler
│   ├── components/                 # Directory for ECS components
│   └── systems/                    # Directory for ECS systems
├── src/main/resources/
│   ├── manifest.json              # Plugin manifest with metadata
│   └── assets/Server/Content/     # Asset pack directory (optional)
├── build.gradle                   # Gradle build configuration (Java 21)
├── settings.gradle                # Gradle project settings
└── .gitignore                     # Git ignore rules

Script Parameters

ParameterRequiredDefaultDescription
PluginNameYes-Name of the plugin (1-64 alphanumeric chars, starts with letter)
GroupNocom.exampleMaven group/Java package prefix
VersionNo1.0.0Semantic version string
AuthorNoAuthorPlugin author name
DescriptionNo(empty)Human-readable plugin description

See references/plugin-lifecycle.md for detailed lifecycle documentation. See references/registry-patterns.md for advanced registration patterns.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

hytale-ui-windows

No summary provided by upstream source.

Repository SourceNeeds Review
General

hytale-networking

No summary provided by upstream source.

Repository SourceNeeds Review
General

hytale-crafting-recipes

No summary provided by upstream source.

Repository SourceNeeds Review
General

hytale-custom-entities

No summary provided by upstream source.

Repository SourceNeeds Review