命令服务 LinCommand 用于声明命令结构、解析参数、检查权限与执行者,并把已经解析的参数交给业务代码。Bukkit Runtime 负责把命令服务绑定到当前插件声明的 PluginCommand,Core 负责注册表、参数解析、帮助信息和执行调度。
通过 Linlang Facade 取得命令服务:
LinCommand commands = lin.linCommand();
Linlang 提供两种注册方式:
两种注册方式共用一套命令系统。描述语句都会被解析为命令节点,写入当前 Facade 的命令注册表。
命令服务不会动态创建 Bukkit 根命令。插件必须先在自己的 plugin.yml 中声明根命令:
commands:
rs:
description: RainbowEdit command
usage: /<command> help
注册第一条完整命令或调用 commands.root("rs") 时,Bukkit Runtime 会通过当前插件取得 rs 对应的 PluginCommand,再安装执行器和补全器。根命令没有声明时,注册会失败,并报告:
LIN-COMMAND-BUKKIT-BIND-FAIL
命令只能绑定当前插件拥有的 PluginCommand,不会接管其他插件的同名命令。
同一个 LinCommand 实例只允许一个根命令。第一条命令使用 rs 后,再注册以其他根字面量开始的命令会抛出 IllegalArgumentException。子命令数量不受此限制。
下面是一条完整的逐条注册命令:
import static api.linlang.command.CommandOptions.options;
commands.register(
"rs reload",
ctx -> reloadPlugin(),
options()
.permission("rainbowedit.reload")
.i18n(lang.command.reload.description)
);
CommandOptions 把权限、执行位置和国际化文字集中在一个具名选项中。执行位置默认为 ALL,因此普通命令不需要重复声明。旧的分项参数注册方法仍然兼容。
玩家或控制台输入 /rs reload 后,命令服务依次完成:
具体注册参数请见:注册命令 ⇱。规范字符串的完整规则请见:命令描述语句 ⇱,类型要求与返回值请见:命令参数类型 ⇱。
每个命令服务都提供帮助和运行信息入口:
/<root> help [page]
/<root> ? [page]
/<root> info
help 与 ? 命令显示当前注册表中的帮助页,二者功能相同。page 从 1 开始,省略或无法解析时显示第一页,超出总页数时会限制到最后一页。帮助页当前每页显示 8 条命令。
帮助页会按发送者的权限筛选命令。发送者必须满足某条命令的全部权限要求,才能在帮助中看到它。执行位置不匹配的命令仍会显示,但整行使用灰色,表示当前发送者可以了解该命令,却不能在当前位置执行。例如控制台查看仅玩家可用的命令时会看到灰色条目。
每条帮助项包含完整用法和命令描述。参数显示名优先解析 CommandOptions 中的语言引用或兼容的延迟语言引用,再回退到规范中的内联说明和稳定参数名。命令描述与帮助页标题、图例、分页文字都会跟随 Facade 当前语言重新解析。
当帮助超过一页时,玩家可以点击上一页或下一页文字执行对应分页命令,并可悬停查看提示。控制台会获得普通文本以及可手动执行的分页命令。
info 命令显示当前 Facade 所属插件的信息,包括:
内建命令标题会使用当前总前缀,字段名称和标题跟随命令内建语言。这里读取的是 Bukkit 插件描述,因此名称、作者和版本来自当前插件的 plugin.yml。
help、? 与 info 由调度入口优先处理,不应再作为普通一级子命令注册。即使注册同名字面量,内建入口也会先接管输入。
这些内建入口本身没有额外的 Linlang 权限节点。Bukkit 在 plugin.yml 中为根命令声明的权限仍可能阻止发送者进入命令服务;进入以后,help 再按照每条叶子的权限决定可见内容。
命令服务还自动为命令提供 Tab 补全。补全同样会过滤当前发送者不可执行的命令。多层字面量会按当前位置补全,例如:
/rs control rename item
命令会这样补全:
/rs control <Tab> -> rename
/rs control rename <Tab> -> item
进入参数位置以后,命令服务先请求对应 TypeResolver 提供候选。玩家、枚举、布尔值和物品类型等解析器可以给出能够直接输入的真实值,因此这些值具有最高优先级。
如果解析器没有提供候选,并且当前参数还没有开始输入,命令服务会显示这个参数在当前语言下的说明。说明依次来自 CommandOptions.label(...)、兼容的旧标签和描述语句中的内联说明;这些位置都没有说明时,直接显示参数名。语言字段重载后,下一次 Tab 补全会重新读取引用,不需要重新注册命令。
参数说明是一项输入提示,不代表可直接执行的参数值。用户开始输入以后,命令服务不会继续用说明覆盖当前内容。固定字面量仍按照当前输入前缀补全,解析前置参数失败时也不会继续提供后续参数候选。
help 和 info 会作为根位置的补全候选,help 还会补全可用页码。? 是可执行别名,但当前不会作为 Tab 候选主动给出。
命令服务统一处理进入业务执行器之前的常见失败:
ExecTarget 不匹配。参数错误会同时显示最可能匹配的命令用法。权限和执行位置在参数解析前检查,因此无权限发送者不会触发交互等待或复杂参数查询。
未知命令有且只有一个可信候选时,错误消息下方还会显示修正后的完整命令。建议只修正一个固定字面量,并保留用户已经输入的参数。例如输入 /rs control renmae Sword 时,可以建议 /rs control rename Sword。
建议首先复用 Tab 补全的分支判断。如果当前 token 是某个字面量的前缀,并且最终只有一个可执行分支,就可以补全该分支。因此,在没有其他 h 分支时,/root h 可以建议 /root help;只有一个 re 分支时,/root re 可以建议 /root restart。若同时存在 reload 和 restart,re 仍然不会产生建议。
无法按前缀匹配时,再使用字符距离处理轻微拼写错误。较长字面量允许至多两次字符插入、删除、替换或相邻交换,短字面量只允许一次。内建的 help 和 info 也参与建议。
对于已经匹配完整字面量的命令,参数解析器会寻找最长的有效参数前缀,并裁掉多余或无效的尾部输入。例如 /root restart s 可以建议 /root restart,而 /root rename Sword true extra 可以保留已解析的 Sword true。字面量拼写错误和尾部多余参数也可以同时修正,例如 /root restarts s 建议 /root restart。
候选仍必须满足发送者的权限和执行位置要求,以免泄露无权查看的命令或建议当前位置无法执行的命令。两个以上字面量同时错误,或存在多个合理分支时不会给出建议。算法不会改写已经输入的玩家名、数字、物品等业务参数,只会保留能够被对应 TypeResolver 成功解析的参数前缀。
这些默认反馈、帮助标题和内建提示都由 CommandMessages 提供,并经过 Bukkit 命令桥渲染 & 与 § 颜色代码。错误消息以及帮助、信息入口的标题会应用当前总前缀,其余帮助条目保持对齐排版。您可以在 YourPlugin/linlang/lincommand/message 目录中修改内建消息的内容。
命令描述和参数显示名可以直接引用语言对象中的 LangText:
CommandOptions commandOptions = CommandOptions.options()
.i18n(
lang.command.renameItem.description,
"id", lang.command.renameItem.id
);
命令规范中的参数名保持稳定,语言引用只改变帮助和用法中的展示文本。语言文件在同一个语言服务内重载后,命令帮助会在渲染时重新解析这些引用,不需要把文本提前转换成 String。
详细用法请见:语言引用 ⇱。
命令框架自己的未知命令、权限不足和参数错误等提示由 CommandMessages 提供,并跟随 Facade 的命令语言。业务代码需要发送消息时,推荐使用 消息服务 ⇱,不要直接依赖命令框架的内部消息键。
普通重载和语言切换均保留 LinCommand 实例及注册表。命令描述与参数标签通过 LangText 或延迟函数取值,使用新语言时不需要重新注册命令。
从旧版升级时,应删除参数应用后重新注册全部命令的代码,否则会遇到重复签名。把需要重新计算的业务缓存放入 lin.onReload("名称", 回调),不要在回调中重复初始化服务。
插件关闭时,Facade 关闭交互监听,并在 Bukkit 命令仍由当前实例持有时解除执行器和补全器绑定,不会覆盖后来由其他代码安装的处理器。已关闭服务不再接受注册和执行。详见重载机制 ⇱。
flowchart TD
Direct["逐条 register"] --> Parse["解析命令规范"]
Group["CommandGroup 相对注册"] --> Expand["展开公共路径与权限"]
Expand --> Parse
Parse --> Registry["写入统一命令注册表"]
Registry --> Bind["绑定当前插件的根命令"]
Input["Bukkit 命令输入"] --> Match["匹配字面量与参数形状"]
Bind --> Match
Match --> Guard["检查目标与权限"]
Guard --> Args["解析参数"]
Args --> Execute["执行叶子处理器"]
Execute --> Callback["通知命令组结果回调"]
讨论