Mastering the Command API in Hytale
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
- Command basics — What a command is and how to build your first one
- Base classes — Which base class to choose and why it matters for threading
- Arguments — Required, Optional, Flags, and the ArgType system
- Permissions & targeting — Controlling access and targeting entities via raycast
- 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:
- A name — what the player types (
/heal,/hello,/give) - 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:
| Phase | When | What to do |
|---|---|---|
setup() | Before assets load | Register commands, events, components |
start() | After assets load | Use 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 Class | What you get | Example use |
|---|---|---|
AbstractAsyncCommand | Just context | /help, /who, /ping |
AbstractPlayerCommand | Store + your Ref | /heal (self), /fly |
AbstractTargetPlayerCommand | Store + source + target Ref | /heal Steve, /kick |
AbstractTargetEntityCommand | Store + entity list | /kill —radius 10 |
AbstractWorldCommand | World access | /time, /weather |
AbstractCommandCollection | Sub-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 inputStore<EntityStore> store— The ECS Store, safe to useRef<EntityStore> ref— YOUR entity in the ECSPlayerRef playerRef— For sending messages, getting nameWorld 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
| Type | Syntax | Example |
|---|---|---|
| RequiredArg | positional | /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.
| ArgType | Parses to | Example input |
|---|---|---|
ArgTypes.STRING | String | "hello" |
ArgTypes.INTEGER | Integer | 42 |
ArgTypes.FLOAT | Float | 3.14 |
ArgTypes.BOOLEAN | Boolean | true |
ArgTypes.PLAYER_REF | PlayerRef | Steve |
ArgTypes.RELATIVE_POSITION | RelativeDoublePosition | ~ ~10 ~ |
ArgTypes.ITEM_ASSET | Item | diamond_sword |
ArgTypes.BLOCK_TYPE_ASSET | BlockType | stone |
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:
| Validator | What 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:
| Type | Syntax | Example |
|---|---|---|
| Positional | Just 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 class | AbstractTargetPlayerCommand | AbstractAsyncCommand |
| Threading | Handled automatically | Must handle yourself |
| Target’s Ref | Given to you | Must fetch manually |
| Flexibility | Args in any order | Order 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 contextsourceRef— The entity of whoever ran the command (can benullif console!)targetRef— The target player’s entitytargetPlayer— The target PlayerRefworld— The worldstore— 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
.otherpermission 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-Commands | Variants | |
|---|---|---|
| Syntax | /world create | /give item or /give player item |
| Purpose | Different actions | Same action, different targets |
| Class | AbstractCommandCollection | addUsageVariant() |
| Dispatch | By sub-command name | By 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:
-
Each variant MUST have a different number of required arguments. Two variants with the same count will cause an error!
-
Variants use a description-only constructor — no command name:
super("Description here") -
Do NOT use
AbstractTargetPlayerCommandfor variants! It adds--playeras an optional argument, not required. This means both your main command and variant would have the same required arg count → conflict → crash. UseAbstractAsyncCommandwith manualRequiredArginstead.
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
| Pattern | How |
|---|---|
| Heal player | EntityStatMap.addStatValue(healthIdx, amount) |
| Kill player | DeathComponent.tryAddComponent(store, ref, damage) |
| Give item | InventoryComponent.addItem(item, count) |
| Teleport | Teleport.addComponent(store, ref, position) |
| Send message | playerRef.sendMessage(Message.raw("text")) |
| Get position | TransformComponent.getPosition() |
Common Mistakes
| Mistake | Problem | Fix |
|---|---|---|
| Command not found | Forgot to register | Call registerCommand() in setup() |
| NullPointerException | Optional arg or component null | Check with != null before using |
| Wrong player affected | Confused sourceRef vs targetRef | In TargetPlayerCommand: sourceRef = who ran it, targetRef = who’s affected |
| Threading crash | Accessing Store from wrong thread | Use the right base class |
| No tab-completion | Wrong ArgType | Verify ArgType name |
| Blocking in async | Using .get() or .join() on futures | Use .thenAccept() or proper async patterns |
| Variant crash | Same required arg count | Each variant needs different number of required args |
Key Takeaways
Choosing the Right Base Class
| Base Class | You Get | Use For |
|---|---|---|
AbstractAsyncCommand | CommandContext | Messages, info |
AbstractPlayerCommand | Store, Ref (self) | Modify yourself |
AbstractTargetPlayerCommand | Store, sourceRef, targetRef | Modify other player |
AbstractTargetEntityCommand | Store, entity list | Modify any entity |
AbstractWorldCommand | World | World operations |
AbstractCommandCollection | Sub-commands | Grouping commands |
The Pattern
- Choose base class based on what you need access to
- Define arguments in constructor with
withXxxArg() - Implement execute() — the base class ensures correct threading
- Set permissions with
requirePermission() - Register in
Plugin.setup()
Quick Reference
| Concept | Purpose |
|---|---|
withRequiredArg() | Positional argument, must be provided |
withOptionalArg() | Named argument, can be omitted |
withDefaultArg() | Optional with static default value |
withFlagArg() | Boolean switch (--silent) |
ArgTypes.XXX | Parse 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!