Your First Plugin
Your First Plugin
Section titled “Your First Plugin”This tutorial walks you through creating a complete Hytale server plugin. 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)
- Java 25
- Hytale server for testing
Step 1: Project Setup
Section titled “Step 1: Project Setup”Start from the official plugin template, which uses the ScaffoldIt Gradle plugin:
git clone https://github.com/HytaleModding/plugin-template.git greeter-plugincd greeter-pluginUpdate settings.gradle.kts to configure your plugin identity:
hytale { usePatchline("release") useVersion("latest")
manifest { Group = "com.example" Name = "Greeter" Main = "com.example.greeter.GreeterPlugin" }}Rename the Java package under src/main/java/ to match, so your project looks like this:
Directorygreeter-plugin/
- build.gradle.kts
- settings.gradle.kts
- gradle.properties
- gradlew
Directorysrc/
Directorymain/
Directoryjava/
Directorycom/
Directoryexample/
Directorygreeter/
- GreeterPlugin.java
- GreeterConfig.java
Directorycommands/
- GreetCommand.java
Directoryresources/
- manifest.json
The ScaffoldIt plugin automatically adds the Hytale Maven repository, the server dependency (com.hypixel.hytale:Server:+), and configures the Java 25 toolchain. You do not need to configure these manually.
Step 2: Plugin Manifest
Section titled “Step 2: Plugin Manifest”Create src/main/resources/manifest.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 {
@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();
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.Message;import com.hypixel.hytale.server.core.command.system.CommandContext;import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;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 RequiredArg<PlayerRef> targetArg;
public GreetCommand(@Nonnull GreeterPlugin plugin) { super("greet", "Greet another player"); this.plugin = plugin;
this.targetArg = withRequiredArg("target", "The player to greet", ArgTypes.PLAYER_REF); }
@Override protected void execute( @Nonnull CommandContext context, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef playerRef, @Nonnull World world) {
PlayerRef target = context.get(targetArg);
if (target == null) { context.sendMessage(Message.raw("Player not found!")); return; }
if (target.getUuid().equals(playerRef.getUuid())) { context.sendMessage(Message.raw("You can't greet yourself!")); return; }
String message = String.format( plugin.getConfig().getGreetMessage(), playerRef.getUsername(), target.getUsername() );
context.sendMessage(Message.raw(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;
private final Config<GreeterConfig> config;
public static GreeterPlugin getInstance() { return instance; }
public GreeterPlugin(@Nonnull JavaPluginInit init) { super(init); this.config = withConfig(GreeterConfig.CODEC); }
@Override protected void setup() { instance = this;
getCommandRegistry().registerCommand(new GreetCommand(this));
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; }
@Nonnull public GreeterConfig getConfig() { return config.get(); }
private void onPlayerConnect(PlayerConnectEvent event) { if (!getConfig().isEnableWelcome()) { return; }
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”The plugin loads its configuration with these defaults:
{ "WelcomeMessage": "Welcome to the server, %s!", "GreetMessage": "%s says hello to %s!", "EnableWelcome": true}To customize messages, create or edit plugins/Greeter/config.json with the values above, 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().registerCommand(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:
- Reads from the config file if it exists, otherwise uses codec defaults
- 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().registerCommand(new AnotherCommand());Listen to More Events
Section titled “Listen to More Events”getEventRegistry().register(PlayerDisconnectEvent.class, this::onPlayerDisconnect);getEventRegistry().register(PlayerChatEvent.class, this::onPlayerChat);The EventPriority parameter is optional. When omitted, the priority defaults internally.
Add Permissions
Section titled “Add Permissions”Commands automatically use permission nodes based on the plugin’s base permission:
// (derived from group.name.command.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