Command Framework — Overview

The EriAPI Command Framework provides a fluent, declarative API for creating Minecraft commands. Instead of manually parsing strings, handling tab completion, and managing permissions, you describe your command structure and the framework does the rest.

Key Features

  • Fluent Builder API — describe commands as readable chains of method calls
  • Type-safe Arguments — 7 built-in argument types with automatic parsing and validation
  • Auto Tab Completion — every argument type provides contextual suggestions out of the box
  • Permissions & Cooldowns — built-in support for permission levels, custom predicates, and per-player cooldowns
  • Auto-generated Help/yourcommand help is created automatically with usage and descriptions
  • Sub-command Trees — nest sub-commands to any depth with isolated arguments and executors
  • Brigo Compatible — works alongside Brigo for Brigadier-style suggestions on 1.12.2
Forge 1.12.2 Commands

This framework wraps Forge's ICommand system. You don't need to know anything about ICommand — the framework handles registration, parsing, and dispatching for you.

Quick Start

Here is everything you need to create and register a simple command in two steps.

Step 1 — Define your command

MyCommand.java
import fr.eri.eriapi.command.EriCommand;
import fr.eri.eriapi.command.arg.StringArg;

public class MyCommand {

    public static void register() {
        EriCommand.create("hello")
            .description("Greets the player")
            .rootAliases("hi", "greet")
            .arg(StringArg.of("name").optional("World"))
            .runs(ctx -> {
                String name = ctx.getString("name");
                ctx.reply("Hello, " + name + "!");
            })
            .register();
    }
}

Step 2 — Hook into your mod lifecycle

YourMod.java
@Mod(modid = "yourmod")
public class YourMod {

    @EventHandler
    public void init(FMLInitializationEvent event) {
        // Register command definitions
        MyCommand.register();
    }

    @EventHandler
    public void onServerStarting(FMLServerStartingEvent event) {
        // Register all commands with the server
        CommandRegistry.getInstance().onServerStarting(event);
    }
}

That's it! Players can now type /hello, /hello Steve, /hi, or /greet in-game. Tab completion and help are automatic.

EriCommand

EriCommand is the entry point for building commands. It uses a fluent builder pattern — every method returns the builder so you can chain calls.

Builder Methods

Method Description
create(String name) Static factory — creates a new command builder with the given name
description(String desc) Sets the description shown in auto-help
permission(int level) Requires a minimum permission level (0=all, 2=op, 4=server)
permission(Predicate<ICommandSender>) Custom permission check (e.g., team membership, item in hand)
rootAliases(String...) Alternative top-level names for the command
autoHelp(boolean) Enable/disable auto-generated /cmd help (default: true)
cooldown(int ticks) Per-player cooldown in ticks (20 ticks = 1 second)
arg(Argument) Adds a typed argument to this command or sub-command
sub(String name) Begins a sub-command definition (returns the sub-command builder)
runs(Consumer<CommandContext>) Sets the executor — the code that runs when the command is invoked
end() Returns to the parent builder (used after sub())
register() Finalizes the command and registers it with CommandRegistry

Minimal Example

Minimal command
EriCommand.create("ping")
    .description("Replies with pong")
    .runs(ctx -> ctx.reply("Pong!"))
    .register();

Argument Types

Arguments define what data a command expects. Each argument type handles parsing, validation, and tab completion automatically. All arguments support .optional(defaultValue) to make them non-required.

IntArg

Parses an integer value. Supports optional range constraints.

IntArg examples
// Required integer, any value
IntArg.of("count")

// With range validation (1 to 64)
IntArg.of("amount").range(1, 64)

// Optional with default value
IntArg.of("amount").range(1, 64).optional(1)

// In a command
EriCommand.create("give")
    .arg(IntArg.of("amount").range(1, 64).optional(1))
    .runs(ctx -> {
        int amount = ctx.getInt("amount");
        // ...
    })
    .register();

FloatArg

Parses a floating-point number. Supports optional range constraints.

FloatArg examples
// Required float
FloatArg.of("speed")

// With range
FloatArg.of("speed").range(0.0f, 10.0f)

// In a command
EriCommand.create("setspeed")
    .arg(FloatArg.of("speed").range(0, 10))
    .runs(ctx -> {
        float speed = ctx.getFloat("speed");
        ctx.reply("Speed set to " + speed);
    })
    .register();

StringArg

Parses a string value. Three modes are available: single word (default), greedy (consumes all remaining input), and quoted (captures text between quotes).

StringArg examples
// Single word
StringArg.of("name")

// Greedy — captures everything after this argument
StringArg.greedy("message")

// Quoted — captures text between "quotes"
StringArg.quoted("text")

// In a command
EriCommand.create("say")
    .arg(StringArg.greedy("message"))
    .runs(ctx -> {
        String msg = ctx.getString("message");
        ctx.broadcastMessage(msg);
    })
    .register();
Greedy arguments must be last

A greedy StringArg consumes all remaining input, so it must always be the last argument in a command or sub-command.

PlayerArg

Parses an online player name. Tab completion suggests all online players.

PlayerArg example
EriCommand.create("heal")
    .permission(2)
    .arg(PlayerArg.of("target"))
    .runs(ctx -> {
        EntityPlayerMP player = ctx.getPlayer("target");
        player.setHealth(player.getMaxHealth());
        ctx.reply("Healed " + player.getName());
    })
    .register();

ItemArg

Parses a Minecraft item identifier (e.g., minecraft:diamond_sword). Tab completion suggests all registered item names.

ItemArg example
EriCommand.create("spawn")
    .arg(ItemArg.of("item"))
    .arg(IntArg.of("count").range(1, 64).optional(1))
    .runs(ctx -> {
        Item item = ctx.getItem("item");
        int count = ctx.getInt("count");
        // Give item to player...
    })
    .register();

PosArg

Parses a 3D position (x y z). Supports relative coordinates with ~ notation (e.g., ~ ~5 ~). Tab completion suggests ~ ~ ~.

PosArg example
EriCommand.create("teleport")
    .permission(2)
    .arg(PosArg.of("pos"))
    .runs(ctx -> {
        BlockPos pos = ctx.getPos("pos");
        ctx.getSenderPlayer().setPositionAndUpdate(
            pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5
        );
        ctx.reply("Teleported to " + pos.getX() + " " + pos.getY() + " " + pos.getZ());
    })
    .register();

EnumArg

Parses a value from a Java enum. Tab completion suggests all enum constant names (case-insensitive).

EnumArg example
public enum GameMode { SURVIVAL, CREATIVE, ADVENTURE, SPECTATOR }

EriCommand.create("gamemode")
    .permission(2)
    .arg(EnumArg.of("mode", GameMode.class))
    .runs(ctx -> {
        GameMode mode = ctx.getEnum("mode", GameMode.class);
        ctx.reply("Game mode set to " + mode.name());
    })
    .register();

Argument Summary

Type Factory Context Getter Tab Completion
IntArg IntArg.of("name") ctx.getInt("name") Suggests <name>
FloatArg FloatArg.of("name") ctx.getFloat("name") Suggests <name>
StringArg StringArg.of("name") ctx.getString("name") Suggests <name>
PlayerArg PlayerArg.of("name") ctx.getPlayer("name") Online player names
ItemArg ItemArg.of("name") ctx.getItem("name") Registered item IDs
PosArg PosArg.of("name") ctx.getPos("name") ~ ~ ~
EnumArg EnumArg.of("name", E.class) ctx.getEnum("name", E.class) Enum constant names

CommandContext

When your command runs, it receives a CommandContext object. This object gives you access to the parsed arguments, the sender, the server, and utility methods for responding.

Available Methods

Method Return Type Description
getString("name") String Gets a string argument value
getInt("name") int Gets an integer argument value
getFloat("name") float Gets a float argument value
getPlayer("name") EntityPlayerMP Gets the resolved player entity
getItem("name") Item Gets the resolved item
getPos("name") BlockPos Gets the parsed block position
getEnum("name", E.class) E Gets the parsed enum value
getSender() ICommandSender The entity or console that sent the command
getSenderPlayer() EntityPlayerMP The player who sent the command (throws if not a player)
getServer() MinecraftServer The server instance
reply(String msg) void Sends a chat message to the sender
replyError(String msg) void Sends a red error message to the sender
broadcastMessage(String msg) void Broadcasts a message to all online players

Usage Example

Using CommandContext
EriCommand.create("info")
    .arg(PlayerArg.of("target"))
    .runs(ctx -> {
        EntityPlayerMP target = ctx.getPlayer("target");
        EntityPlayerMP sender = ctx.getSenderPlayer();

        ctx.reply("Player: " + target.getName());
        ctx.reply("Health: " + target.getHealth() + "/" + target.getMaxHealth());
        ctx.reply("Position: " + target.getPosition().toString());

        if (target.getHealth() < 5) {
            ctx.replyError("Warning: this player is low on health!");
        }
    })
    .register();

Sub-commands

Use .sub(name) to create nested sub-commands. Each sub-command can have its own arguments, permissions, description, and executor. Call .end() to return to the parent builder.

Basic Sub-commands

Sub-command tree
EriCommand.create("team")
    .description("Team management commands")

    .sub("create")
        .description("Create a new team")
        .permission(2)
        .arg(StringArg.of("name"))
        .runs(ctx -> {
            String name = ctx.getString("name");
            ctx.reply("Team '" + name + "' created!");
        })
    .end()

    .sub("invite")
        .description("Invite a player to your team")
        .arg(PlayerArg.of("player"))
        .runs(ctx -> {
            EntityPlayerMP player = ctx.getPlayer("player");
            ctx.reply("Invited " + player.getName());
        })
    .end()

    .sub("leave")
        .description("Leave your current team")
        .runs(ctx -> ctx.reply("You left the team."))
    .end()

    .register();

This creates: /team create <name>, /team invite <player>, and /team leave. The auto-generated /team help lists all three sub-commands with their descriptions.

Nested Sub-commands

You can nest sub-commands within sub-commands to create deep command trees:

Deeply nested sub-commands
EriCommand.create("admin")
    .permission(4)

    .sub("config")
        .sub("set")
            .arg(StringArg.of("key"))
            .arg(StringArg.of("value"))
            .runs(ctx -> {
                ctx.reply("Set " + ctx.getString("key") + " = " + ctx.getString("value"));
            })
        .end()

        .sub("get")
            .arg(StringArg.of("key"))
            .runs(ctx -> {
                ctx.reply("Value of " + ctx.getString("key") + ": ...");
            })
        .end()
    .end()

    .register();

This creates /admin config set <key> <value> and /admin config get <key>.

Root executor is optional

A command with sub-commands does not need a .runs() on the root. If no root executor is set and the player types just /team, the auto-help is shown automatically.

Permissions & Cooldowns

Permission Levels

Forge uses integer permission levels. You can set a minimum level on any command or sub-command:

Level Who can use it
0 Everyone (default)
1 Moderators
2 Operators (op)
3 Operators (higher)
4 Server console / owner only
Permission level
EriCommand.create("ban")
    .permission(2)  // Only ops can use this
    .arg(PlayerArg.of("target"))
    .arg(StringArg.greedy("reason").optional("No reason given"))
    .runs(ctx -> {
        EntityPlayerMP target = ctx.getPlayer("target");
        String reason = ctx.getString("reason");
        // ban logic...
    })
    .register();

Custom Permission Predicate

For more complex checks, use a predicate:

Custom permission
EriCommand.create("vip")
    .permission(sender -> {
        // Only allow if the sender is a player with a specific tag
        if (sender instanceof EntityPlayerMP) {
            return ((EntityPlayerMP) sender).getTags().contains("vip");
        }
        return false;
    })
    .runs(ctx -> ctx.reply("Welcome, VIP!"))
    .register();

Cooldowns

Prevent command spam with per-player cooldowns. The cooldown is specified in ticks (20 ticks = 1 second).

Cooldown example
EriCommand.create("heal")
    .cooldown(600)  // 30 seconds (600 ticks)
    .runs(ctx -> {
        EntityPlayerMP player = ctx.getSenderPlayer();
        player.setHealth(player.getMaxHealth());
        ctx.reply("You have been healed!");
    })
    .register();
Cooldown feedback

When a player triggers a command on cooldown, they automatically receive a message telling them how many seconds remain before they can use it again.

Tab Completion

Tab completion is handled automatically based on your argument types. When the player presses Tab, the framework determines which argument is being typed and provides appropriate suggestions.

Built-in Suggestions

  • PlayerArg — suggests online player names
  • ItemArg — suggests all registered item identifiers
  • EnumArg — suggests all enum constant names
  • PosArg — suggests ~ ~ ~ (relative coords)
  • Sub-commands — suggests available sub-command names
  • IntArg / FloatArg / StringArg — shows <name> as a hint

Custom Suggestions

You can provide custom suggestions for any argument using .suggests():

Custom tab suggestions
EriCommand.create("warp")
    .arg(StringArg.of("destination")
        .suggests(() -> Arrays.asList("spawn", "arena", "shop", "hub"))
    )
    .runs(ctx -> {
        String dest = ctx.getString("destination");
        ctx.reply("Warping to " + dest + "...");
    })
    .register();

Brigo Compatibility

If you use Brigo (a Brigadier bridge for 1.12.2), EriCommand's tab completion integrates seamlessly. Brigo provides client-side suggestion rendering with tooltips — EriCommand supplies the suggestions, and Brigo handles the presentation.

Brigo integration
// No extra code needed — EriCommand automatically exposes
// its argument structure to Brigo if Brigo is present.
// The player sees Brigadier-style suggestions in the chat bar.

EriCommand.create("example")
    .arg(PlayerArg.of("target"))  // Brigo renders player names as suggestions
    .arg(IntArg.of("amount"))     // Brigo shows <amount> hint
    .runs(ctx -> { /* ... */ })
    .register();

CommandRegistry

CommandRegistry is a singleton that collects all commands built with EriCommand and registers them with the Forge server when it starts.

How It Works

  1. Build phase — calling .register() on an EriCommand adds it to CommandRegistry.
  2. Server start — calling CommandRegistry.getInstance().onServerStarting(event) registers all collected commands with Forge's command manager.
CommandRegistry usage
// Typically in your @Mod class:

@EventHandler
public void onServerStarting(FMLServerStartingEvent event) {
    CommandRegistry.getInstance().onServerStarting(event);
}

// You can also query registered commands:
CommandRegistry registry = CommandRegistry.getInstance();
int count = registry.getCommandCount();
boolean exists = registry.hasCommand("mycommand");
Registration order does not matter

You can call EriCommand.create(...).register() at any time before onServerStarting. The registry collects them and registers them all at once.

Full Example

Here is a complete, real-world example demonstrating most features of the Command Framework: sub-commands, multiple argument types, permissions, cooldowns, and custom suggestions.

CommandShop.java — Full example
import fr.eri.eriapi.command.EriCommand;
import fr.eri.eriapi.command.arg.*;

public class CommandShop {

    public enum Category { WEAPONS, ARMOR, TOOLS, FOOD, BLOCKS }

    public static void register() {
        EriCommand.create("shop")
            .description("In-game item shop")
            .rootAliases("store", "market")

            // /shop buy <item> [amount]
            .sub("buy")
                .description("Buy an item from the shop")
                .cooldown(40) // 2 second cooldown
                .arg(ItemArg.of("item"))
                .arg(IntArg.of("amount").range(1, 64).optional(1))
                .runs(ctx -> {
                    Item item = ctx.getItem("item");
                    int amount = ctx.getInt("amount");
                    EntityPlayerMP player = ctx.getSenderPlayer();

                    // Purchase logic...
                    ctx.reply("Bought " + amount + "x " + item.getRegistryName());
                })
            .end()

            // /shop sell
            .sub("sell")
                .description("Sell the item in your hand")
                .runs(ctx -> {
                    EntityPlayerMP player = ctx.getSenderPlayer();
                    ItemStack held = player.getHeldItemMainhand();
                    if (held.isEmpty()) {
                        ctx.replyError("You must hold an item to sell!");
                        return;
                    }
                    ctx.reply("Sold " + held.getDisplayName());
                })
            .end()

            // /shop browse <category>
            .sub("browse")
                .description("Browse items by category")
                .arg(EnumArg.of("category", Category.class))
                .runs(ctx -> {
                    Category cat = ctx.getEnum("category", Category.class);
                    ctx.reply("Browsing " + cat.name() + " category...");
                })
            .end()

            // /shop admin set-price <item> <price> (op only)
            .sub("admin")
                .permission(2)
                .description("Shop administration")

                .sub("set-price")
                    .description("Set the price of an item")
                    .arg(ItemArg.of("item"))
                    .arg(FloatArg.of("price").range(0, 10000))
                    .runs(ctx -> {
                        Item item = ctx.getItem("item");
                        float price = ctx.getFloat("price");
                        ctx.reply("Price of " + item.getRegistryName() + " set to " + price);
                    })
                .end()

                .sub("reload")
                    .description("Reload shop configuration")
                    .permission(4)
                    .runs(ctx -> ctx.reply("Shop config reloaded."))
                .end()
            .end()

            .register();
    }
}

This single registration creates the following command tree:

/shop (or /store, /market)
  ├── buy <item> [amount] — Buy an item (2s cooldown)
  ├── sellSell held item
  ├── browse <category> — Browse by category
  ├── admin (op only)
  │   ├── set-price <item> <price>
  │   └── reload (server only)
  └── helpAuto-generated
Ready to build?

The Command Framework is designed to let you focus on what your commands do, not on boilerplate parsing and validation. Define your structure, write your logic, and let EriAPI handle the rest.