Skip to content

v5 KamiCommand

Jake Moore edited this page Aug 30, 2026 · 5 revisions

KamiCommand

⚠️ Usage ⚠️

Available in spigot-utils and its inheritors (spigot-jar).

A command framework with typed parameters, requirements and automatic help. Each command in your tree is a class extending KamiCommand, configured in its constructor.

We acknowledge MassiveCraft's MassiveCore as the original source and inspiration for this system.

A complete command

public class CmdHome extends KamiCommand {
    public CmdHome() {
        addAliases("home");
        addParameter(Parameter.of(TypeString.get()).name("home"));                 // required
        addParameter(Parameter.of(TypeBooleanYes.get()).name("silent")
                .defaultValue(false, "no"));                                       // optional
        addRequirements(RequirementHasPerm.get("myplugin.home"));
        addRequirements(RequirementIsPlayer.get());
        setDesc("Teleport to one of your homes");
        addChild(new CmdHomeSet());
    }

    @Override
    public void perform(@NotNull CommandContext context) throws KamiCommonException {
        Player player = Objects.requireNonNull(context.getMe());
        String home = readArg();
        boolean silent = readArg();
        // ...
    }
}

Register it in your plugin's enable:

new CmdHome().registerCommand(this);
KamiCommonCommandRegistration.updateRegistrations();   // required after server start

and unregister in disable with command.unregisterCommand().

Only root commands can be registered. Registering a child throws IllegalStateException.

CommandContext

Everything about the invocation arrives in the perform parameter.

context.getSender()            // CommandSender
context.getMe()                // Player, or null when console
context.isSenderIsConsole()
context.getArgs()              // raw arguments
context.getLabel()             // the alias actually typed

Migrating from v4: execute(...) gained a String label parameter and is now final. Override perform, not execute. The CommandContext parameter on perform() is not new; it arrived in v4.

getLabel() is new in v5. It is used internally for the help title, so a command reached through two aliases renders the right title under each one. The page-flip arrows build their command from aliases rather than from the label.

Parameters

Parameters are declared in order and read in order. readArg() returns the next one, already parsed to the right type.

addParameter(Parameter.of(TypeInteger.get()).name("page").defaultValue(1));

Builder methods are fluent, with no set prefix:

method meaning
.name(String) shown in the usage template. Defaults to the type's name
.defaultValue(T) makes the parameter optional
.defaultValue(T, String desc) as above, with a description shown in help
.requiredFromConsole(boolean) required when the sender is console, optional otherwise
.concatFromHere(boolean) swallow all remaining arguments into this parameter

There is no required() flag: a parameter is required exactly when it has no default value. A default of null is legal and different from having no default.

Ordering is enforced. A required parameter after an optional one throws IllegalArgumentException, and anything after a concatenating parameter throws IllegalStateException.

Migrating from v3: the many addParameter(...) overloads collapsed into the builder. this.addParameter(1, TypeInteger.get(), "page") becomes addParameter(Parameter.of(TypeInteger.get()).name("page").defaultValue(1)). v4 already had the two builder overloads, so a v4 caller needs no change.

Types

A Type<T> turns a string argument into a Java object and supplies tab completions. These ship:

Primitives. TypeString, TypeStringConfirmation, TypeByte, TypeShort, TypeInteger, TypeLong, TypeFloat, TypeDouble, TypeBooleanTrue (true/false), TypeBooleanOn (on/off), TypeBooleanYes (yes/no)

Senders. TypePlayer, TypeOfflinePlayer, TypeSender

Bukkit. TypeWorld, TypeMaterial, TypeXMaterial, TypeGameMode, TypeEnchantment, TypePotionEffectType, TypePermission, TypeColor, TypeNamespacedKey

Other. TypeRange, TypeDate, TypeTimeZone, TypeUUID

All are singletons reached with get(). TypeRange.get(min, max), TypeMaterial.get(exclusions...) and TypeXMaterial.get(exclusions...) take arguments. TypePlayer hides vanished players.

Writing your own

Extend TypeAbstract<T> and implement two methods:

public class TypeArena extends TypeAbstract<Arena> {
    private static final TypeArena i = new TypeArena();
    public TypeArena() { super(Arena.class); }
    public static TypeArena get() { return i; }

    @Override
    public Arena read(String str, CommandSender sender) throws KamiCommonException {
        Arena arena = ArenaManager.get(str);
        if (arena == null) {
            throw new KamiCommonException().addMsgFromMiniMessage(
                    "<red>No arena named <light_purple>" + str + "<red>.");
        }
        return arena;
    }

    @Override
    public Collection<String> getTabList(CommandSender sender, String arg) {
        return ArenaManager.names().stream()
                .filter(n -> n.toLowerCase().startsWith(arg.toLowerCase()))
                .limit(20)
                .collect(Collectors.toList());
    }
}

Throwing KamiCommonException is the correct way to reject input. The framework catches it and sends your message to the player. Never return null.

Tab completions are prefix-filtered for you. Override shouldShowAllTabCompletions() to return true if you have already filtered them yourself.

Migrating from v4: KamiCommonException.addMsg(String) is deprecated. Use addMsgFromMiniMessage(String), addMsgFromLegacyColors(String) or addMsg(VersionedComponent). getKMessage() is now getComponent(). On the abstract types, extractErrorMessage is now extractErrorMessageMini. The old name still compiles as a new method and is never called.

Requirements

Requirements gate a command. All of them must pass before perform runs, and a failing one sends its own error message.

Built in: RequirementHasPerm.get(permission), RequirementIsPlayer.get(), RequirementIsntPlayer.get(), RequirementHasItemInHand.get(materials...).

public class RequirementInWorld extends RequirementAbstract {
    private final String world;
    public RequirementInWorld(String world) { this.world = world; }
    public static RequirementInWorld get(String world) { return new RequirementInWorld(world); }

    @Override
    public boolean apply(CommandSender sender, KamiCommand command) {
        return sender instanceof Player && ((Player) sender).getWorld().getName().equals(world);
    }

    @Override
    public @NotNull VersionedComponent createErrorMessage(CommandSender sender, KamiCommand command) {
        return NmsAPI.getVersionedComponentSerializer()
                .fromMiniMessage("<red>You must be in " + world + ".");
    }
}

Children and help

Add children with addChild(...). The first child you add also causes a help command to be inserted automatically at index 0, so /home ?, /home h and /home help work without you writing anything.

A command with children and no perform override shows that help by default. Help titles come from the alias the player actually typed. Set extra lines above it with setHelpComments(...), and control ordering with KamiCommandHelp.Config.setSortHelpCommands(true).

Hide a command from help with setVisibility(...): VISIBLE, SECRET or INVISIBLE.

Bukkit permissions

New in v5. The permission handed to Bukkit controls whether the command appears in root tab completion at all, which the old requirement check could not do.

setBukkitCommandPermission("myplugin.home");

If you do not call it, the permission is derived from your requirements: the first RequirementHasPerm in the list wins.

⚠️ With more than one RequirementHasPerm on a command, derivation picks the first by declaration order, not the most specific, and logs a warning. Reordering two addRequirements lines would change the Bukkit permission. Set it explicitly whenever a command has several permission requirements.

Only root commands are affected. A command with no RequirementHasPerm and no explicit call gets no Bukkit permission.

Paging long output

List<VersionedComponent> lines = /* ... */;
CommandPaging.getPage(this, lines, page, "Arenas").forEach(line -> line.sendTo(sender));

Page height defaults to 9 for players and 50 for console; pass an explicit height with the five-argument overload. Call it synchronously inside perform, because it reads the live CommandContext.

Clickable next and previous arrows appear automatically if your command has a parameter literally named "page" and every parameter before it has a default value.

Migrating from v4: getPage moved off Txt and the argument order changed. Txt.titleizedPageTitle still exists, but lost its KamiCommand and List<String> parameters and no longer renders page arrows. Txt.titleizedPageTitle(title, pageCount, pageNum, ...) is now CommandPaging.titleizedPageTitle(command, title, pageNum, pageCount). pageNum and pageCount are transposed. The arity dropped from five to four and command moved to the front, so a blind port will not compile, but a port that preserves v4's ordering will, and silently reverses your page counter.

Configuring the messages

KamiCommand.Config holds every string and colour the framework emits: command and parameter colours, the "you should use the command like this" line, ambiguous and missing sub-command messages, and the permission-denied message. KamiCommandHelp.Config covers the help title, CommandPaging.Config the pagination arrows and title, and TypeAbstractChoice.Config the "no match" and "ambiguous" errors.

All values are MiniMessage. Accessors that hold MiniMessage text carry a Mini suffix, so KamiCommand.Config is uniformly suffixed while the others are mixed. KamiCommandHelp.Config has plain setHelpTitleFormat, and CommandPaging.Config has both setBackIcon and setActiveIconColorMini.

KamiCommand.Config.setErrorColorMini("<dark_red>");
KamiCommandHelp.Config.setHelpTitleFormat("Commands for {TITLE}");

Migrating from v4: the Lang class was removed; these config classes replace it. Placeholders that were literal substrings are now MiniMessage tags: {REPLACEMENT} is now <replacement>, and {prevPage} / {nextPage} are now <prev_page> / <next_page>. A custom format still using the old literals renders them as visible text and loses the click actions.

Clone this wiki locally