home / skills / mnkyarts / hytale-skills / hytale-plugin-basics

hytale-plugin-basics skill

/skills/hytale-plugin-basics

npx playbooks add skill mnkyarts/hytale-skills --skill hytale-plugin-basics

Review the files below or copy the command above to add this skill to your agents.

Files (5)
SKILL.md
14.2 KB
---
name: hytale-plugin-basics
description: 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".
metadata:
  author: Liam Robinson (MnkyArts)
  version: "1.0.0"
---

# 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

| Type | Location | Description |
|------|----------|-------------|
| Core Plugins | Registered programmatically | Built-in server functionality |
| Builtin Plugins | `server.jar/../builtin/` | Shipped with server |
| External Plugins | `mods/` directory | User-installed plugins |
| Early Plugins | `earlyplugins/` | 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:

```json
{
  "Group": "com.example",
  "Name": "MyPlugin",
  "Version": "1.0.0",
  "Description": "My awesome Hytale plugin",
  "Authors": [
    {
      "Name": "Author Name",
      "Email": "[email protected]",
      "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

| Field | Required | Description |
|-------|----------|-------------|
| `Group` | No | Organization/namespace identifier |
| `Name` | Yes | Plugin name (1-64 chars) |
| `Version` | No | Semantic version string |
| `Description` | No | Human-readable description |
| `Authors` | No | Array of author info objects |
| `Main` | Yes | Fully qualified main class name |
| `ServerVersion` | No | Required server version constraint |
| `Dependencies` | No | Required plugins with version constraints |
| `OptionalDependencies` | No | Optional plugins with version constraints |
| `LoadBefore` | No | Plugins this should load before |
| `DisabledByDefault` | No | Start disabled (default: false) |
| `IncludesAssetPack` | No | Has embedded assets (default: false) |

## Main Plugin Class

Extend `JavaPlugin` and implement lifecycle methods:

```java
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:

| Registry | Method | Purpose |
|----------|--------|---------|
| Commands | `getCommandRegistry()` | Register slash commands |
| Events | `getEventRegistry()` | Register event listeners |
| Tasks | `getTaskRegistry()` | Register async tasks |
| Block States | `getBlockStateRegistry()` | Register block state types |
| Entities | `getEntityRegistry()` | Register entity types |
| Client Features | `getClientFeatureRegistry()` | Register client features |
| Assets | `getAssetRegistry()` | Register asset stores |
| Entity Components | `getEntityStoreRegistry()` | Register ECS components/systems |
| Chunk Components | `getChunkStoreRegistry()` | Register chunk components/systems |
| Codecs | `getCodecRegistry(codec)` | Register serializable types |

### Command Registration

```java
@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

```java
@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)

```java
@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

```java
@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:

```java
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

```java
// 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:

```java
@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

```groovy
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

```java
@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:

```java
// 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:

```java
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

```bash
# 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

```batch
:: 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

| Parameter | Required | Default | Description |
|-----------|----------|---------|-------------|
| PluginName | Yes | - | Name of the plugin (1-64 alphanumeric chars, starts with letter) |
| Group | No | `com.example` | Maven group/Java package prefix |
| Version | No | `1.0.0` | Semantic version string |
| Author | No | `Author` | Plugin author name |
| Description | No | *(empty)* | Human-readable plugin description |

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