指令树
厌倦了手动拆分参数、解析指令?厌烦了复杂的判断条件?快来尝试 MCDR 的指令构建系统吧!
MCDR 内置了一个指令树构建系统,供插件构建其指令。它如同一个 Mojang 的 brigadier 的精简版
工作流程
MCDR 维护了一个 dict 用于储存注册的指令。该 dict 的值均为指令树根节点列表,而值对应的键则是根节点的字面值。有了它,MCDR 可以快速地找到可能可以接收到来指令的指令树
每次处理用户信息时,MCDR都会尝试将用户输入解析为指令。它将用户输入的第一个分段作为键来查询指令树存储字典。如果指令存在,则调用 cancel_send_to_server()
来阻止将信息发送到服务器的标准输入流 ,然后使用对应的指令树来处理该指令
如果解析指令时发生错误,且插件未将错误设置为已处理,则 MCDR 会将翻译后的指令错误消息发送到指令源
先瞅一眼…
让我们来看看指令树的实际含义。例如,假设某插件包含3种指令:
!!email list
!!email remove <email_id>
!!email send <player> <message>
要实现这些指令,我们可以构建如下所示的指令树:
Literal('!!email')
├─ Literal('list')
├─ Literal('remove')
│ └─ Integer('email_id')
└─ Literal('send')
└─ Text('player')
└─ GreedyText('message')
当执行 !!email remove 21
指令时,以下过程将会发生:
于节点
Literal('!!email')
解析指令!!email remove 21
字面量节点
Literal('!!email')
获取了!!email remove 21
的第一个元素,它是!!email
——与字面量节点匹配现在余下的指令是
remove 21
于是,它搜索其字面量子节点,找到与下一个指令元素
remove
匹配的子节点Literal('remove')
这样,它让该子节点处理其余指令
于节点
Literal('remove')
解析指令remove 21
字面量节点
Literal('remove')
获取了remove 21
的第一个元素,它是remove
——与字面量节点匹配现在余下的指令是
21
然后它搜索其字面量子节点,但未找到与下一个指令元素
21
匹配的任何字面量子节点因此,它让它的非字面量子节点
Integer('email_id')
处理剩余指令
于节点
Integer('email_id')
解析指令21
整数节点
Integer('email_id')
获得了21
的第一个元素,这是一个合法的整数它使用键
email_id
将值21
存储到上下文 dict 中然后,它发现指令解析已经完成,因此它以指令源和上下文 dict 作为参数来调用回调函数
至此,指令解析完成
以上是指令构建系统逻辑部分的快速概述,主要是为了帮助你建立对指令树和指令构建系统的感性理解
匹配文字节点,解析剩余指令,将解析后的值存储在上下文字典中,这就是指令系统的工作方式
构建指令树的方法
如果你熟悉 Mojang 在 Minecraft 中使用的指令树类库 brigadier,或者你需要使用 MCDR 指令树的完整特性,阅读相关的 类参考 以了解如何创建指令节点、增添子节点,以及设置节点属性
如果你是刚接触基于树的指令构建系统的新手,不知道如何整指令树的这一套东西,你可以试试 简易指令构建器 这个工具,来进行简单的指令树构建
除了阅读本文档外,学习使用 MCDR 指令构建系统的另一种好办法是引用和模仿现有代码。你可以在 mcdreforged.plugin.builtin.mcdreforged_plugin.MCDReforgedPlugin
类的 __register_commands
方法下找到 !!MCDR
指令的构建代码
上下文
上下文(Context)储存着当前指令解析过程中的信息,是一个继承自 dict 的类
指令解析过程中解析得到的值将会使用 dict 的方法,储存在上下文中。这意味着你可以使用 context['arg_name']
来访问这些值
简易指令构建器
在 v2.6.0 版本加入.
对指令树一头雾水?厌烦了基于树的指令构造方式?快来试试这个不含树的指令构建器,体验清晰简单的指令构建流程吧
声明&定义,你要做的就这些
用法
可以使用以下代码构造 先瞅一眼… 部分中的指令树:
from mcdreforged.api.command import SimpleCommandBuilder, Integer, Text, GreedyText
def on_load(server: PluginServerInterface, prev_module):
builder = SimpleCommandBuilder()
# declare your commands
builder.command('!!email list', list_email)
builder.command('!!email remove <email_id>', remove_email)
builder.command('!!email send <player> <message>', send_email)
# define your command nodes
builder.arg('email_id', Integer)
builder.arg('player', Text)
builder.arg('message', GreedyText)
# done, now register the commands to the server
builder.register(server)
其中 list_email
、remove_email
和 send_email
为对应指令的回调函数
就这么简单!
参见
类 SimpleCommandBuilder
的参考
自定义
MCDR 支持自定义参数节点。它也许能节省一些你为构建指令而重复工作的时间
要创建自定义参数节点,你需要声明一个继承自 AbstractNode
的类,然后实现 parse
的方法逻辑
自定义异常提供了一种使用 on_error
方法处理异常的精确方法。如果你想在参数节点无法解析文本时引发自定义异常,则需要使自定义异常继承自 CommandSyntaxError
这是一个自定义参数节点 PointArgument
的简单示例。它接受连续 3 个 float 类型的参数输入作为坐标,并将它们作为点储存到列表中。如果它获得非浮点输入,则抛出 IllegalPoint
异常。如果指令在读取完三个浮点数之前结束,则抛出 IncompletePoint
异常
class IllegalPoint(CommandSyntaxError):
def __init__(self, char_read: int):
super().__init__('Invalid Point', char_read)
class IncompletePoint(CommandSyntaxError):
def __init__(self, char_read: int):
super().__init__('Incomplete Point', char_read)
class PointArgument(ArgumentNode):
def parse(self, text: str) -> ParseResult:
total_read = 0
coords = []
for i in range(3):
total_read += len(text[total_read:]) - len(command_builder_util.remove_divider_prefix(text[total_read:]))
value, read = command_builder_util.get_float(text[total_read:])
if read == 0:
raise IncompletePoint(total_read)
total_read += read
if value is None:
raise IllegalPoint(total_read)
coords.append(value)
return ParseResult(coords, total_read)
对于它的用法,这是一个简单的示例,以及一个对应的输入/输出表:
def on_load(server, prev):
server.register_command(
Literal('!!mypoint').then(
PointArgument('pt').
runs(lambda src, ctx: src.reply('You have input a point ({}, {}, {})'.format(*ctx['pt'])))
)
)
输入值 |
输出值 |
---|---|
!!mypoint 1 2 3 |
You have input a point (1.0, 2.0, 3.0) |
!!mypoint 1 2 |
Incomplete Point: !!mypoint 1 2<– |
!!mypoint xxx |
Invalid Point: !!mypoint xxx<– |
!!mypoint 1 2 x |
Invalid Point: !!mypoint 1 2 x<– |