未发布版本 v6.3.0-SNAPSHOT。 本页内容来自 alpha 分支,随时可能变更,不属于任何已发布版本。

commit 49619c6 · 注入于 2026-09-13 12:21 UTC

Skip to content

命令执行器

在传统的 Bukkit 插件开发中,我们通常会使用 Bukkit 的 CommandExecutor 接口来处理命令。

但在某些情况下,我们需要判断命令的发送者是否为玩家,是否拥有某些权限,判断参数等等。

如果一个插件存在多个命令,那么这些判断逻辑就会重复出现在每个命令的处理方法中,这样的代码是非常冗余的。

除此之外,我们也可能还需要处理命令错误,输出帮助信息等等。

UltiTools-API 对原生的 CommandExecutor 接口进行了封装,提供了一个更加简洁的命令处理方式。

创建命令执行器

从 v6.2.0 开始,你应该继承 BaseCommandExecutor 类,并重写 handleHelp 方法。这里的 @CmdTarget@CmdExecutor 注解是代表了该命令的目标类型和执行器信息。

v6.3.0 起已移除

AbstractCommandExecutor(以及空壳 AbstractCommendExecutor)自 v6.2.0 起已弃用,并在 v6.3.0 中被彻底删除——不是不可达,而是这个类在 jar 里已经不存在了。请使用 BaseCommandExecutor,它提供了相同的注解驱动功能,同时支持可插拔的验证链、改进的上下文管理和自定义类型解析器支持。完整迁移指南见 COMPATIBILITY.mdMigrating off AbstractCommandExecutor(该文件为英文,是仓库的权威版本对照文档)一节。

/命令 help 现在与其他调用一样受同一套校验

v6.3.0 之前,help 子命令会在校验链之前执行,导致在 @CmdTarget(PLAYER) 命令上用控制台调用、或发送者缺少所需权限时,handleHelp 仍然会被调用。v6.3.0 起,handleGatedHelp 会先跑 SenderTypeValidatorPermissionValidator,发送者类型不匹配或权限缺失都会在你的 handleHelp 实现被调用之前被拒绝。

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.command.BaseCommandExecutor;
import com.ultikits.ultitools.annotations.command.CmdExecutor;
import com.ultikits.ultitools.annotations.command.CmdTarget;
import org.bukkit.command.CommandSender;

// Command limits executor
@CmdTarget(CmdTarget.CmdTargetType.BOTH)
@CmdExecutor(
        // Command permission (optional)
        permission = "ultikits.example.all",
        // Command description (optional)
        description = "Test command",
        // Command alias
        alias = {"test", "ts"},
        // Whether to register manually (optional)
        manualRegister = false,
        // Whether to require OP permission (optional)
        requireOp = false
)
public class ExampleCommand extends BaseCommandExecutor {

    @Override
    protected void handleHelp(CommandSender sender) {
        // Send help message to command sender
    }
}

这样你就完成了一个空的什么都不做的命令执行器。这里的 @CmdTarget@CmdExecutor 注解是代表了该命令的发送者类型和执行器信息。我们将在下一节详细介绍这两个注解。

注册命令

六参数连接器构造器已标记为待移除

下面的示例调用的是六参数 UltiToolsPlugin 构造器,它带有 @Deprecated(since = "6.0.8", forRemoval = true) 并把资源目录路径写死,因此每次编译都会产生一条移除警告。 改用外部插件 API,在你自己的 JavaPlugin 里调用 UltiToolsAPI.connect,或者保留连接器、改调七参数构造器并自行传入 resourceFolderPath:这两种写法在 v6.2.5 上都可用。 连接器的替代签名仍在 issue #217 中讨论,移除动作本身跟踪于 issue #213

和spigot开发一样,有了执行器,就需要去注册它。我们可以在 registerSelf 方法中使用 getCommandManager().register() 方法来注册命令。

如果你的模块存在大量的命令执行器而不想手动注册,也可以使用 UltiTools 提供的自动注册功能,详情可以查看这篇文章

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.UltiToolsPlugin;

import java.io.IOException;
import java.util.List;

public class UltiToolsConnector extends UltiToolsPlugin {

    public UltiToolsConnector(String pluginName, String version, List<String> authors, List<String> loadAfter, int minUltiToolsVersion, String mainClass) {
        super(pluginName, version, authors, loadAfter, minUltiToolsVersion, mainClass);
    }

    @Override
    public boolean registerSelf() throws IOException {
        // register command
        getCommandManager().register(this, ExampleCommand.class);
        return true;
    }

    @Override
    public void unregisterSelf() {

    }

    @Override
    public void reloadSelf() {
        super.reloadSelf();
    }
}

基于映射的命令执行器

快速上手

假如你的插件拥有一个设置传送点的功能,你希望玩家输入一个带有传送点名称的命令,以此来设立一个传送点。

那么这个命令应该会长这样:/point add name

如果是使用传统的方法,你需要判断参数输入的合法性,发送者以及权限等,如果还有其他功能,你还需要编写一大堆的 switch ... caseif ... else 语句,疯狂嵌套。 使得代码可读性变差,提高了维护的难度。(还容易烧干你的脑子)

使用这个方法,你只需要编写最主要的逻辑即可,剩下的交给 UltiTools。

首先你需要创建一个继承了 BaseCommandExecutor 的执行器类。

接着创建一个名为 addPoint 的方法,并添加你想要的参数:

java
public void addPoint(@CmdSender Player player, String name) {
  ...
}

是的,你的每一个功能都使用一个独立的函数,没有多余的判断。

如果你希望拿到的是 Player 对象而不是 CommandSender 对象,那你拿到的就是 Player,完全不需要判断与转换。你只需要在希望获取发送者对象的参数前面添加 @CmdSender 注解即可。

然后,你只需要添加 @CmdMapping 注解,以便 UltiTools 能够根据输入的命令匹配你的方法:

java
@CmdMapping(format = "add <name>")
public void addPoint(@CmdSender Player player, String name) {
  ...
}

最后,使用 @CmdParam 来绑定命令参数:

java
@CmdMapping(format = "add <name>")
public void addPoint(@CmdSender Player player, @CmdParam("name") String name) {
  ...
}

至此,你只需要注册命令执行器即可完成所有工作。

参数Tab提示补全

BaseCommandExecutor 的 Tab 补全已接线

自 v6.3.0 起,suggest(Player, Command, String[]) 通过共享的 commands/tabcomplete/ 分发实现,既解析 @CmdMapping format 首段的第一参数字面量,也解析参数位上 @CmdParamsuggest 属性——下面几节说明完整的解析顺序。你仍然可以重写 suggest(...) 来自定义行为,该方法保持 protected

每次写完一个命令之后希望给自己的命令添加Tab提示补全,但是又不想写一大堆的代码?

为Tab补全绞尽脑汁判断每个命令的长度和之前的参数来生成一个补全List,这非常容易把人累死。

现在你只需要简单的为每一个参数写一个方法返回补全List即可!这个方法可以被反复利用,所有繁杂的参数数量判断都交给 UltiTools 来完成。

你所需要做的只是在 @CmdParam 注解中添加 suggest 属性,指定一个方法名即可。

java
@CmdMapping(format = "add <name>")
public void addPoint(@CmdSender Player player, @CmdParam(value = "name", suggest="listName") String name) {
  ...
}

public List<String> listName(Player player, Command command, String[] args) {
  ...
}

UltiTools会首先在当前类中搜索匹配的方法名,并尝试调用此方法。

你的方法可以包含最多三个参数,分别对应的类型是 PlayerCommandString[],你可以选择任意的参数数量和顺序,但是类型只能是这三种,每种类型一个参数。

Player 代表了发送此命令的玩家,Command 代表了当前的命令,String[] 代表了当前命令的参数。

你的方法需要返回一个 List<String> 类型的值,UltiTools 将会将此值作为补全列表返回给玩家。

内置补全器(@key 记法)

自 v6.3.0 起,以 @ 开头的 suggest 值会通过一个已注册的补全器解析,而不是当作方法名——完全不需要写方法:

java
@CmdMapping(format = "tp <target>")
public void tp(@CmdSender Player player, @CmdParam(value = "target", suggest = "@players") String target) {
  ...
}

框架内置四个键:@players(在线玩家)、@worlds(已加载世界)、@materials(也支持 @blocks/@items)、@boolean(也支持 @toggle)。模块也可以通过 TabCompletionManager.register(String, TabCompleter) 在运行时注册自己的键。

@ 不是合法的 Java 标识符起始字符,因此这种记法永远不会与方法名值冲突——本页其他用普通方法名的示例都不需要改动。未知@key 会拒绝声明它的模块加载,并指明类、方法与键;它不会退回到下面介绍的纯字符串提示行为。

TIP

如果你仅仅只是想返回一个简单的提示字符串,那么你只需要在 suggest 字段中写上你想要的字符串即可。这里的字符串也支持i18n国际化。

java
@CmdMapping(format = "add <name>")
public void addPoint(@CmdSender Player player, 
                     @CmdParam(value = "name", suggest="[名称]") String name) {
  ...
}

TIP

如果你对UltiTools生成的补全列表不满意,你可以重写 suggest 方法,自己生成补全列表。

java
@Override
protected List<String> suggest(Player player, Command command, String[] strings) {
    ...
}

@CmdSuggest 注解

@CmdSuggest 在 BaseCommandExecutor 上已可读取

自 v6.3.0 起,下面示例中的类会通过共享的 commands/tabcomplete/ 分发被读取,因此 PointSuggest 中的方法会按预期被查找并调用。

如果你希望你的这个补全方法与其他命令类共享,那么你可以创建一个类,将想要复用的方法写在此类下。

在需要使用此类中的方法的类上添加 @CmdSuggest 注解,指定此类的类名即可。

java
@CmdSuggest({PointSuggest.class})
public class PointCommand extends BaseCommandExecutor {
    
    @CmdMapping(format = "add <name>")
    public void addPoint(@CmdSender Player player, @CmdParam(value = "name", suggest="listName") String name) {
        ...
    }
}
java
public class PointSuggest {
    public List<String> listName(Player player, Command command, String[] args) {
        ...
    }
}

参数

无参命令

如果该命令无需任何参数,那么只需要将 format 值留空即可

java
@CmdMapping(format="")

该种命令最多存在一个

不定参数

在一个方法的最后一个参数,允许使用数组类型,你需要在 format 中的最后一个参数添加 ..., 下面是一个示例:

java
@CmdMapping(format = "add <name...>")
public void addPoint(@CmdSender Player player, @CmdParam("name") String[] name) {
  ...
}

这样在玩家输入 /somecmd add aa bb cc 时,name 就为 ['aa', 'bb', 'cc']

类型解析

在对方法进行传参之前,UltiTools 会根据方法所需参数的类型对命令的可变参数进行转换。

所有的解析器被储存在一个名为 parsers 的 Map 中,你可以使用 getParser() 获取。

对于部分类型,BaseCommandExecutor 通过 TypeParserRegistry.getInstance() 提供了默认的解析器(包括基类与数组):

  • String (Java 内建)
  • Float (Java 内建)
  • Double (Java 内建)
  • Integer (Java 内建)
  • Short (Java 内建)
  • Byte (Java 内建)
  • Long (Java 内建)
  • OfflinePlayer (Bukkit API)
  • Player (Bukkit API)
  • Material (Bukkit API)
  • UUID (Java 内建)
  • Boolean (Java 内建)

如果你希望使用自定义的解析器,那么你需要创建一个可以使用 Function 接口的方法。

支持的解析器类型为 <String, ?>, 即方法有且仅有一个 String 类型的参数,并返回一个任意类型的值。

java
public static SomeType toSomeType(String s) {
  //do something...
  return result;
}

权限

方法权限

如果你需要为某一个方法指定权限,你只需要在 @CmdMapping 添加 permission 属性即可

java
@CmdMapping(..., permission = "point.set.add")

TIP

@CmdExecutor@CmdMapping 中定义的权限是叠加的——两者会被独立检查。命令发送者必须同时拥有类级别 @CmdExecutor(permission=...) 和方法级别 @CmdMapping(permission=...) 指定的权限才能执行该命令。

OP 限定

如果你希望全部方法只能由OP执行,你只需要在 @CmdExecutor 中设置 requireOp 属性为 true 即可

java
@CmdExecutor(..., requireOp = true)

如果你希望某一个方法只能由OP执行,你只需要在 @CmdMapping 中设置 requireOp 属性为 true 即可

java
@CmdMapping(..., requireOp = true)

限定发送者

如果你希望为全部方法指定发送者,你只需要在你的类前面添加 @CmdTarget 注解即可。

如果为某一个方法,则在该方法前面添加即可。

java
@CmdTarget(CmdTarget.CmdTargetType.BOTH)

方法级注解会替换类级注解

方法级 @CmdTarget 会完全覆盖类级 @CmdTarget,而不是要求两者都满足。 类上标 PLAYER、方法上标 BOTH 时,控制台可以执行该方法。 恢复交集语义(收窄而非覆盖)的诉求见 issue #320

异步执行

如果一个命令需要执行比较耗时的任务,你需要在相应的方法前面添加 @RunAsync:

java
@CmdMapping(format = "list")
@RunAsync
public void listPoint(@CmdSender Player player) {
  //do query
}

这将会创建一个新的异步线程来执行该方法,避免在Bukkit主线程中执行而造成阻塞。

由于 Bukkit API 不允许被异步调用,因此如果你需要调用 Bukkit API,你需要新建一个同步执行的Task:

java
@CmdMapping(format = "list")
@RunAsync
public void listPoint(@CmdSender Player player) {
  //do query
  new BukkitRunnable() {
    @Override
      public void run() {
          //call bukkit api
      }
    }.runTask(PluginMain.getInstance());
  }

@RunAsync 方法体不得直接访问世界、实体、方块或区块状态。

这里的异步只留给纯 CPU 或 I/O 工作。异步方法体里任何对世界、实体、方块或区块的访问,都必须作为同步任务调度回主线程:像上面示例那样用 BukkitRunnable#runTask(...),或任何其他同步调度方法(如 runTaskLater(...));注解本身从不授予对这些 API 的安全访问。方法体只做这类状态访问的处理器根本没有理由标注它。

未标注 @RunAsync 的命令方法体并不会在调用线程上直接执行:框架本身会通过 runTask() 把它推迟一个 tick 执行。这个推迟只改变方法体何时开始,不改变它是否会阻塞:tick 到达后,整个方法体(包括其中任何耗时的 CPU 或 I/O 工作)依然会在主线程上同步执行。去掉 @RunAsync 只对本来就不做耗时工作的方法体是安全的,恢复的是其他命令同样使用的这套调度;如果方法体同时包含耗时工作与 Bukkit 访问,需要做的是把耗时部分留在异步线程,只把触碰 Bukkit 状态的那一段通过 runTask(...)(就像上面示例那样)调度回主线程,而不是简单去掉注解。Paper 也不会拦截异步方法体里全部对世界、实体、方块或区块的访问:它的异步操作检查只覆盖一部分不安全的操作,因此违反上面这条规则时,方法体可能在不安全或不一致的状态下继续运行,而不是直接崩溃;没有抛出异常并不能证明代码是安全的。对确实应该异步执行、且不需要更多控制的方法体,@RunAsync 仍然是正确的选择;如果它还需要处理中提示、超时或可配置的边界行为,就改用 @AsyncCommand

命令冷却

如果你不希望一个指令被大量执行而消耗服务器资源,那么你可以在相应的方法前面添加 @CmdCD:

java
@CmdCD(60)

参数类型为整数,单位为秒。

冷却结束之前执行该指令将会发送消息:操作频繁,请 %d 秒后再试%d 会替换为剩余秒数)。

此限制仅对玩家生效。

冷却在每一次执行尝试之后都会生效,不只是在执行成功之后。 即使映射的方法抛出异常,冷却依然会像正常返回一样开始计时——校验器不区分成功与失败。这是有意为之:报错的指令同样消耗服务器资源,而在紧密循环里反复重试一个报错指令,正是冷却机制要防止的模式。

校验链无法强制执行的 @CmdCD 现在会拒绝加载

自 v6.3.0 起,标注了 @CmdCD、而其校验链中缺少 CooldownValidator 的类或方法——最常见的情形是省略了它的自定义 ValidatorChain,见下方创建自定义验证器一节——会在插件加载时被拒绝,并指出问题类与方法。这关闭了此前「看似已声明,实则拦不住任何调用」的缺口。

执行锁

如果你希望一个命令只能被一条一条地执行,那么可以在相应的方法前面添加 @UsageLimit 注解:

java
@UsageLimit(ContainConsole = false, value = LimitType.SENDER)

其中 ContainConsole 为是否将限制应用于控制台,value 为限制类型。自 v6.3.0 起,ContainConsole 默认值为 true——除非像上面这样显式设为 false,否则控制台发送者现在也受此限制约束。

可用的类型有:

  • LimitType.SENDER 限制每个发送者每次只能有一条该指令在执行
  • LimitType.ALL 限制全服只能有一条该指令在执行
  • LimitType.NONE 不作限制

LimitType.SENDER 策略下,玩家在上一条该指令执行完毕之前重复执行前将会收到提示:请先等待上一条命令执行完毕!

@UsageLimit 现在真正实现了串行化

自 v6.3.0 起,获取即为门槛:锁在验证本身内部被获取,被拦截的发送者在方法执行前就会被拒绝;ALL 范围的锁只能由获取它的发送者释放,其他发送者的完成不再能释放它。与上面的 @CmdCD 一样,链中缺少 UsageLockValidator@UsageLimit(SENDER|ALL) 会在加载时拒绝,并指出问题类与方法;LimitType.NONE 不受此约束。

LimitType.ALL 策略下,玩家在服内上一条该指令执行完毕之前重复执行前将会收到提示:请先等待其他玩家发送的命令执行完毕!

命令上下文

CommandContext 是一个不可变的对象,它封装了命令调用的所有信息。它被传递给验证器,并在执行期间可用于访问命令元数据。

访问上下文信息

java
// 检查发送者是否是玩家
boolean isPlayer = context.isPlayer();

// 获取玩家(如果发送者不是玩家则返回 null)
Player player = context.getPlayer();

// 获取原始命令发送者
CommandSender sender = context.getSender();

// 获取命令及其别名
Command command = context.getCommand();
String alias = context.getAlias();

// 获取原始参数
String[] args = context.getRawArgs();
int argCount = context.getArgCount();
String firstArg = context.getArg(0);

// 按名称获取已解析的参数
String[] nameValues = context.getParam("name");
String singleValue = context.getParamValue("name");

// 获取匹配的方法和格式
Method method = context.getMatchedMethod();
String format = context.getMatchedFormat();

// 获取命令调用时间戳
long timestamp = context.getTimestamp();

命令验证链

验证链实现了责任链模式,允许你组合多个按顺序执行的验证器。内置验证器处理常见需求,如权限、发送者类型、冷却和执行锁。

内置验证器

SenderTypeValidator

验证命令发送者是否与预期的目标类型匹配(玩家、控制台或两者):

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.command.BaseCommandExecutor;
import com.ultikits.ultitools.annotations.command.CmdExecutor;
import com.ultikits.ultitools.annotations.command.CmdTarget;
import org.bukkit.command.CommandSender;

@CmdTarget(CmdTarget.CmdTargetType.PLAYER)
@CmdExecutor(alias = {"mycmd"})
public class PlayerOnlyCommand extends BaseCommandExecutor {
    // Automatically rejects console users

    @Override
    protected void handleHelp(CommandSender sender) {
        sender.sendMessage("/mycmd");
    }
}

PermissionValidator

验证发送者是否具有所需权限:

java
@CmdExecutor(
    alias = {"admin"},
    permission = "myadmin.use",  // 所有命令的基本权限
    requireOp = false
)
@CmdMapping(format = "reload", permission = "myadmin.reload")  // 方法特定权限
public void reload(@CmdSender CommandSender sender) {
    // 只有拥有 "myadmin.reload" 权限的用户才能执行此命令
}

CooldownValidator

使用 @CmdCD 管理每个玩家的命令冷却:

java
@CmdMapping(format = "expensive")
@CmdCD(30)  // 30 秒冷却
public void expensiveOperation(@CmdSender Player player) {
    // 执行昂贵的操作
    // 玩家必须等待 30 秒后才能再次执行
}

以编程方式访问冷却状态:

java
public void checkCooldown(UUID playerId, String methodKey) {
    long remaining = getCooldownValidator().getRemainingCooldown(playerId, methodKey);
    if (remaining > 0) {
        // 玩家仍在冷却中
    }
}

冷却校验器通过 getCooldownValidator() 获取,它是 BaseCommandExecutor 上的一个实例字段。每个命令执行器各持有自己的一份,因此该调用只能取到当前执行器的冷却状态。

UsageLockValidator

使用 @UsageLimit 防止并发执行:

java
@CmdMapping(format = "backup")
@UsageLimit(value = UsageLimit.LimitType.ALL)  // 仅限服务器执行一个
public void backup(@CmdSender CommandSender sender) {
    // 同时只有一个玩家可以运行此命令
}

@CmdMapping(format = "download")
@UsageLimit(value = UsageLimit.LimitType.SENDER)  // 每个玩家仅一个
public void download(@CmdSender Player player) {
    // 每个玩家同时只能运行一个
}

创建自定义验证器

实现 CommandValidator 来创建自定义验证逻辑:

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.command.CommandContext;
import com.ultikits.ultitools.abstracts.command.validation.CommandValidator;
import org.bukkit.entity.Player;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class WorldRestrictionValidator implements CommandValidator {

    private final Set<String> allowedWorlds = new HashSet<>();

    public WorldRestrictionValidator(String... worlds) {
        allowedWorlds.addAll(Arrays.asList(worlds));
    }

    @Override
    public ValidationResult validate(CommandContext context) {
        if (!context.isPlayer()) {
            return ValidationResult.success();
        }

        Player player = context.getPlayer();
        if (!allowedWorlds.contains(player.getWorld().getName())) {
            return ValidationResult.failure(
                "You can only use this command in: " + String.join(", ", allowedWorlds),
                "command.error.wrong_world"
            );
        }

        return ValidationResult.success();
    }

    @Override
    public int getOrder() {
        return 400;  // Execute after permission validators
    }

    @Override
    public String getName() {
        return "WorldRestrictionValidator";
    }
}

在你的命令执行器中注册验证器:

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.command.BaseCommandExecutor;
import org.bukkit.command.CommandSender;

public class ValidatorCommand extends BaseCommandExecutor {

    public ValidatorCommand() {
        super();
        addValidator(new WorldRestrictionValidator("world", "world_nether"));
    }

    @Override
    protected void handleHelp(CommandSender sender) { }
}

自定义验证链会替换掉全部四个默认验证器

ValidatorChain 传给 super(...) 会跳过 createDefaultValidatorChain(),因此 SenderTypeValidator(类上的 @CmdTarget)、PermissionValidatorrequireOp 与方法级 @CmdMapping 的权限,类级 permission 仍由 Bukkit 强制)、UsageLockValidator@UsageLimit)与 CooldownValidator@CmdCD)都不在链里,除非你自己把它们加进去。 改用上面那种写法:调用 super() 后用 addValidator(...) 注册自己的验证器,四个默认验证器保留,你的验证器按 getOrder() 排序。 自 v6.3.0 起,链中缺少匹配验证器的 @CmdCD/@UsageLimit 会在插件加载时被拒绝,而不再是静默记录状态却不强制执行——见上方命令冷却执行锁小节的提示。

或使用自定义验证链:

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.command.BaseCommandExecutor;
import com.ultikits.ultitools.abstracts.command.validation.ValidatorChain;
import com.ultikits.ultitools.abstracts.command.validation.validators.PermissionValidator;
import com.ultikits.ultitools.abstracts.command.validation.validators.SenderTypeValidator;
import org.bukkit.command.CommandSender;

public class ChainCommand extends BaseCommandExecutor {

    // Build the chain inside the constructor. A chain assigned to a local
    // variable outside the class is not reachable from super(...).
    public ChainCommand() {
        super(ValidatorChain.builder()
            .add(SenderTypeValidator.fromAnnotation(null))
            .add(new PermissionValidator("myadmin.use", false))
            .add(new WorldRestrictionValidator("world"))
            .build());
    }

    @Override
    protected void handleHelp(CommandSender sender) { }
}

验证器执行顺序

验证器按其 getOrder() 值的顺序执行(较低的值优先):

  1. 100 - SenderTypeValidator(确保正确的用户类型)
  2. 200 - PermissionValidator(检查权限)
  3. 250 - UsageLockValidator(防止并发执行)
  4. 300 - CooldownValidator(检查冷却状态)
  5. 400+ - 自定义验证器

异步命令

使用 @AsyncCommand 以异步方式执行命令而不阻塞服务器线程。它比 @RunAsync 提供更多配置选项:

java
@CmdMapping(format = "backup")
@AsyncCommand
public void backupWorld(@CmdSender Player player) {
    // 异步运行 - 适合 I/O 操作
    performBackupLogic();

    // 同步回主线程以进行 Bukkit 操作
    Bukkit.getScheduler().runTask(UltiTools.getInstance(), () -> {
        player.sendMessage("备份已完成!");
    });
}

异步命令选项

@AsyncCommand 的 timeout 现在真正生效

自 v6.3.0 起,timeout() 是框架"等待多久"的截止时间,不是对方法体的取消:配置的时长耗尽而命令体仍在运行时,框架停止等待并向发送者发送一条超时消息,但命令体本身永远不会被中断,会自行运行至完成。timeout = 0 会完全禁用该监视器——框架会无限期等待,永远不会报告超时。

java
@AsyncCommand(
    showProcessing = true,                      // 显示"处理中..."消息
    processingMessageKey = "command.backup.processing",  // 自定义 i18n 消息
    timeout = 60                                // 60 秒超时(0 = 无超时,不设置时默认 30 秒)
)
@CmdMapping(format = "backup")
public void backupWorld(@CmdSender Player player) {
    // 上述配置的作用:
    // - 执行时显示"处理中..."
    // - 使用自定义 i18n 键而不是默认值
}

自定义类型解析器

类型解析器将命令参数字符串转换为你的方法所需的类型。UltiTools 为原始类型、Bukkit 实体和数组提供了内置解析器。

内置解析器

  • 原始类型: String、Integer、Double、Float、Long、Short、Byte、Boolean
  • Bukkit 实体: Player、OfflinePlayer、Material、World
  • 其他类型: UUID、Location、GameMode、Enchantment
  • 数组: 上述所有类型都支持数组语法

创建自定义解析器

实现 TypeParser<T>

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.command.parser.TypeParseException;
import com.ultikits.ultitools.abstracts.command.parser.TypeParser;
import org.bukkit.Color;

import java.util.Arrays;
import java.util.List;

public class ColorParser implements TypeParser<Color> {

    @Override
    public Class<Color> getPrimaryType() {
        return Color.class;
    }

    @Override
    public List<Class<?>> getSupportedTypes() {
        return Arrays.asList(Color.class, Color[].class);
    }

    @Override
    public Color parse(String value) throws TypeParseException {
        try {
            // Parse hex color like "FF0000"
            int rgb = Integer.parseInt(value, 16);
            return Color.fromRGB(rgb);
        } catch (IllegalArgumentException e) {
            // Catch IllegalArgumentException, not NumberFormatException. Two
            // different failures land here and only one of them is a format
            // error: Integer.parseInt throws NumberFormatException on bad hex,
            // while Color.fromRGB throws plain IllegalArgumentException for a
            // value outside the low 24 bits (e.g. "1000000" parses fine, then
            // gets rejected). NumberFormatException extends
            // IllegalArgumentException, so catching the supertype covers both.
            // Catching only the subtype lets the range error escape as an
            // undeclared runtime exception instead of a TypeParseException.
            throw new TypeParseException(value, Color.class,
                "Invalid color. Use 6-digit hexadecimal RGB (e.g., FF0000)", e);
        }
    }

    @Override
    public int getPriority() {
        return 0;
    }
}

注册解析器:

java
@Autowired
private UltiToolsPlugin plugin;

@PostConstruct
public void init() {
    TypeParserRegistry.getInstance().register(new ColorParser());
}

在你的命令中使用:

java
@CmdMapping(format = "setcolor <color>")
public void setColor(@CmdSender Player player, @CmdParam("color") Color color) {
    // color 自动解析
}

支持数组的高级解析器:

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.command.parser.TypeParseException;
import com.ultikits.ultitools.abstracts.command.parser.TypeParser;

import java.util.Arrays;
import java.util.List;

public class RangeParser implements TypeParser<IntRange> {

    @Override
    public Class<IntRange> getPrimaryType() {
        return IntRange.class;
    }

    @Override
    public List<Class<?>> getSupportedTypes() {
        return Arrays.asList(IntRange.class, IntRange[].class);
    }

    @Override
    public IntRange parse(String value) throws TypeParseException {
        String[] parts = value.split("-");
        if (parts.length != 2) {
            throw new TypeParseException(value, IntRange.class,
                "Range format: min-max (e.g., 1-100)");
        }

        try {
            int min = Integer.parseInt(parts[0]);
            int max = Integer.parseInt(parts[1]);
            return new IntRange(min, max);
        } catch (NumberFormatException e) {
            throw new TypeParseException(value, IntRange.class,
                "Range bounds must be integers", e);
        }
    }
}
java
// 使用方式
@CmdMapping(format = "random <range>")
public void randomNumber(@CmdSender Player player,
                         @CmdParam("range") IntRange range) {
    int value = ThreadLocalRandom.current().nextInt(range.min, range.max + 1);
    player.sendMessage("随机值: " + value);
}

传统命令执行器

游戏内命令

如果你希望一个指令只能在游戏内使用(由玩家执行),那么可以继承 AbstractPlayerCommandExecutor 类,并重写 onPlayerCommand 方法。

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.AbstractPlayerCommandExecutor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;

public class PlayerCommandExample extends AbstractPlayerCommandExecutor {
    @Override
    protected boolean onPlayerCommand(Command command, String[] strings, Player player) {
        // your code
        return true;
    }

    // Required: sendHelpMessage is abstract in AbstractCommand.
    @Override
    protected void sendHelpMessage(CommandSender sender) {
        // send help message
    }
}

Player 类型的参数外,该方法与 CommandExecutor#onCommand 方法相同。

如果尝试在控制台执行该命令,将会收到一条错误消息:只有游戏内可以执行这个指令!

如果你希望该指令能够使用 Tab 补全,请看下一节。

命令补全

自Minecraft 1.13起,Bukkit API提供了一个新的 TabCompleter 接口,用于处理命令补全。

UltiTools 对该接口进行了封装,提供了一个更加简洁的命令补全方式。

你只需要继承 AbstractTabExecutor 类,并重写 onPlayerTabComplete 方法。

java
@Override
protected List<String> onPlayerTabComplete(Command command, String[] strings, Player player) {
    // 你的代码
    return null;
}

Player 类型的参数外,该方法与 TabCompleter#onTabComplete 方法相同。

其余的用法与 AbstractPlayerCommandExecutor 类相同。

控制台指令

如果你希望一个指令只能在控制台使用,那么可以继承 AbstractConsoleCommandExecutor 类,并重写 onConsoleCommand 方法。

java
package com.ultikits.docs.command;

import com.ultikits.ultitools.abstracts.AbstractConsoleCommandExecutor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;

public class ConsoleCommandExample extends AbstractConsoleCommandExecutor {
    @Override
    protected boolean onConsoleCommand(CommandSender commandSender, Command command, String[] strings) {
        // your code
        return true;
    }

    // Required: sendHelpMessage is abstract in AbstractCommand.
    @Override
    protected void sendHelpMessage(CommandSender sender) {
        // send help message
    }
}

该方法与 CommandExecutor#onCommand 方法相同。

如果尝试在游戏内执行该命令,将会收到一条错误消息:只可以在后台执行这个指令!

指令帮助

上述三个类都从 AbstractCommand 继承了 sendHelpMessage,它在那里声明为 protected abstract。 它不是框架替你提供的,每个子类都必须自己实现——上面两个示例里的那个 override 就是因为这个:

java
@Override
protected void sendHelpMessage(CommandSender sender) {
    // 你的代码,向玩家发送信息
}

当发送 /somecommand help 指令时,将会调用该方法。

错误处理

你可能会发现,上述三个类的 onCommand 方法都返回了一个 boolean 类型的值。

与原生的 CommandExecutor 接口相同,该值用于表示命令是否执行成功。

当命令执行返回 false 时,将自动向命令发送者提示信息。

贡献者

暂无相关贡献者

基于 MIT 许可发布