Skip to content

Creating Commands

This guide covers how to create custom commands for your Hytale server mods.

AbstractPlayerCommand.execute() runs on the world threadWorld implements Executor and the command pipeline dispatches the callback to it. This means Store and Ref access inside execute() is safe.

Safe for heavy operations — these don’t block any world thread:

Base ClassThread
CommandBaseCommand pipeline thread (no world access)
AbstractAsyncCommandDepends on executor passed to runAsync()
AbstractCommandCollectionCommand pipeline thread (dispatches to subcommands)
AbstractAsyncPlayerCommandWorld thread for context resolution, then your executor
AbstractAsyncWorldCommandWorld thread for resolution, then your executor

Blocks the world thread — keep execute() fast, no loops or sleeps:

Base ClassThread
AbstractPlayerCommandPlayer’s world thread (safe to access store/ref)
AbstractWorldCommandTarget world thread
AbstractTargetEntityCommandTarget world thread
AbstractTargetPlayerCommandTarget player’s world thread

The world thread runs ticks, entity updates, and other commands — blocking it freezes the entire world. For batch or long-running operations, use the async variants (AbstractAsyncWorldCommand, AbstractAsyncPlayerCommand) or AbstractAsyncCommand with ArgTypes.WORLD.

If you need world access from CommandBase, use world.execute(() -> { ... }) to dispatch to the world thread. See Threading for details.

Here’s a minimal command:

HelloCommand.java
public class HelloCommand extends CommandBase {
public HelloCommand() {
super("hello", "myplugin.commands.hello.desc");
}
@Override
protected void executeSync(@Nonnull CommandContext context) {
// context.sendMessage(Message.raw("Hello, world!"));
context.sendMessage(Message.translation("myplugin.commands.hello.success"));
}
}
src/main/resources/Server/Languages/en-US/myplugin/commands.lang
hello.desc = Say hello
hello.success = Hello, world!

The second constructor parameter is a translation key, not a raw string. The file path relative to the locale directory becomes the key prefix: myplugin/commands.lang with key hello.desc resolves to myplugin.commands.hello.desc. See Translations for details.

Register it in your mod’s setup() method:

MyPlugin.java
public class MyPlugin extends JavaPlugin {
public MyPlugin(@Nonnull JavaPluginInit init) {
super(init);
}
@Override
protected void setup() {
this.getCommandRegistry().registerCommand(new HelloCommand());
}
}

See Arguments for the full list of built-in ArgTypes — including positions with relative coordinates, enums for fixed choices, and validators for input validation.

GreetCommand.java
public class GreetCommand extends CommandBase {
private final RequiredArg<String> nameArg;
private final OptionalArg<Integer> countArg;
public GreetCommand() {
super("greet", "myplugin.commands.greet.desc");
// required argument: /greet <name>
this.nameArg = this.withRequiredArg("name",
"myplugin.commands.greet.name.desc", ArgTypes.STRING);
// optional argument: /greet <name> [--count=1]
this.countArg = this.withOptionalArg("count",
"myplugin.commands.greet.count.desc", ArgTypes.INTEGER, 1);
// set permission
this.requirePermission("myplugin.command.greet");
// add aliases
this.addAliases("hi", "wave");
}
@Override
protected void executeSync(@Nonnull CommandContext context) {
String name = this.nameArg.get(context);
int count = this.countArg.get(context);
for (int i = 0; i < count; i++) {
context.sendMessage(Message.translation("myplugin.commands.greet.success")
.param("name", name));
}
}
}

The command above requires a .lang file in your asset pack to work:

src/main/resources/Server/Languages/en-US/myplugin/commands.lang
greet.desc = Greet someone by name
greet.name.desc = Name to greet
greet.count.desc = Number of times to greet
greet.success = Hello, {name}!

Usage:

  • /greet Alice outputs “Hello, Alice!”
  • /greet Bob --count=3 outputs “Hello, Bob!” (3 times)
  • /hi Charlie outputs “Hello, Charlie!” (using alias)

Use AbstractPlayerCommand for commands that require a player sender:

HealCommand.java
public class HealCommand extends AbstractPlayerCommand {
public HealCommand() {
super("heal", "myplugin.commands.heal.desc");
this.requirePermission("myplugin.command.heal");
}
@Override
protected void execute(@Nonnull CommandContext context,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world) {
context.sendMessage(Message.translation("myplugin.commands.heal.success")
.param("name", playerRef.getUsername()));
}
}
src/main/resources/Server/Languages/en-US/myplugin/commands.lang
heal.desc = Heal a player
heal.success = Healed {name}

Every command receives a CommandSender — either a player or the server console. The CommandSender interface extends IMessageReceiver (for sendMessage()) and PermissionHolder (for hasPermission()).

There are two implementations:

SenderisPlayer()PermissionsUUIDOutput
PlayertrueChecked via PermissionsModulePlayer’s UUIDNetwork packet to client
ConsoleSenderfalseAlways true (all permissions)UUID(0, 0)Server terminal

ConsoleSender is a singleton (ConsoleSender.INSTANCE) that always returns true for all permission checks. This means console can run any command regardless of permission requirements.

Not all base classes work from the console. Some require a player for context:

Base ClassConsole?Notes
CommandBaseYesNo sender restriction
AbstractAsyncCommandYesNo sender restriction
AbstractCommandCollectionYesDisplays subcommand help
AbstractPlayerCommandNoSends error automatically
AbstractAsyncPlayerCommandNoSends error automatically
AbstractWorldCommandPartialWorks if --world provided, or if the server has exactly one world
AbstractAsyncWorldCommandPartialSame as above
AbstractTargetPlayerCommandPartialWorks if --player provided
AbstractTargetEntityCommandPartialWorks if --entity, --player, or --world provided

For commands that should work from both console and players, use CommandBase or AbstractAsyncCommand and branch on sender type when needed.

// branch on sender type
if (context.isPlayer()) {
// player-specific logic
} else {
// console logic
}
// get the raw sender
CommandSender sender = context.sender();
// cast to specific type (throws SenderTypeException on mismatch)
Player player = context.senderAs(Player.class);
// get player ref (throws SenderTypeException if not a player)
Ref<EntityStore> ref = context.senderAsPlayerRef();

SenderTypeException is caught by the command pipeline and automatically sends a translated error message to the sender.

There is no AbstractConsoleCommand base class. Use CommandBase and check context.isPlayer() to reject player senders:

ShutdownCommand.java
public class ShutdownCommand extends CommandBase {
public ShutdownCommand() {
super("shutdown", "myplugin.commands.shutdown.desc");
}
@Override
protected void executeSync(@Nonnull CommandContext context) {
if (context.isPlayer()) {
context.sendMessage(Message.translation("myplugin.commands.shutdown.consoleonly"));
return;
}
// console-only logic here
}
}
src/main/resources/Server/Languages/en-US/myplugin/commands.lang
shutdown.desc = Shut down the server
shutdown.consoleonly = This command can only be run from the console

Alternatively, use context.senderAs(ConsoleSender.class) which throws a SenderTypeException (with an automatic error message) if the sender isn’t the console.

Use AbstractCommandCollection for parent commands with subcommands. Note that subcommand permissions are checked up the entire parent chain — see Subcommand Permission Chain.

public class MyToolCommand extends AbstractCommandCollection {
public MyToolCommand() {
super("mytool", "myplugin.commands.mytool.desc");
this.addSubCommand(new MyToolEnableCommand());
this.addSubCommand(new MyToolDisableCommand());
this.addSubCommand(new MyToolStatusCommand());
}
}
// usage: /mytool enable, /mytool disable, /mytool status

Usage variants let one command name handle multiple argument signatures. For example, /tp supports /tp <x> <y> <z> (3 params) and /tp <player> (1 param) — same command name, different parameter counts.

Each variant is a separate command class with no name (use the description-only constructor). The system routes to variants by counting required positional parameters — each variant must have a unique count.

// this is a variant — note: description key only, no name
public class TpToCoordinatesCommand extends AbstractPlayerCommand {
private final RequiredArg<RelativeIntPosition> posArg;
public TpToCoordinatesCommand() {
super("server.commands.tp.coords.desc"); // no name
// RELATIVE_BLOCK_POSITION consumes 3 tokens: <x> <y> <z>
// supports ~ for relative coords, _ for surface height, c for chunk scale
this.posArg = this.withRequiredArg("position",
"server.commands.tp.position.desc", ArgTypes.RELATIVE_BLOCK_POSITION);
}
@Override
protected void execute(@Nonnull CommandContext context,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world) {
// resolves ~ and _ prefixes relative to the player's current position
Vector3i pos = this.posArg.get(context).getBlockPosition(context, store);
// teleport to pos...
}
}
// usage: /tp 10 64 200, /tp ~ ~5 ~, /tp ~10 _ ~-10

Register it on the parent command with addUsageVariant():

// in your parent command's constructor
this.addUsageVariant(new TpToCoordinatesCommand());

You can register both on the same parent. The built-in /tp command does this:

public class TeleportCommand extends AbstractCommandCollection {
public TeleportCommand() {
super("tp", "server.commands.tp.desc");
this.setPermissionGroup(GameMode.Creative);
this.addAliases("teleport");
// variants — dispatched by required parameter count
this.addUsageVariant(new TpToPlayerCommand()); // /tp <player> (1 param)
this.addUsageVariant(new TpOtherToPlayerCommand()); // /tp <player> <player> (2 params)
this.addUsageVariant(new TpToCoordinatesCommand()); // /tp <x> <y> <z> (3 params)
this.addUsageVariant(new TpPlayerToCoordinatesCommand()); // /tp <player> <x> <y> <z> (4 params)
// subcommands — dispatched by name
this.addSubCommand(new TeleportAllCommand()); // /tp all ...
this.addSubCommand(new TeleportHomeCommand()); // /tp home
this.addSubCommand(new TeleportTopCommand()); // /tp top
this.addSubCommand(new TeleportBackCommand()); // /tp back
}
}

When a user types /tp back, the first token matches the back subcommand. When they type /tp 10 20 30, no subcommand matches, so the variant with 3 required parameters fires.

  • Each variant must have a unique required parameter count — registering two variants with the same count throws IllegalArgumentException at startup. Variants are dispatched by token count, not by argument type, so you cannot disambiguate by type alone (e.g., 3 numbers vs 3 strings).
  • Variants cannot be nested — you cannot add a variant to another variant.
  • Variants inherit the parent command’s permission by default.
  • Named arguments (--player, --world, etc.) do not count toward the positional parameter total — only required positional args affect variant routing.
ClassUse Case
CommandBaseSimple synchronous commands
AbstractAsyncCommandBase for async commands
AbstractCommandCollectionParent command with only subcommands
AbstractPlayerCommandCommands requiring a player sender
AbstractAsyncPlayerCommandPlayer commands with async execution
AbstractWorldCommandCommands operating on a world (adds --world option)
AbstractAsyncWorldCommandWorld commands with async execution
AbstractTargetEntityCommandCommands targeting entities by radius/player/entity/look-at
AbstractTargetPlayerCommandCommands targeting self or other player (auto .other permission)

See Base Classes for detailed examples and the full API.

The CommandContext provides:

  • context.sender() - The CommandSender (player or console)
  • context.sendMessage(Message) - Send response message
  • context.isPlayer() - Check if sender is a player
  • context.senderAs(Class) - Cast sender to specific type (throws SenderTypeException on mismatch)
  • context.senderAsPlayerRef() - Get sender’s entity reference (throws SenderTypeException if not a player)

Use Message for responses. Prefer Message.translation() for player-facing text — see Translations for the .lang file format, parameter syntax, and plural rules.

// raw text (debug/internal only)
context.sendMessage(Message.raw("Hello!"));
// translation key (for player-facing text)
context.sendMessage(Message.translation("myplugin.commands.hello.success"));
// with parameters
context.sendMessage(Message.translation("myplugin.commands.greet.success")
.param("name", playerName)
.param("count", count));

param() accepts String, int, long, float, double, boolean, and Message values directly. See Translations — Parameters for the full list and formatting options.