Your First Plugin
Your First Plugin
Section titled “Your First Plugin”This tutorial walks you through creating a complete Hytale server plugin from scratch. By the end, you’ll have a working plugin that responds to player connections and provides a custom command.
What We’ll Build
Section titled “What We’ll Build”A “Greeter” plugin that:
- Welcomes players when they connect
- Provides a
/greetcommand to greet other players - Uses configuration for customizable messages
Prerequisites
Section titled “Prerequisites”- Development environment set up (see Setup Guide)
- Basic Java knowledge
- Hytale server for testing
Step 1: Project Setup
Section titled “Step 1: Project Setup”Create a new project with this structure:
greeter-plugin/├── build.gradle├── settings.gradle├── libs/│ └── HytaleServer.jar└── src/ └── main/ ├── java/ │ └── com/ │ └── example/ │ └── greeter/ │ ├── GreeterPlugin.java │ ├── GreeterConfig.java │ └── commands/ │ └── GreetCommand.java └── resources/ └── plugin.jsonbuild.gradle
Section titled “build.gradle”plugins { id 'java'}
group = 'com.example'version = '1.0.0'
java { toolchain { languageVersion = JavaLanguageVersion.of(21) }}
repositories { mavenCentral()}
dependencies { compileOnly files('libs/HytaleServer.jar')}
jar { from('src/main/resources') { include 'plugin.json' }}settings.gradle
Section titled “settings.gradle”rootProject.name = 'greeter-plugin'Step 2: Plugin Manifest
Section titled “Step 2: Plugin Manifest”Create src/main/resources/plugin.json:
{ "Group": "com.example", "Name": "Greeter", "Version": "1.0.0", "Description": "Welcomes players and provides greeting commands", "Main": "com.example.greeter.GreeterPlugin", "Authors": [ { "Name": "Your Name" } ]}Step 3: Configuration Class
Section titled “Step 3: Configuration Class”Create src/main/java/com/example/greeter/GreeterConfig.java:
package com.example.greeter;
import com.hypixel.hytale.codec.Codec;import com.hypixel.hytale.codec.KeyedCodec;import com.hypixel.hytale.codec.builder.BuilderCodec;import javax.annotation.Nonnull;
public class GreeterConfig {
// Define the codec for serialization/deserialization @Nonnull public static final BuilderCodec<GreeterConfig> CODEC = BuilderCodec .builder(GreeterConfig.class, GreeterConfig::new) .append( new KeyedCodec<>("WelcomeMessage", Codec.STRING), (config, value) -> config.welcomeMessage = value, config -> config.welcomeMessage ) .add() .append( new KeyedCodec<>("GreetMessage", Codec.STRING), (config, value) -> config.greetMessage = value, config -> config.greetMessage ) .add() .append( new KeyedCodec<>("EnableWelcome", Codec.BOOLEAN), (config, value) -> config.enableWelcome = value, config -> config.enableWelcome ) .add() .build();
// Configuration fields with defaults private String welcomeMessage = "Welcome to the server, %s!"; private String greetMessage = "%s says hello to %s!"; private boolean enableWelcome = true;
public String getWelcomeMessage() { return welcomeMessage; }
public String getGreetMessage() { return greetMessage; }
public boolean isEnableWelcome() { return enableWelcome; }}Step 4: Greet Command
Section titled “Step 4: Greet Command”Create src/main/java/com/example/greeter/commands/GreetCommand.java:
package com.example.greeter.commands;
import com.example.greeter.GreeterPlugin;import com.hypixel.hytale.component.Ref;import com.hypixel.hytale.component.Store;import com.hypixel.hytale.server.core.command.system.CommandContext;import com.hypixel.hytale.server.core.command.system.arguments.system.Argument;import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;import com.hypixel.hytale.server.core.universe.PlayerRef;import com.hypixel.hytale.server.core.universe.world.World;import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;import javax.annotation.Nonnull;
public class GreetCommand extends AbstractPlayerCommand {
private final GreeterPlugin plugin; private final Argument<?, PlayerRef> targetArg;
public GreetCommand(@Nonnull GreeterPlugin plugin) { super("greet", "Greet another player"); this.plugin = plugin;
// Add a required player argument using ArgTypes.PLAYER_REF this.targetArg = addArgument("target", ArgTypes.PLAYER_REF); }
@Override protected void execute( @Nonnull CommandContext context, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef playerRef, @Nonnull World world) {
// Get the target player from the argument PlayerRef target = context.get(targetArg);
if (target == null) { context.sendError("Player not found!"); return; }
if (target.getUuid().equals(playerRef.getUuid())) { context.sendError("You can't greet yourself!"); return; }
// Format and broadcast the greeting String message = String.format( plugin.getConfig().getGreetMessage(), playerRef.getUsername(), target.getUsername() );
// Send success message context.sendSuccess(message); }}Step 5: Main Plugin Class
Section titled “Step 5: Main Plugin Class”Create src/main/java/com/example/greeter/GreeterPlugin.java:
package com.example.greeter;
import com.example.greeter.commands.GreetCommand;import com.hypixel.hytale.event.EventPriority;import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent;import com.hypixel.hytale.server.core.plugin.JavaPlugin;import com.hypixel.hytale.server.core.plugin.JavaPluginInit;import com.hypixel.hytale.server.core.util.Config;import java.util.logging.Level;import javax.annotation.Nonnull;
public class GreeterPlugin extends JavaPlugin {
private static GreeterPlugin instance;
// Configuration holder private final Config<GreeterConfig> config;
public static GreeterPlugin getInstance() { return instance; }
public GreeterPlugin(@Nonnull JavaPluginInit init) { super(init); // Register config in constructor (before setup) this.config = withConfig(GreeterConfig.CODEC); }
@Override protected void setup() { instance = this;
// Register the greet command getCommandRegistry().register(new GreetCommand(this));
// Register player connect event listener getEventRegistry().register( EventPriority.NORMAL, PlayerConnectEvent.class, this::onPlayerConnect );
getLogger().at(Level.INFO).log("Greeter plugin setup complete!"); }
@Override protected void start() { getLogger().at(Level.INFO).log( "Greeter v%s started! Welcome messages: %s", getManifest().getVersion(), getConfig().isEnableWelcome() ? "enabled" : "disabled" ); }
@Override protected void shutdown() { getLogger().at(Level.INFO).log("Greeter shutting down..."); instance = null; }
/** * Get the plugin configuration. */ @Nonnull public GreeterConfig getConfig() { return config.get(); }
/** * Handle player connection events. */ private void onPlayerConnect(PlayerConnectEvent event) { if (!getConfig().isEnableWelcome()) { return; }
// Use getPlayerRef() instead of deprecated getPlayer() String welcomeMessage = String.format( getConfig().getWelcomeMessage(), event.getPlayerRef().getUsername() );
getLogger().at(Level.INFO).log(welcomeMessage); }}Step 6: Build and Test
Section titled “Step 6: Build and Test”Build the Plugin
Section titled “Build the Plugin”./gradlew buildThe compiled plugin JAR will be in build/libs/greeter-plugin-1.0.0.jar.
Install
Section titled “Install”- Copy
greeter-plugin-1.0.0.jarto your server’splugins/directory - Start or restart the server
Configuration File
Section titled “Configuration File”On first run, the plugin creates plugins/Greeter/config.json:
{ "WelcomeMessage": "Welcome to the server, %s!", "GreetMessage": "%s says hello to %s!", "EnableWelcome": true}Edit this file to customize messages, then restart the server.
Test the Plugin
Section titled “Test the Plugin”- Connect to the server - you should see a welcome log message
- Use
/greet <playername>to greet another player
Understanding the Code
Section titled “Understanding the Code”Plugin Lifecycle
Section titled “Plugin Lifecycle”- Constructor: Called when plugin is loaded. We register our config here.
- preLoad(): (inherited) Loads the config file asynchronously
- setup(): Register commands, events, and other handlers
- start(): Plugin is fully active, log startup message
- shutdown(): Clean up when plugin is disabled
Event Handling
Section titled “Event Handling”getEventRegistry().register( EventPriority.NORMAL, // When to handle (relative to other handlers) PlayerConnectEvent.class, // Event type to listen for this::onPlayerConnect // Method to call);Events are automatically unregistered when the plugin shuts down.
Command Registration
Section titled “Command Registration”getCommandRegistry().register(new GreetCommand(this));Commands extend base classes like AbstractPlayerCommand (in basecommands package) which handle:
- Permission checking
- Player-only restriction (ensures sender is a player in a world)
- Argument parsing via
ArgTypes - Tab completion
Configuration
Section titled “Configuration”this.config = withConfig(GreeterConfig.CODEC);The withConfig() method:
- Creates a config file if it doesn’t exist
- Loads existing config data
- Provides type-safe access via
config.get()
Extending the Plugin
Section titled “Extending the Plugin”Add More Commands
Section titled “Add More Commands”Create additional command classes and register them in setup():
getCommandRegistry().register(new AnotherCommand());Listen to More Events
Section titled “Listen to More Events”getEventRegistry().register(PlayerDisconnectEvent.class, this::onPlayerDisconnect);getEventRegistry().register(PlayerChatEvent.class, this::onPlayerChat);Add Permissions
Section titled “Add Permissions”Commands automatically use permission nodes based on the plugin’s base permission:
// (derived from group.name.commandname)Common Patterns
Section titled “Common Patterns”Singleton Access
Section titled “Singleton Access”private static GreeterPlugin instance;
public static GreeterPlugin getInstance() { return instance;}This allows other classes to access the plugin instance.
Null Safety
Section titled “Null Safety”Always use @Nonnull and @Nullable annotations:
import javax.annotation.Nonnull;import javax.annotation.Nullable;
@Nonnullpublic GreeterConfig getConfig() { return config.get();}Logging
Section titled “Logging”Use the plugin’s logger for consistent output:
getLogger().at(Level.INFO).log("Message: %s", value);getLogger().at(Level.WARNING).log("Warning!");getLogger().at(Level.SEVERE).withCause(exception).log("Error occurred");Next Steps
Section titled “Next Steps”- Event System - Deep dive into events
- Command System - Advanced command features
- ECS Overview - Entity Component System
- Built-in Events - All available events