-
Notifications
You must be signed in to change notification settings - Fork 1
Commands
The codebase uses a fluent, type-safe command system built on inheritance patterns with automatic permission generation and flexible argument handling.
Located in server/core/command/system/
| Class | Purpose |
|---|---|
AbstractCommand |
Base class for all commands with argument registration and permission handling |
CommandManager |
Central registry for command registration and execution dispatch |
CommandContext |
Holds execution context including sender, parsed arguments, and input |
CommandSender |
Interface for entities that can send commands (extends PermissionHolder) |
ParseResult |
Tracks errors during command parsing |
ParserContext |
Categorizes input tokens into required/optional arguments |
Located in basecommands/
AbstractCommand (base)
├── CommandBase (synchronous execution)
├── AbstractAsyncCommand (asynchronous execution)
│ ├── AbstractPlayerCommand (player-context commands)
│ ├── AbstractWorldCommand (world-context commands)
│ └── AbstractTargetPlayerCommand (target other players)
└── AbstractCommandCollection (subcommand containers/menus)
For commands that execute immediately and block until complete.
public class MyCommand extends CommandBase {
public MyCommand() {
super("mycommand", "server.commands.mycommand.desc");
}
@Override
protected void executeSync(@Nonnull CommandContext context) {
// Your synchronous logic here
context.sendMessage(Message.raw("Command executed!"));
}
}For commands that return a CompletableFuture<Void> and don't block.
public class MyAsyncCommand extends AbstractAsyncCommand {
public MyAsyncCommand() {
super("myasync", "server.commands.myasync.desc");
}
@Nonnull
@Override
protected CompletableFuture<Void> executeAsync(@Nonnull CommandContext context) {
return CompletableFuture.runAsync(() -> {
// Your async logic here
});
}
}For commands that require a player to execute and need access to player/world context.
public class MyPlayerCommand extends AbstractPlayerCommand {
public MyPlayerCommand() {
super("myplayercmd", "server.commands.myplayercmd.desc");
}
@Override
protected void execute(
@Nonnull CommandContext context,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world
) {
// Access to player store, reference, and world
Player player = store.getComponent(ref, Player.getComponentType());
// Your player-specific logic here
}
}For commands that act as menus containing subcommands.
public class PlayerCommand extends AbstractCommandCollection {
public PlayerCommand() {
super("player", "server.commands.player.desc");
this.addSubCommand(new PlayerResetCommand());
this.addSubCommand(new PlayerStatsSubCommand());
this.addSubCommand(new PlayerEffectSubCommand());
}
}System commands are registered directly with the CommandManager:
// In CommandManager.registerCommands()
this.registerSystemCommand(new GameModeCommand());
this.registerSystemCommand(new KillCommand());
this.registerSystemCommand(new GiveCommand());Permission format: hytale.system.command.<commandname>
Plugins register commands through their CommandRegistry:
public class MyPlugin extends PluginBase {
@Override
protected void setup() {
CommandRegistry commandRegistry = this.getCommandRegistry();
commandRegistry.registerCommand(new MyCommand());
commandRegistry.registerCommand(new AnotherCommand());
}
}Permission format: <plugin.basePermission>.command.<commandname>
registerCommand(AbstractCommand)
└── command.setOwner(plugin/manager)
└── Auto-generate permission if not set
└── CommandManager.register(command)
└── Add to commandRegistration map
└── command.completeRegistration()
└── Validate arguments and variants
└── Register aliases
└── Return CommandRegistration (for unregistration)
Located in arguments/system/
| Type | Description | Usage Pattern |
|---|---|---|
RequiredArg<T> |
Must be provided by user | Positional: /cmd <value>
|
DefaultArg<T> |
Optional with default value |
--name value or uses default |
OptionalArg<T> |
Optional, null if not provided | --name value |
FlagArg |
Boolean flag (true if present) | --flagname |
Arguments are defined as fields in your command class using fluent methods:
public class GiveCommand extends AbstractPlayerCommand {
// Required argument - must be provided
private final RequiredArg<Item> itemArg =
this.withRequiredArg("item", "server.commands.give.item.desc", ArgTypes.ITEM_ASSET);
// Default argument - has fallback value
private final DefaultArg<Integer> quantityArg =
this.withDefaultArg("quantity", "server.commands.give.quantity.desc", ArgTypes.INTEGER, 1, "1");
// Optional argument - null if not provided
private final OptionalArg<String> metadataArg =
this.withOptionalArg("metadata", "server.commands.give.metadata.desc", ArgTypes.STRING);
// Flag argument - boolean presence check
private final FlagArg verboseFlag =
this.withFlagArg("verbose", "server.commands.give.verbose.desc");
}For arguments that accept multiple values:
// Required list argument
private final RequiredArg<List<String>> namesArg =
this.withListRequiredArg("names", "description", ArgTypes.STRING);
// Default list argument
private final DefaultArg<List<Integer>> idsArg =
this.withListDefaultArg("ids", "description", ArgTypes.INTEGER, List.of(1, 2, 3), "1, 2, 3");
// Optional list argument
private final OptionalArg<List<UUID>> uuidsArg =
this.withListOptionalArg("uuids", "description", ArgTypes.UUID);@Override
protected void execute(CommandContext context, ...) {
// Get required argument value
Item item = this.itemArg.get(context);
// Get default argument (returns default if not provided)
Integer quantity = this.quantityArg.get(context);
// Check if optional argument was provided
if (this.metadataArg.provided(context)) {
String metadata = this.metadataArg.get(context);
}
// Check flag presence
boolean isVerbose = this.verboseFlag.get(context);
}Optional arguments can have dependencies on other arguments:
private final OptionalArg<Integer> radiusArg = this.withOptionalArg("radius", "desc", ArgTypes.INTEGER)
.requiredIf(this.positionArg) // Required when position is provided
.requiredIfAbsent(this.targetArg) // Required when target is NOT provided
.availableOnlyIfAll(this.enabledFlag) // Only available when enabled flag is set
.availableOnlyIfAllAbsent(this.simpleFlag); // Only available when simple flag is NOT setOptional arguments can have aliases:
private final OptionalArg<Integer> countArg = this.withOptionalArg("count", "desc", ArgTypes.INTEGER)
.addAlias("c")
.addAlias("num");
// All of these work: --count 5, --c 5, --num 5Optional arguments can require specific permissions:
private final OptionalArg<Boolean> forceArg = this.withOptionalArg("force", "desc", ArgTypes.BOOLEAN);
// Set permission in constructor:
this.forceArg.setPermission("mycommand.force");Located in ArgTypes.java
| Type | Description | Examples |
|---|---|---|
ArgTypes.BOOLEAN |
true/false values |
true, false
|
ArgTypes.INTEGER |
Whole numbers |
-27432, 0, 56346
|
ArgTypes.FLOAT |
Decimal numbers (single precision) |
3.14159, -2.5
|
ArgTypes.DOUBLE |
Decimal numbers (double precision) |
-3.14, 3.141596
|
ArgTypes.STRING |
Text values | "Hytale is cool!" |
ArgTypes.UUID |
UUID identifiers | 550e8400-e29b-41d4-a716-446655440000 |
| Type | Description | Examples |
|---|---|---|
ArgTypes.PLAYER_UUID |
Player by UUID or username |
john_doe, <uuid>
|
ArgTypes.PLAYER_REF |
Player reference |
john_doe, user123
|
ArgTypes.ENTITY_ID |
Entity by UUID | <uuid> |
| Type | Description | Examples |
|---|---|---|
ArgTypes.WORLD |
World by name | default |
ArgTypes.RELATIVE_POSITION |
3D double coordinates (relative) |
~ ~ ~, ~5.5 ~ ~
|
ArgTypes.RELATIVE_BLOCK_POSITION |
3D int coordinates (relative) |
~ ~ ~, ~5 ~ ~-3
|
ArgTypes.RELATIVE_CHUNK_POSITION |
Chunk coordinates |
5 10, ~c2 ~c-3
|
ArgTypes.VECTOR3I |
3D integer vector | 124 232 234 |
ArgTypes.VECTOR2I |
2D integer vector | 124 232 |
ArgTypes.ROTATION |
Pitch/yaw/roll | 124.63 232.27 234.22 |
| Type | Description |
|---|---|
ArgTypes.ITEM_ASSET |
Item asset reference |
ArgTypes.BLOCK_TYPE_ASSET |
Block type asset reference |
ArgTypes.MODEL_ASSET |
Model asset reference |
ArgTypes.PARTICLE_SYSTEM |
Particle system asset |
ArgTypes.SOUND_EVENT_ASSET |
Sound event asset |
ArgTypes.EFFECT_ASSET |
Entity effect asset |
ArgTypes.WEATHER_ASSET |
Weather asset |
ArgTypes.ENVIRONMENT_ASSET |
Environment asset |
| Type | Description | Examples |
|---|---|---|
ArgTypes.GAME_MODE |
Game mode enum |
SURVIVAL, CREATIVE, BUILDER
|
ArgTypes.SOUND_CATEGORY |
Sound category |
MASTER, MUSIC, AMBIENT
|
ArgTypes.COLOR |
Color value |
#FF0000, 16711680, 0xFF0000
|
ArgTypes.TICK_RATE |
Tick rate |
30tps, 33ms, 60
|
| Type | Description | Examples |
|---|---|---|
ArgTypes.INT_RANGE |
Min/max integer range |
-2 8, 1 5
|
ArgTypes.BLOCK_PATTERN |
Block pattern with weights | [20%Rock_Stone, 80%Rock_Shale] |
ArgTypes.BLOCK_MASK |
Block mask filters | [!Fluid_Water, !^Fluid_Lava] |
// For any enum type
SingleArgumentType<MyEnum> MY_ENUM = ArgTypes.forEnum("my.enum.name", MyEnum.class);Add named subcommands to create command hierarchies:
public class EntityCommand extends AbstractCommandCollection {
public EntityCommand() {
super("entity", "server.commands.entity.desc");
this.addSubCommand(new EntitySpawnCommand()); // /entity spawn
this.addSubCommand(new EntityKillCommand()); // /entity kill
this.addSubCommand(new EntityListCommand()); // /entity list
}
}Rules:
- Subcommand must have a name
- Cannot reuse subcommand instances (each needs unique parent)
- Subcommand names and aliases must be unique within parent
Add alternate syntaxes with different argument counts:
public class GameModeCommand extends AbstractPlayerCommand {
private final RequiredArg<GameMode> gameModeArg =
this.withRequiredArg("gamemode", "desc", ArgTypes.GAME_MODE);
public GameModeCommand() {
super("gamemode", "server.commands.gamemode.desc");
this.addAliases("gm");
this.requirePermission(HytalePermissions.fromCommand("gamemode.self"));
// Add variant for targeting other players
this.addUsageVariant(new GameModeOtherCommand());
}
// Self-targeting: /gamemode creative
@Override
protected void execute(...) {
GameMode gameMode = this.gameModeArg.get(context);
// Set sender's gamemode
}
// Variant for others: /gamemode creative PlayerName
private static class GameModeOtherCommand extends CommandBase {
private final RequiredArg<GameMode> gameModeArg =
this.withRequiredArg("gamemode", "desc", ArgTypes.GAME_MODE);
private final RequiredArg<PlayerRef> playerArg =
this.withRequiredArg("player", "desc", ArgTypes.PLAYER_REF);
GameModeOtherCommand() {
super("server.commands.gamemode.other.desc"); // No name for variants!
this.requirePermission(HytalePermissions.fromCommand("gamemode.other"));
}
@Override
protected void executeSync(CommandContext context) {
GameMode gameMode = this.gameModeArg.get(context);
PlayerRef target = this.playerArg.get(context);
// Set target's gamemode
}
}
}Rules:
- Variants must NOT have a name (use description-only constructor)
- Variants are matched by number of required parameters
- Cannot have two variants with the same parameter count
Permissions are automatically generated based on the command hierarchy:
System command: hytale.system.command.<name>
Plugin command: <plugin.basePermission>.command.<name>
Subcommand: <parent.permission>.<subcommand.name>
public MyCommand() {
super("mycommand", "description");
this.requirePermission("custom.permission.node");
}Group commands by game mode or category:
public MyCommand() {
super("mycommand", "description");
this.setPermissionGroup(GameMode.BUILDER); // Only for builder mode
// OR
this.setPermissionGroups("admin", "moderator"); // Multiple groups
}hasPermission(sender)
└── Check if sender has this command's permission
└── If yes, recursively check parent command permissions
└── Return true only if all permissions pass
1. User Input: "/gamemode creative PlayerName"
2. CommandManager.handleCommand()
└── Parse command name from input ("gamemode")
└── Find command in registry or aliases
└── Call runCommand()
3. Tokenization
└── Tokenizer.parseArguments() - Split into tokens
└── ParserContext.of() - Categorize tokens
4. AbstractCommand.acceptCall()
└── Check for subcommand/variant match
└── Verify permissions
└── Check for --help flag
└── Check confirmation requirement (if any)
└── Validate required parameter count
5. Argument Processing
└── processRequiredArguments() - Parse positional args
└── processOptionalArguments() - Parse --name value pairs
└── Verify argument dependencies
6. Execution
└── execute(CommandContext)
├── CommandBase: executeSync()
├── AbstractAsyncCommand: executeAsync()
└── AbstractPlayerCommand: execute(context, store, ref, playerRef, world)
| Category | Description | Example |
|---|---|---|
| Pre-optional single | Positional arguments | creative PlayerName |
| Pre-optional list | Multi-value between `{ | and |
| Optional args | Key-value pairs | --count 5 --verbose |
public MyCommand() {
super("mycommand", "description");
this.addAliases("mc", "mycmd"); // /mc and /mycmd also work
}For dangerous commands that need explicit confirmation:
public DangerousCommand() {
super("dangerous", "description", true); // requiresConfirmation = true
}
// User must run: /dangerous --confirmMark commands as unavailable in singleplayer:
public MyCommand() {
super("mycommand", "description");
this.setUnavailableInSingleplayer(true);
}Allow commands to accept more arguments than defined:
public MyCommand() {
super("mycommand", "description");
this.setAllowsExtraArguments(true);
}package com.example.plugin.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.arguments.system.*;
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
import com.hypixel.hytale.server.core.command.system.basecommands.CommandBase;
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;
public class HealCommand extends AbstractPlayerCommand {
// Required argument
private final RequiredArg<Float> amountArg =
this.withRequiredArg("amount", "server.commands.heal.amount.desc", ArgTypes.FLOAT);
// Optional flag
private final FlagArg maxFlag =
this.withFlagArg("max", "server.commands.heal.max.desc");
public HealCommand() {
super("heal", "server.commands.heal.desc");
this.addAliases("hp", "health");
this.requirePermission("myplugin.command.heal.self");
// Add variant for healing other players
this.addUsageVariant(new HealOtherCommand());
}
@Override
protected void execute(
@Nonnull CommandContext context,
@Nonnull Store<EntityStore> store,
@Nonnull Ref<EntityStore> ref,
@Nonnull PlayerRef playerRef,
@Nonnull World world
) {
Player player = store.getComponent(ref, Player.getComponentType());
float amount = this.amountArg.get(context);
if (this.maxFlag.get(context)) {
// Heal to max
context.sendMessage(Message.translation("server.commands.heal.maxed"));
} else {
// Heal by amount
context.sendMessage(
Message.translation("server.commands.heal.success")
.param("amount", amount)
);
}
}
// Variant for healing other players
private static class HealOtherCommand extends CommandBase {
private final RequiredArg<PlayerRef> targetArg =
this.withRequiredArg("target", "server.commands.heal.target.desc", ArgTypes.PLAYER_REF);
private final RequiredArg<Float> amountArg =
this.withRequiredArg("amount", "server.commands.heal.amount.desc", ArgTypes.FLOAT);
HealOtherCommand() {
super("server.commands.heal.other.desc");
this.requirePermission("myplugin.command.heal.other");
}
@Override
protected void executeSync(@Nonnull CommandContext context) {
PlayerRef target = this.targetArg.get(context);
float amount = this.amountArg.get(context);
context.sendMessage(
Message.translation("server.commands.heal.other.success")
.param("target", target.getUsername())
.param("amount", amount)
);
}
}
}public class MyPlugin extends PluginBase {
@Override
protected void setup() {
this.getCommandRegistry().registerCommand(new HealCommand());
}
}The server includes many built-in commands registered in CommandManager:
-
gamemode/gm- Change game mode -
kill- Kill self or other player -
damage- Apply damage -
give- Give items -
inventory- Inventory management -
player- Player subcommands (stats, effects, camera, etc.) -
hide- Hide player -
sudo- Execute command as another player -
whereami- Show current location -
whoami- Show player info
-
chunk- Chunk operations -
entity- Entity management -
spawnblock- Block spawning -
worldgen- World generation
-
auth- Authentication -
kick- Kick player -
maxplayers- Set max players -
stop- Stop server -
who- List online players
-
help- Show help -
backup- Create backups -
notify- Send notifications -
sound- Play sounds -
worldmap- World map operations -
lighting- Lighting commands -
sleep- Sleep/pause -
network- Network commands -
commands- List commands
-
ping- Check latency -
version- Show version -
log- Logging control -
server- Server stats (CPU, memory, GC) -
assets- Asset information -
packs- Pack management -
stresstest- Stress testing - And many more...
| Feature | Implementation |
|---|---|
| Type Safety | Generics throughout (RequiredArg<T>, ArgumentType<T>) |
| Fluent API | Chain methods for argument definition |
| Auto Permissions | Generated hierarchically from command structure |
| Sync/Async | Choose CommandBase or AbstractAsyncCommand
|
| Player Context |
AbstractPlayerCommand provides store/ref/world access |
| Subcommands |
addSubCommand() for hierarchical commands |
| Variants |
addUsageVariant() for alternate syntaxes |
| Validation | Arguments validate automatically during parsing |
| Tab Completion | Built into ArgumentType.suggest()
|
| Localization | Description keys support i18n |