Skip to content

Commands

Commands are the primary way players and administrators interact with your server plugins. This guide covers everything you need to know to create powerful, type-safe commands.

Hytale’s command system provides a robust architecture for creating custom commands with automatic argument parsing, permission checking, and async execution support.

BaseCommand (Abstract)
├── AbstractCommand → Basic commands (any sender)
└── AbstractAsyncCommand
└── AbstractPlayerCommand → Player-only commands

AbstractCommand - Use for commands that:

  • Can be executed from console or command blocks
  • Don’t specifically need player context
  • Are administrative or utility commands

AbstractPlayerCommand - Use for commands that:

  • Must be executed by a player
  • Need access to player components via ECS
  • Require player position, inventory, or other player-specific data

Let’s create a simple greeting command that works for players:

package com.yourplugin.commands;
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.basecommands.AbstractPlayerCommand;
import com.hypixel.hytale.server.core.entity.entities.Player;
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 HelloCommand extends AbstractPlayerCommand {
public HelloCommand() {
super("hello", "Sends a greeting message");
}
@Override
protected void execute(@Nonnull CommandContext commandContext,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world) {
// Get the player component
Player player = store.getComponent(ref, Player.getComponentType());
// Send greeting
player.sendMessage(Message.raw("Hello, " + player.getDisplayName() + "!"));
}
}

In your plugin’s main class:

@Override
public void setup() {
this.getCommandRegistry().registerCommand(new HelloCommand());
getLogger().atInfo().log("Commands registered!");
}

For commands that should work from both console and in-game:

public class ReloadCommand extends AbstractCommand {
public ReloadCommand() {
super("reload", "Reloads the plugin configuration");
}
@Override
public CompletableFuture<Void> execute(@Nonnull CommandContext ctx) {
// Get sender
CommandSender sender = ctx.sender();
// Reload logic here
ctx.sendMessage(Message.raw("Configuration reloaded!"));
return CompletableFuture.completedFuture(null);
}
}
FeatureAbstractCommandAbstractPlayerCommand
Sender typeAny (Player, Console, etc.)Player only
Return typeCompletableFuture<Void>void
ThreadingMain threadAsync (off main thread)
Player checkingManualAutomatic

Arguments allow your commands to accept parameters from users with automatic type validation.

public class TeleportCommand extends AbstractPlayerCommand {
// Define arguments as fields
RequiredArg<String> playerArg = this.withRequiredArg(
"player", // Name
"Player to teleport to", // Description
ArgTypes.STRING // Type
);
OptionalArg<Integer> delayArg = this.withOptionalArg(
"delay",
"Delay in seconds (default: 0)",
ArgTypes.INTEGER
);
public TeleportCommand() {
super("tp", "Teleport to another player");
}
@Override
protected void execute(@Nonnull CommandContext ctx, ...) {
// Get argument values
String targetName = playerArg.get(ctx);
Integer delay = delayArg.get(ctx);
// Use default if optional argument not provided
if (delay == null) delay = 0;
// Implementation...
}
}
ArgTypeJava TypeExample Values
ArgTypes.STRINGString"hello", "world"
ArgTypes.INTEGERInteger1, 42, -10
ArgTypes.BOOLEANBooleantrue, false
ArgTypes.FLOATFloat1.5, 3.14
ArgTypes.DOUBLEDouble1.5, 3.14159
ArgTypes.UUIDUUID550e8400-e29b-...

Always check for null when using optional arguments:

Integer amount = optionalArg.get(ctx);
// Option 1: Provide default
if (amount == null) amount = 1;
// Option 2: Check before using
if (amount != null && amount > 10) {
// Use amount
}

Create organized, hierarchical command structures for complex features:

// Main command
public class EconomyCommand extends AbstractPlayerCommand {
public EconomyCommand() {
super("economy", "Economy management commands");
// Register sub-commands
addSubCommand(new EconomyGiveCommand());
addSubCommand(new EconomyTakeCommand());
addSubCommand(new EconomyBalanceCommand());
}
@Override
protected void execute(...) {
// Shows help when /economy is used alone
Player player = store.getComponent(ref, Player.getComponentType());
player.sendMessage(Message.raw("Available commands:"));
player.sendMessage(Message.raw("- /economy give <player> <amount>"));
player.sendMessage(Message.raw("- /economy take <player> <amount>"));
player.sendMessage(Message.raw("- /economy balance"));
}
}
// Sub-command
public class EconomyGiveCommand extends AbstractPlayerCommand {
RequiredArg<String> playerArg = this.withRequiredArg("player", "Target player", ArgTypes.STRING);
RequiredArg<Double> amountArg = this.withRequiredArg("amount", "Amount", ArgTypes.DOUBLE);
public EconomyGiveCommand() {
super("give", "Give money to a player");
}
@Override
protected void execute(...) {
String target = playerArg.get(commandContext);
double amount = amountArg.get(commandContext);
// Implementation...
}
}

AbstractPlayerCommand extends AbstractAsyncCommand, meaning it executes off the main server thread. This is important for thread safety:

@Override
protected void execute(...) {
// ❌ Wrong - Unsafe on async thread!
player.teleport(location);
// ✅ Correct - Schedule on main thread
server.getScheduler().runTask(() -> {
player.teleport(location);
});
}

When using AbstractPlayerCommand:

  • Reading data is usually safe
  • Modifying game state requires main thread
  • Use the scheduler for game-modifying operations

The Entity Component System (ECS) provides access to player data:

@Override
protected void execute(@Nonnull CommandContext commandContext,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world) {
// Get player component
Player player = store.getComponent(ref, Player.getComponentType());
// Get transform (position/rotation)
TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType());
// Get inventory
InventoryComponent inventory = store.getComponent(ref, InventoryComponent.getComponentType());
// Get UUID
UUIDComponent uuid = store.getComponent(ref, UUIDComponent.getComponentType());
}

Format and send messages to players:

// Simple text
player.sendMessage(Message.raw("Hello, world!"));
// Colored text
player.sendMessage(Message.raw("Success!").color("#00FF00"));
// Combined messages
Message combined = Message.join(
Message.raw("Welcome, ").color("#FFFFFF"),
Message.raw(player.getDisplayName()).color("#FFD700"),
Message.raw("!").color("#FFFFFF")
);
player.sendMessage(combined);

Check permissions before executing sensitive operations:

@Override
protected void execute(...) {
Player player = store.getComponent(ref, Player.getComponentType());
if (!player.hasPermission("yourplugin.command.use")) {
player.sendMessage(Message.raw("You don't have permission!"));
return;
}
// Command logic...
}

Here’s a production-ready command with all best practices:

public class WarpCommand extends AbstractPlayerCommand {
private final WarpManager warpManager;
RequiredArg<String> warpArg = this.withRequiredArg("warp", "Warp name", ArgTypes.STRING);
public WarpCommand(WarpManager warpManager) {
super("warp", "Teleport to a warp point");
this.warpManager = warpManager;
}
@Override
protected void execute(@Nonnull CommandContext commandContext,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world) {
Player player = store.getComponent(ref, Player.getComponentType());
String warpName = warpArg.get(commandContext);
// Permission check
if (!player.hasPermission("warps.use." + warpName.toLowerCase())) {
player.sendMessage(Message.raw("You don't have access to this warp!").color("#FF0000"));
return;
}
// Get warp
Warp warp = warpManager.getWarp(warpName);
if (warp == null) {
player.sendMessage(Message.raw("Warp '" + warpName + "' not found!").color("#FF0000"));
return;
}
// Get transform for teleport
TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType());
// Schedule teleport on main thread
commandContext.source().getServer().getScheduler().runTask(() -> {
transform.setPosition(warp.getLocation());
player.sendMessage(Message.raw("Teleported to " + warpName + "!").color("#00FF00"));
});
}
}

Problem: /mycommand says “Unknown command”

Solution: Verify registration in your plugin’s setup() method:

@Override
public void setup() {
this.getCommandRegistry().registerCommand(new MyCommand());
getLogger().atInfo().log("Commands registered!");
}

NullPointerException with Optional Arguments

Section titled “NullPointerException with Optional Arguments”

Problem: Getting NPE when accessing optional argument

Solution: Always check for null:

Integer amount = optionalArg.get(ctx);
if (amount == null) amount = 1; // Provide default

Problem: Sub-command doesn’t execute

Solution: Only register the main command, not sub-commands:

// ✅ Correct
registry.registerCommand(new MainCommand());
// ❌ Wrong - don't register sub-commands separately
registry.registerCommand(new SubCommand());
  1. Always validate input - Check permissions and arguments before execution
  2. Use appropriate command type - AbstractCommand for console support, AbstractPlayerCommand for player-only
  3. Handle thread safety - Schedule game-modifying operations on main thread when using AbstractPlayerCommand
  4. Provide clear feedback - Send helpful messages for success and error cases
  5. Use sub-commands - Organize related commands hierarchically
  6. Check for null - Always handle null values from optional arguments

Now that you understand commands, explore:

  • Creating custom events to respond to game actions
  • Building user interfaces for complex interactions
  • Managing persistent data with databases
  • Implementing cooldowns and rate limiting