Creating Commands
This guide covers how to create custom commands for your Hytale server mods.
Threading
Section titled “Threading”AbstractPlayerCommand.execute() runs on the world thread — World 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 Class | Thread |
|---|---|
CommandBase | Command pipeline thread (no world access) |
AbstractAsyncCommand | Depends on executor passed to runAsync() |
AbstractCommandCollection | Command pipeline thread (dispatches to subcommands) |
AbstractAsyncPlayerCommand | World thread for context resolution, then your executor |
AbstractAsyncWorldCommand | World thread for resolution, then your executor |
Blocks the world thread — keep execute() fast, no loops or sleeps:
| Base Class | Thread |
|---|---|
AbstractPlayerCommand | Player’s world thread (safe to access store/ref) |
AbstractWorldCommand | Target world thread |
AbstractTargetEntityCommand | Target world thread |
AbstractTargetPlayerCommand | Target 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.
Quick Start
Section titled “Quick Start”Here’s a minimal command:
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")); }}hello.desc = Say hellohello.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:
public class MyPlugin extends JavaPlugin { public MyPlugin(@Nonnull JavaPluginInit init) { super(init); }
@Override protected void setup() { this.getCommandRegistry().registerCommand(new HelloCommand()); }}Command with Arguments
Section titled “Command with Arguments”See Arguments for the full list of built-in ArgTypes — including positions with relative coordinates, enums for fixed choices, and validators for input validation.
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:
greet.desc = Greet someone by namegreet.name.desc = Name to greetgreet.count.desc = Number of times to greetgreet.success = Hello, {name}!Usage:
/greet Aliceoutputs “Hello, Alice!”/greet Bob --count=3outputs “Hello, Bob!” (3 times)/hi Charlieoutputs “Hello, Charlie!” (using alias)
Player-Only Commands
Section titled “Player-Only Commands”Use AbstractPlayerCommand for commands that require a player sender:
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())); }}heal.desc = Heal a playerheal.success = Healed {name}Command Senders
Section titled “Command Senders”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:
| Sender | isPlayer() | Permissions | UUID | Output |
|---|---|---|---|---|
Player | true | Checked via PermissionsModule | Player’s UUID | Network packet to client |
ConsoleSender | false | Always 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.
Console Compatibility by Base Class
Section titled “Console Compatibility by Base Class”Not all base classes work from the console. Some require a player for context:
| Base Class | Console? | Notes |
|---|---|---|
CommandBase | Yes | No sender restriction |
AbstractAsyncCommand | Yes | No sender restriction |
AbstractCommandCollection | Yes | Displays subcommand help |
AbstractPlayerCommand | No | Sends error automatically |
AbstractAsyncPlayerCommand | No | Sends error automatically |
AbstractWorldCommand | Partial | Works if --world provided, or if the server has exactly one world |
AbstractAsyncWorldCommand | Partial | Same as above |
AbstractTargetPlayerCommand | Partial | Works if --player provided |
AbstractTargetEntityCommand | Partial | Works 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.
Checking Sender Type
Section titled “Checking Sender Type”// branch on sender typeif (context.isPlayer()) { // player-specific logic} else { // console logic}
// get the raw senderCommandSender 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.
Console-Only Commands
Section titled “Console-Only Commands”There is no AbstractConsoleCommand base class. Use CommandBase and check context.isPlayer() to reject player senders:
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 }}shutdown.desc = Shut down the servershutdown.consoleonly = This command can only be run from the consoleAlternatively, use context.senderAs(ConsoleSender.class) which throws a SenderTypeException (with an automatic error message) if the sender isn’t the console.
Subcommands
Section titled “Subcommands”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 statusUsage Variants
Section titled “Usage Variants”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.
How variants work
Section titled “How variants work”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.
Defining a variant
Section titled “Defining a variant”// this is a variant — note: description key only, no namepublic 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 _ ~-10Register it on the parent command with addUsageVariant():
// in your parent command's constructorthis.addUsageVariant(new TpToCoordinatesCommand());Mixing subcommands and variants
Section titled “Mixing subcommands and variants”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.
Constraints
Section titled “Constraints”- Each variant must have a unique required parameter count — registering two variants with the same count throws
IllegalArgumentExceptionat 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.
Key Concepts
Section titled “Key Concepts”Base Classes
Section titled “Base Classes”| Class | Use Case |
|---|---|
| CommandBase | Simple synchronous commands |
| AbstractAsyncCommand | Base for async commands |
| AbstractCommandCollection | Parent command with only subcommands |
| AbstractPlayerCommand | Commands requiring a player sender |
| AbstractAsyncPlayerCommand | Player commands with async execution |
| AbstractWorldCommand | Commands operating on a world (adds --world option) |
| AbstractAsyncWorldCommand | World commands with async execution |
| AbstractTargetEntityCommand | Commands targeting entities by radius/player/entity/look-at |
| AbstractTargetPlayerCommand | Commands targeting self or other player (auto .other permission) |
See Base Classes for detailed examples and the full API.
Command Context
Section titled “Command Context”The CommandContext provides:
context.sender()- TheCommandSender(player or console)context.sendMessage(Message)- Send response messagecontext.isPlayer()- Check if sender is a playercontext.senderAs(Class)- Cast sender to specific type (throwsSenderTypeExceptionon mismatch)context.senderAsPlayerRef()- Get sender’s entity reference (throwsSenderTypeExceptionif not a player)
Messages
Section titled “Messages”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 parameterscontext.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.
Next Steps
Section titled “Next Steps”- Command Syntax - Tokenization, quoting, relative coordinates, and the parsing pipeline
- Base Classes - Detailed reference for all command base classes
- Arguments - Full argument type reference and validation
- Permissions - Permission system and groups
- Built-in Reference - Examples from built-in commands