Mastering the Command API in Hytale

tutorial java plugin commands api

If you’ve worked with Minecraft plugins, you’re used to commands like /heal or /give diamond. In Hytale, commands work similarly on the surface, but underneath lies a sophisticated system with built-in argument parsing, permission checking, tab-completion, and automatic threading management. Let’s explore how it all works.

Prefer video? Watch this tutorial on YouTube.

What You’ll Learn

  1. Command basics — What a command is and how to build your first one
  2. Base classes — Which base class to choose and why it matters for threading
  3. Arguments — Required, Optional, Flags, and the ArgType system
  4. Permissions & targeting — Controlling access and targeting entities via raycast
  5. Advanced patterns — Sub-commands, variants, and real-world patterns

We’ll build a Heal Command as our running example — starting simple and progressively adding features like arguments, permissions, and player targeting.

What is a Command?

At its core, a command is simply:

  1. A name — what the player types (/heal, /hello, /give)
  2. An execute method — what happens when they type it

When a player types /give Steve diamond_sword --count 3, the command system parses this into:

  • Command: "give"
  • Arguments: player=Steve, item=diamond_sword, count=3
  • Logic: Add 3 diamond swords to Steve’s inventory
flowchart TB
    subgraph Command["A Command"]
        N["Name: heal"]
        D["Description: Heal a player"]
        A["Aliases: [h]"]
        subgraph Args["Arguments"]
            A1["--amount (optional)"]
            A2["--player (optional)"]
        end
        P["Permissions: [hytale.command.heal]"]
        E["execute(): The actual logic"]
    end

Your First Command: HelloCommand

Let’s start with the simplest possible command — one that just sends a message back to the player:

public class HelloCommand extends AbstractAsyncCommand {

    public HelloCommand() {
        super("hello", "Says hello to the player");
    }

    @Override
    protected CompletableFuture<Void> executeAsync(CommandContext context) {
        context.sendMessage(Message.raw("Hello, World!"));
        return CompletableFuture.completedFuture(null);
    }
}

Every command extends a base class and implements an execute method. Let’s break this down:

AbstractAsyncCommand is the simplest base class. The “Async” part means your code runs on a background thread, separate from the game worlds. This is fine for commands that just send messages or do calculations — anything that doesn’t need to touch game data directly.

The constructor takes the command name (what players type after the slash: /hello) and a description (shown in /help and tab-completion).

CommandContext contains everything about this specific command call: who ran it (player or console), the raw input string, and methods to send messages back.

Message.raw() is Hytale’s way to create text messages. For simple strings, Message.raw("text") is all you need. There are fancier options for formatting and colors, but raw() covers most cases.

CompletableFuture<Void> is Java’s way of saying “this task will complete eventually, and it doesn’t return anything.” For now, just always return CompletableFuture.completedFuture(null) at the end of your method.

Registering Commands

A command doesn’t exist until you register it. In your plugin’s setup() method:

public class MyPlugin extends JavaPlugin {

    public MyPlugin(@Nonnull JavaPluginInit init) {
        super(init);
    }

    @Override
    protected void setup() {
        // Commands don't need assets — register them here
        getCommandRegistry().registerCommand(new HelloCommand());
    }

    @Override
    protected void start() {
        // For things that need loaded assets (models, textures, etc.)
    }
}

The plugin lifecycle has two phases:

PhaseWhenWhat to do
setup()Before assets loadRegister commands, events, components
start()After assets loadUse assets, load data, start tasks

With registration handled, let’s make our command actually do something useful.

The Problem: Accessing Game Data

HelloCommand works, but it’s not very useful. What if we want to actually heal the player? You might try:

player.setHealth(100);  // This doesn't exist in Hytale!

In Hytale, there’s no player object with a setHealth() method. Player data lives in the ECS (Entity Component System). To change a player’s health, you need their Health component from the Store. But AbstractAsyncCommand doesn’t give you Store access.

This is where different base classes come in.

Base Classes: The Heart of the System

Hytale provides several base classes, each giving you different levels of access:

flowchart TD
    AC[AbstractCommand] --> AAC[AbstractAsyncCommand]
    AAC --> ACC[AbstractCommandCollection]
    AAC --> AWC[AbstractWorldCommand]
    AAC --> APC[AbstractPlayerCommand]
    AAC --> ATPC[AbstractTargetPlayerCommand]
    AAC --> ATEC[AbstractTargetEntityCommand]

    ACC -.- ACC_L["Groups sub-commands"]
    AWC -.- AWC_L["World operations"]
    APC -.- APC_L["Modify YOURSELF"]
    ATPC -.- ATPC_L["Modify A PLAYER"]
    ATEC -.- ATEC_L["Modify ENTITIES"]

Here’s when to use each:

Base ClassWhat you getExample use
AbstractAsyncCommandJust context/help, /who, /ping
AbstractPlayerCommandStore + your Ref/heal (self), /fly
AbstractTargetPlayerCommandStore + source + target Ref/heal Steve, /kick
AbstractTargetEntityCommandStore + entity list/kill —radius 10
AbstractWorldCommandWorld access/time, /weather
AbstractCommandCollectionSub-commands/player stats

Why Different Base Classes?

Remember from the threading tutorial: each World in Hytale runs on its own thread. You can only safely access a Store from its World’s thread. The base classes handle this threading for you.

When you extend AbstractPlayerCommand, your execute() method runs on the correct World thread automatically. The Store and Ref you receive are safe to use — no need to think about threading.

sequenceDiagram
    participant P as Player
    participant C as Command System
    participant W as World Thread

    P->>C: Types /heal
    C->>W: Schedules execute()
    W->>W: Runs on correct thread
    W->>W: Safe to access Store
    W->>P: Sends response

Now that you understand why these base classes exist, here’s how to choose the right one for your command:

Decision Tree

flowchart TD
    Q1{Store access?}
    Q1 -->|NO| AAC[AbstractAsyncCommand]
    Q1 -->|YES| Q2{Who do you modify?}

    Q2 -->|Yourself| APC[AbstractPlayerCommand]
    Q2 -->|A Player| ATPC[AbstractTargetPlayerCommand]
    Q2 -->|Entities| ATEC[AbstractTargetEntityCommand]
    Q2 -->|World only| AWC[AbstractWorldCommand]

Building HealCommand

With the decision tree in mind, let’s build a command that actually heals the player. We need Store access to modify health, so we’ll use AbstractPlayerCommand:

public class HealCommand extends AbstractPlayerCommand {

    public HealCommand() {
        super("heal", "Heals yourself to full health");
    }

    @Override
    protected void execute(CommandContext context,
                           Store<EntityStore> store,
                           Ref<EntityStore> ref,
                           PlayerRef playerRef,
                           World world) {

        // Get the EntityStatMap component (holds all stats including health)
        EntityStatMap stats = store.getComponent(ref, EntityStatMap.getComponentType());
        if (stats == null) {
            playerRef.sendMessage(Message.raw("Could not find stats!"));
            return;
        }

        // Get the health stat and heal to max
        int healthIdx = DefaultEntityStatTypes.getHealth();
        EntityStatValue health = stats.get(healthIdx);
        if (health == null) {
            playerRef.sendMessage(Message.raw("Could not find health stat!"));
            return;
        }

        float missing = health.getMax() - health.get();
        stats.addStatValue(healthIdx, missing);

        // Send feedback
        playerRef.sendMessage(Message.raw("Healed to full health!"));
    }
}

Notice the different method signature. Instead of executeAsync(CommandContext context), we get:

  • CommandContext context — Who ran the command, raw input
  • Store<EntityStore> store — The ECS Store, safe to use
  • Ref<EntityStore> ref — YOUR entity in the ECS
  • PlayerRef playerRef — For sending messages, getting name
  • World world — The world you’re in

The base class handles getting the Store and Ref, and ensures your code runs on the correct thread. Note that stats.get() can return null if the stat doesn’t exist, so always check.

The Argument System

Our basic heal command works, but it always heals to full. What if we want to specify an amount? That’s what arguments are for.

Three Types of Arguments

TypeSyntaxExample
RequiredArgpositional/give diamond
OptionalArg--name value/heal --amount 50
FlagArg--flag/heal --silent

Arguments are defined in your constructor:

public class HealCommand extends AbstractPlayerCommand {

    private final OptionalArg<Integer> amountArg;

    public HealCommand() {
        super("heal", "Heals yourself");

        this.amountArg = withOptionalArg("amount", "HP to restore", ArgTypes.INTEGER)
            .addAliases("hp")
            .addValidator(Validators.greaterThan(0));
    }

    @Override
    protected void execute(CommandContext context, ...) {
        // Read argument value
        Integer amount = amountArg.get(context);  // null if not provided

        if (amount == null) {
            // Heal to max
            amount = (int) (health.getMax() - health.get());
        }

        stats.addStatValue(healthIdx, amount);
    }
}

ArgTypes

ArgTypes convert strings to Java types. When a player types /heal --amount 50, ArgTypes.INTEGER parses "50" into the integer 50.

ArgTypeParses toExample input
ArgTypes.STRINGString"hello"
ArgTypes.INTEGERInteger42
ArgTypes.FLOATFloat3.14
ArgTypes.BOOLEANBooleantrue
ArgTypes.PLAYER_REFPlayerRefSteve
ArgTypes.RELATIVE_POSITIONRelativeDoublePosition~ ~10 ~
ArgTypes.ITEM_ASSETItemdiamond_sword
ArgTypes.BLOCK_TYPE_ASSETBlockTypestone

ArgTypes also provide:

  • Tab-completion — Type /heal --player <TAB> and you’ll see all online player names
  • Fuzzy matching — Type “St” and it matches “Steve”
  • Validation — Player not found? Error message shown automatically

Validators

Validators check argument values before your execute method runs:

this.amountArg = withOptionalArg("amount", "Heal amount", ArgTypes.INTEGER)
    .addValidator(Validators.greaterThan(0))
    .addValidator(Validators.max(1000));
/heal --amount 50     → OK, execute() runs
/heal --amount -100   → Error: "Value must be greater than 0"
/heal --amount 9999   → Error: "Value must be at most 1000"

Available validators include:

ValidatorWhat it does
Validators.greaterThan(value)Must be > value
Validators.lessThan(value)Must be < value
Validators.max(value)Must be <= value
Validators.range(min, max)Must be in range
Validators.nonEmptyString()String can’t be blank
Validators.or(v1, v2, ...)Any validator passes

For complex validation logic, you can write custom validators. Note that validators use a BiConsumer pattern — they don’t return a result, they call methods on the ValidationResults object:

this.amountArg = withOptionalArg("amount", "Heal amount", ArgTypes.INTEGER)
    .addValidator((value, results) -> {
        if (value != null && value <= 0) {
            results.fail("Amount must be positive");
        }
        // No return - void method
    });

Note: Stat values like health are automatically clamped between min/max by the game. You can’t overheal or go below zero. But validators catch bad input before any processing — useful for giving clear error messages.

Positional vs Named Arguments

Hytale supports two ways to pass arguments:

TypeSyntaxExample
PositionalJust the value/tp Steve
Named--name value/heal --player Steve
// Positional (RequiredArg) — parsed left to right, no -- needed
this.playerArg = withRequiredArg("player", "Target player", ArgTypes.PLAYER_REF);

// Named (OptionalArg) — requires --player prefix
this.playerArg = withOptionalArg("player", "Target player", ArgTypes.PLAYER_REF);

Here’s the trade-off:

Named (--player)Positional
Syntax/heal --player Steve/heal Steve
Base classAbstractTargetPlayerCommandAbstractAsyncCommand
ThreadingHandled automaticallyMust handle yourself
Target’s RefGiven to youMust fetch manually
FlexibilityArgs in any orderOrder matters

If you use positional arguments with AbstractAsyncCommand, you must handle threading yourself:

protected CompletableFuture<Void> executeAsync(CommandContext ctx) {
    PlayerRef target = this.targetArg.get(ctx);
    World targetWorld = target.getWorld();

    // Must explicitly run on target's thread!
    return targetWorld.execute(() -> {
        Ref<EntityStore> ref = target.getReference();
        Store<EntityStore> store = ref.getStore();
        store.getComponent(ref, ...);  // Now safe
    });
}

Recommendation: Use AbstractTargetPlayerCommand for most cases. The --player syntax is explicit and the base class handles all the complexity.

With arguments and validators, you can build commands that accept any input you need. But what about affecting other players?

Targeting Other Players

What if we want /heal Steve to heal another player? That’s where AbstractTargetPlayerCommand comes in.

Click to show complete HealPlayerCommand.java
public class HealPlayerCommand extends AbstractTargetPlayerCommand {

    public HealPlayerCommand() {
        super("healplayer", "Heals another player");
    }

    @Override
    protected void execute(CommandContext context,
                           @Nullable Ref<EntityStore> sourceRef,  // Who ran it (null if console)
                           Ref<EntityStore> targetRef,             // Target's entity
                           PlayerRef targetPlayer,                 // Target player
                           World world,
                           Store<EntityStore> store) {

        // Note: sourceRef can be null if run from console!

        EntityStatMap stats = store.getComponent(targetRef, EntityStatMap.getComponentType());
        if (stats == null) {
            context.sendMessage(Message.raw("Target has no stats!"));
            return;
        }

        int healthIdx = DefaultEntityStatTypes.getHealth();
        EntityStatValue health = stats.get(healthIdx);
        if (health == null) {
            context.sendMessage(Message.raw("Target has no health stat!"));
            return;
        }

        stats.addStatValue(healthIdx, health.getMax() - health.get());

        // Notify the target
        targetPlayer.sendMessage(Message.raw("You have been healed!"));
    }
}

Notice the signature has 6 parameters:

  • context — Command context
  • sourceRef — The entity of whoever ran the command (can be null if console!)
  • targetRef — The target player’s entity
  • targetPlayer — The target PlayerRef
  • world — The world
  • store — The Store

AbstractTargetPlayerCommand automatically adds a --player argument. Usage: /healplayer --player Steve

The base class handles:

  • Finding the player by name
  • Getting their Ref
  • Running your code on their World’s thread
  • Automatic .other permission suffix when targeting others

Raycast Targeting

What if you want to heal whatever you’re looking at? AbstractTargetEntityCommand uses a raycast — an invisible line from your eyes in the direction you’re looking:

flowchart LR
    P[Player] -->|raycast| E[Entity]
    P -->|"looks at"| E
    E -->|"becomes target"| T[Target Ref List]
Click to show complete HealEntityCommand.java
public class HealEntityCommand extends AbstractTargetEntityCommand {

    public HealEntityCommand() {
        super("healentity", "Heals entities you're looking at");
    }

    @Override
    protected void execute(CommandContext context,
                           ObjectList<Ref<EntityStore>> entities,  // List of entities!
                           World world,
                           Store<EntityStore> store) {

        int healed = 0;

        for (Ref<EntityStore> ref : entities) {
            EntityStatMap stats = store.getComponent(ref, EntityStatMap.getComponentType());
            if (stats == null) continue;

            int healthIdx = DefaultEntityStatTypes.getHealth();
            EntityStatValue health = stats.get(healthIdx);
            if (health == null) continue;

            stats.addStatValue(healthIdx, health.getMax() - health.get());
            healed++;
        }

        context.sendMessage(Message.raw("Healed " + healed + " entities!"));
    }
}

Notice that AbstractTargetEntityCommand gives you an ObjectList<Ref<EntityStore>> — a list of entities, not a single one! This is because commands like /kill --radius 10 can target multiple entities at once.

If no entity is in the crosshair and no targeting arguments are provided, the base class automatically shows “No entity found” — you only write the success logic.

Permissions

So far, anyone could use our commands. But on a real server, you’ll want to control who can run them. Permissions handle this:

public class HealCommand extends AbstractPlayerCommand {

    public HealCommand() {
        super("heal", "Heals yourself");

        // Only players with this permission can use /heal
        requirePermission(HytalePermissions.fromCommand("heal"));
    }
}

If a player without the permission tries /heal, they get an error message automatically. You don’t need to check manually.

Permission Naming

Hytale uses hierarchical permission names. The HytalePermissions.fromCommand() method adds the hytale.command. prefix:

// Simple permission
requirePermission(HytalePermissions.fromCommand("heal"));
// → Permission: "hytale.command.heal"

// Sub-permissions
requirePermission(HytalePermissions.fromCommand("heal.self"));
requirePermission(HytalePermissions.fromCommand("heal.other"));
// → "hytale.command.heal.self", "hytale.command.heal.other"

For AbstractTargetPlayerCommand, permissions are automatic: heal for healing yourself, heal.other for healing others.

Sub-Commands

As your plugin grows, you’ll want to organize related commands. Instead of having /worldcreate, /worlddelete, and /worldlist as separate commands, you can group them under /world using AbstractCommandCollection:

Click to show complete WorldCommand.java
public class WorldCommand extends AbstractCommandCollection {

    public WorldCommand() {
        super("world", "World management commands");

        addSubCommand(new WorldListCommand());
        addSubCommand(new WorldCreateCommand());
    }

    private static class WorldListCommand extends AbstractAsyncCommand {
        WorldListCommand() {
            super("list", "Lists all worlds");
        }

        @Override
        protected CompletableFuture<Void> executeAsync(CommandContext ctx) {
            ctx.sendMessage(Message.raw("Worlds: Overworld, Nether, End"));
            return CompletableFuture.completedFuture(null);
        }
    }

    private static class WorldCreateCommand extends AbstractAsyncCommand {
        private final RequiredArg<String> nameArg;

        WorldCreateCommand() {
            super("create", "Creates a new world");
            this.nameArg = withRequiredArg("name", "World name", ArgTypes.STRING);
        }

        @Override
        protected CompletableFuture<Void> executeAsync(CommandContext ctx) {
            String name = nameArg.get(ctx);
            ctx.sendMessage(Message.raw("Created world: " + name));
            return CompletableFuture.completedFuture(null);
        }
    }
}

Usage:

/world list           → "Worlds: Overworld, Nether, End"
/world create MyWorld → "Created world: MyWorld"
/world                → Shows available sub-commands

You can nest collections inside collections for deep hierarchies like /admin user ban.

Command Variants

Sub-commands organize different operations under one parent. But what if you want the same operation to accept different argument patterns? What if you want /give diamond to give to yourself, but /give Steve diamond to give to someone else? Same command name, different argument patterns. That’s a variant.

Sub-CommandsVariants
Syntax/world create/give item or /give player item
PurposeDifferent actionsSame action, different targets
ClassAbstractCommandCollectionaddUsageVariant()
DispatchBy sub-command nameBy required arg count

Rule of thumb: Different verbs = sub-commands. Same verb, different targets = variants.

Click to show complete GiveCommand.java with variant
public class GiveCommand extends AbstractPlayerCommand {
    private final RequiredArg<String> itemArg;  // 1 required arg

    public GiveCommand() {
        super("give", "Gives item to yourself");
        this.itemArg = withRequiredArg("item", "Item", ArgTypes.STRING);
        addUsageVariant(new GiveOtherVariant());
    }

    @Override
    protected void execute(CommandContext ctx, Store<EntityStore> store,
                           Ref<EntityStore> ref, PlayerRef playerRef, World world) {
        String item = itemArg.get(ctx);
        // Give to self logic...
        playerRef.sendMessage(Message.raw("Gave yourself: " + item));
    }

    // Variant with 2 required args — uses description-only constructor!
    private static class GiveOtherVariant extends AbstractAsyncCommand {
        private final RequiredArg<PlayerRef> playerArg;
        private final RequiredArg<String> itemArg;

        GiveOtherVariant() {
            super("Gives item to another player");  // No name for variants!
            this.playerArg = withRequiredArg("player", "Target", ArgTypes.PLAYER_REF);
            this.itemArg = withRequiredArg("item", "Item", ArgTypes.STRING);
        }

        @Override
        protected CompletableFuture<Void> executeAsync(CommandContext ctx) {
            PlayerRef target = playerArg.get(ctx);
            String item = itemArg.get(ctx);
            target.sendMessage(Message.raw("You received: " + item));
            return CompletableFuture.completedFuture(null);
        }
    }
}

Hytale dispatches to variants based on number of required arguments:

  • /give diamond → 1 arg → Main command → Gives to yourself
  • /give Steve diamond → 2 args → Variant → Gives to Steve

Critical rules for variants:

  1. Each variant MUST have a different number of required arguments. Two variants with the same count will cause an error!

  2. Variants use a description-only constructor — no command name: super("Description here")

  3. Do NOT use AbstractTargetPlayerCommand for variants! It adds --player as an optional argument, not required. This means both your main command and variant would have the same required arg count → conflict → crash. Use AbstractAsyncCommand with manual RequiredArg instead.

Advanced Patterns

You now have all the fundamentals: base classes, arguments, permissions, sub-commands, and variants. Here are some additional patterns that make your commands more robust:

Confirmation for Dangerous Commands

Hytale has built-in confirmation for destructive commands:

public class DeleteWorldCommand extends AbstractAsyncCommand {

    public DeleteWorldCommand() {
        super("delete", "Deletes a world permanently", true);  // requiresConfirmation!
    }
}
/world delete MyWorld
→ "This command requires confirmation. Add --confirm to proceed."

/world delete MyWorld --confirm
→ World deleted!

DefaultArg with Static Value

For arguments with a default value, use DefaultArg. Note that defaults must be static values, not computed:

private final DefaultArg<Integer> countArg;

public MyCommand() {
    this.countArg = withDefaultArg(
        "count",
        "Number of items",
        ArgTypes.INTEGER,
        1,          // Static default value
        "1"         // Description shown in help
    );
}

@Override
protected void execute(...) {
    // get() always returns a value, never null
    int count = countArg.get(context);
}

AbstractWorldCommand

For commands that affect the world itself (not specific entities), use AbstractWorldCommand:

public class TimeCommand extends AbstractWorldCommand {

    private final RequiredArg<Integer> timeArg;

    public TimeCommand() {
        super("time", "Sets the world time");
        this.timeArg = withRequiredArg("time", "Time in ticks", ArgTypes.INTEGER);
    }

    @Override
    protected void execute(CommandContext context, World world, Store<EntityStore> store) {
        int time = timeArg.get(context);
        world.setTimeOfDay(time);
        context.sendMessage(Message.raw("Time set to " + time));
    }
}

Fuzzy Matching

Hytale automatically suggests corrections for typos:

Player types: /hael

Server responds: "Unknown command 'hael'. Did you mean 'heal'?"

This is built-in — you don’t need to do anything.

Common Patterns Reference

PatternHow
Heal playerEntityStatMap.addStatValue(healthIdx, amount)
Kill playerDeathComponent.tryAddComponent(store, ref, damage)
Give itemInventoryComponent.addItem(item, count)
TeleportTeleport.addComponent(store, ref, position)
Send messageplayerRef.sendMessage(Message.raw("text"))
Get positionTransformComponent.getPosition()

Common Mistakes

MistakeProblemFix
Command not foundForgot to registerCall registerCommand() in setup()
NullPointerExceptionOptional arg or component nullCheck with != null before using
Wrong player affectedConfused sourceRef vs targetRefIn TargetPlayerCommand: sourceRef = who ran it, targetRef = who’s affected
Threading crashAccessing Store from wrong threadUse the right base class
No tab-completionWrong ArgTypeVerify ArgType name
Blocking in asyncUsing .get() or .join() on futuresUse .thenAccept() or proper async patterns
Variant crashSame required arg countEach variant needs different number of required args

Key Takeaways

Choosing the Right Base Class

Base ClassYou GetUse For
AbstractAsyncCommandCommandContextMessages, info
AbstractPlayerCommandStore, Ref (self)Modify yourself
AbstractTargetPlayerCommandStore, sourceRef, targetRefModify other player
AbstractTargetEntityCommandStore, entity listModify any entity
AbstractWorldCommandWorldWorld operations
AbstractCommandCollectionSub-commandsGrouping commands

The Pattern

  1. Choose base class based on what you need access to
  2. Define arguments in constructor with withXxxArg()
  3. Implement execute() — the base class ensures correct threading
  4. Set permissions with requirePermission()
  5. Register in Plugin.setup()

Quick Reference

ConceptPurpose
withRequiredArg()Positional argument, must be provided
withOptionalArg()Named argument, can be omitted
withDefaultArg()Optional with static default value
withFlagArg()Boolean switch (--silent)
ArgTypes.XXXParse strings to Java types
arg.get(context)Read argument value
.addValidator()Validate before execution
requirePermission()Control access
addSubCommand()Create command hierarchy
addUsageVariant()Multiple signatures, same name

Happy modding!