指令

厌倦了手动拆分参数、解析指令?厌烦了复杂的判断条件?快来尝试 MCDR 的指令构建系统吧!

MCDR 内置了一个指令树构建系统,供插件构建其指令。它如同一个 Mojang 的 brigadier 的精简版。

工作流程

MCDR维护了一个 dict 用于储存注册的指令。该 dict 的值均为指令树根节点列表,而值对应的键则是根节点的字面值。有了它,MCDR 可以快速地找到可能可以接收到来指令的指令树。

每次处理用户信息时,MCDR都会尝试将用户输入解析为指令。它将用户输入的第一个分段作为键来查询指令树存储字典。如果指令存在,则调用 info.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 指令时,以下过程将会发生:

  1. 于节点 Literal('!!email') 解析指令 !!email remove 21

    1. 字面量节点 Literal('!!email') 获取了 !!email remove 21 的第一个元素,它是 !!email ——与字面量节点匹配。

    2. 现在余下的指令是 remove 21

    3. 于是,它搜索其字面量子节点,找到与下一个指令元素 remove 匹配的子节点 Literal('remove')

    4. 这样,它让该子节点处理其余指令。

  2. 于节点 Literal('remove') 解析指令 remove 21

    1. 字面量节点 Literal('remove') 获取了 remove 21 的第一个元素,它是 remove ——与字面量节点匹配。

    2. 现在余下的指令是 21

    3. 然后它搜索其字面量子节点,但未找到与下一个指令元素 21 匹配的任何字面量子节点。

    4. 因此,它让它的非字面量子节点 Integer('email_id') 处理剩余指令。

  3. 于节点 Integer('email_id') 解析指令 21

    1. 整数节点 Integer('email_id') 获得了 21 的第一个元素,这是一个合法的整数。

    2. 它使用键 `` email_id`` 将值 21 存储到上下文 dict 中。

    3. 然后,它发现指令解析已经完成,因此它以指令源和上下文 dict 作为参数来调用回调函数。

    4. 至此,指令解析完成。

以上是指令构建系统逻辑部分的快速概述,主要是为了帮助你建立对指令树和指令构建系统的感性理解。

匹配文字节点,解析剩余指令,将解析后的值存储在上下文字典中,这就是指令系统的工作方式。

除了阅读本文档外,学习使用 MCDR 指令构建系统的另一种好办法是引用和模仿现有代码。你可以在 mcdreforged.plugin.permanent.mcdreforged_plugin.MCDReforgedPlugin 类的 __register_commands 方法下找到 !!MCDR 指令的构建代码。

参数节点

MCDR 内置参数节点列表及其用法。

ArgumentNode

ArgumentNode 是所有参数节点的基础节点,也是一个类。它提供了几种构建指令树的方法。

then

def then(self, node: 'ArgumentNode') -> ArgumentNode

将子节点附加到其子列表中,然后返回自身。

用于构建指令树结构。

参数 node:要添加到当前节点的子级列表中的节点实例。

可以使用以下代码构建 “先瞅一眼…” 部分中的指令树:

Literal('!!email'). \
then(
    Literal('list')
). \
then(
    Literal('remove').
    then(
        Integer('email_id')
    )
). \
then(
    Literal('send').
    then(
        Text('player').
        then(
            GreedyText('message')
        )
    )
)

runs

def runs(self, func: Union[Callable[[], Any], Callable[[CommandSource], Any], Callable[[CommandSource, dict], Any]]) -> ArgumentNode

设置节点的回调函数。在该节点上的指令解析完成后,将执行指定的回调函数。

参数 func:一个最多可接收 2 个参数的可调用对象。参数列表:CommandSourcedict (上下文)

回调函数允许接受 0 到 2 个参数( CommandSource 作为指令源,而 dict 作为上下文)。例如,是可用的四个回调函数:

def callback1():
    pass

def callback2(source: CommandSource):
    pass

def callback3(source: CommandSource, context: dict):
    pass

callback4 = lambda src: src.reply('pong')
node1.runs(callback1)
node2.runs(callback2)
node3.runs(callback3)
node4.runs(callback4)

它们都可以用作 runs 方法的参数

这种动态回调参数适配在所有指令节点的回调调用中使用。

requires

def requires(self, requirement: Union[Callable[[], bool], Callable[[CommandSource], bool], Callable[[CommandSource, dict], bool]], failure_message_getter: Optional[Union[Callable[[], str], Callable[[CommandSource], str], Callable[[CommandSource, dict], str]]] = None) -> ArgumentNode

设置节点的需要的测试器回调。进入此节点时,MCDR将调用需求测试器以查看当前指令源和上下文是否符合你设定的条件。

如果测试器返回 True,则MCDR将继续解析指令的其余部分。

如果测试器返回False,则会引发 RequirementNotMet 异常。此时,如果 failure_message_getter 参数可用,则MCDR将调用 failure_message_getter 以获取消息字符串作为 RequirementNotMet 的异常消息,否则将使用默认消息。

参数 requirement: 一个最多接受 2 个参数并返回一个 bool 的可调用对象。参数列表:CommandSource , dict (上下文)

参数 failure_message_getter:一个可选的可调用对象,最多接受 2 个参数并返回一个str。参数列表:CommandSourcedict (上下文)

一些示例用法:

node.requires(lambda src: src.has_permission(3))  # Permission check
node.requires(lambda src, ctx: ctx['page_count'] <= get_max_page())  # Dynamic range check
node.requires(lambda src, ctx: is_legal(ctx['target']), lambda src, ctx: 'target {} is illegal'.format(ctx['target']))  # Customized failure message

redirects

def redirects(self, redirect_node: ArgumentNode) -> ArgumentNode

将所有其他子节点指令解析重定向到另一个给定节点。当你需要一个简短的指令和一个全路径指令来执行相同的指令时,redirect 将使其更简单。

参数 redirect_node:当前节点的重定向目标

例如:

command_node = Literal('command'). \
    then(Literal('x').runs(do_something1)). \
    then(Literal('y').runs(do_something2)). \
    then(Literal('z').runs(do_something3))

long_node = Literal('a').then(Literal('long').then(Literal('way').then(Literal('to').then(Literal('the').then(command_node)))))
short_node = Literal('quick').redirects(command_node)

root_executor = Literal('foo').then(long_node).then(short_node)

以上示例中,指令从 root_executor 这个节点开始解析。

以下指令:

  • “foo a long way to the command x”

  • “foo a long way to the command y”

  • “foo a long way to the command z”

与以下指令相同:

  • “foo quick x”

  • “foo quick y”

  • “foo quick z”

注意 redirectsthen 之间的区别。redirects 是重定向子节点,而 then 是添加子节点。如果你执行以下操作:

short_node2 = Literal('fast').then(command_node)
root_executor = Literal('foo').then(long_node).then(short_node).then(short_node2)

在以上示例中,所有最终执行 do_something1 的指令将是:

  • foo a long way to the command x

  • foo quick x

  • foo fast command x

on_error

def on_error(self, error_type: Type[CommandError], handler: Union[Callable[[], Any], Callable[[CommandSource], Any], Callable[[CommandSource, CommandError], Any], Callable[[CommandSource, CommandError, dict], Any]], *, handled: bool = False) -> ArgumentNode

当发生指令错误时,给定的将调用给定的处理程序以处理错误。

参数 error_type:CommandError 的子类

参数 handler:一个最多可以接受 3 个参数的可调用对象。参数列表:CommandSourceCommandErrordict (上下文)

关键字参数 handled:如果将 handled 设置为 True,则在调用处理程序回调时会自动调用 error.set_handled()

对于 error.set_handled() 的用法,请查看 CommandError 类的相关说明。

on_child_error

def on_child_error(self, error_type: Type[CommandError], handler: Union[Callable[[], Any], Callable[[CommandSource], Any], Callable[[CommandSource, CommandError], Any], Callable[[CommandSource, CommandError, dict], Any]], *, handled: bool = False) -> ArgumentNode

on_error 类似,不过它仅在该节点收到了一个来自其子孙节点的指令错误时触发

Literal

字面值(Literal)节点是一个特殊的节点。它不输出任何值,更像是一个指令分支的载体。

字面值节点可以在其构造的函数中接受 str 作为其字面值。当解析指令的下一个元素与节点的字面值完全匹配时,字面值节点才接受并解析指令。

字面值节点是唯一可以发起指令执行的节点。

例如:

Literal('foo').runs(lambda src: src.reply('Foo!'))  # input "foo", get reply "Foo!"
Literal('foo').then(
    Literal('bar').runs(lambda src: src.reply('Foo Bar'))
)  # input "foo bar", get reply "Foo Bar"

NumberNode

这是一个抽象类。它被 NumberIntegerFloat 继承。它代表一种基于数字的节点。

对于 NumberNode 实例,你可以限制数字参数的范围。如果解析的数字超出范围,则会抛出 NumberOutOfRange 异常。

默认情况下,没有范围限制。

at_min

def at_min(self, min_value) -> NumberNode

将数字范围的下限设置为 min_value

at_max

def at_max(self, max_value) -> NumberNode

将数字范围的上限设置为 max_value

in_range

def in_range(self, min_value, max_value) -> NumberNode

同时设置数字范围的上、下限。

Number

Number 节点接受数字参数——整数或浮点数。如果下一个元素不是数字,则将抛出 InvalidNumber 异常。

Integer

Integer 节点只接受 int 类型的参数,即整数。如果下一个元素不是整数,则将抛出 InvalidInteger 异常。

Float

Float 节点只接受 float 类型的参数,即浮点数。如果下一个元素不是浮点数,则将抛出 InvalidFloat 异常。

TextNode

这是一个抽象类。它被 TextQuotableTextGreedyText 继承。它代表一种基于文本的节点。

对于 TextNode 实例,你可以限制文本参数的长度范围。如果解析的文本长度超出范围,则将抛出 TextLengthOutOfRange 异常。

默认情况下,没有长度范围限制。

at_min_length

def at_min_length(self, min_length) -> TextNode

将长度范围的下限设置为 min_length

at_max_length

def at_max_length(self, max_length) -> TextNode

将长度范围的上限设置为 max_length

in_length_range

def in_length_range(self, min_length, max_length) -> TextNode

同时设置文本长度的上、下限。

Text

Text 节点接受一个字符串元素。空格字符是MCDR指令解析的分隔符,所以 Text 节点将获取第一个空格之前的字符。

QuotableText

QuotableText 的工作方式与 Text 节点一样,但是它提供了一种输入带有空格的文本的方法:使用两个双引号将文本内容括起来。

使用双引号括住文本内容后,你可以使用转义字符 \ 来转义 "\ 本身。

例如,以下是可被 QuotableText 接受的一些文本:

  • 我是一段文字 -> 我是一段文字

  • "这段 文本 包括 空格 !" -> 这段 文本 包含 空格

  • "也可以像这样包含 \\ 或者 \" !" -> 也可以像这样包含 \ 或者 "

GreedyText

GreedyText 的原理很简单:它贪婪地取出指令中所有剩余的文本。

将任何子节点附加到 GreedyText 上不是明智的决定,因为这样子节点永远无法获得任何剩余指令。

自定义

MCDR 支持自定义参数节点。它也许能节省一些你为构建指令而重复工作的时间。

要创建自定义参数节点,你需要声明一个继承自 ArgumentNode 的类,然后实现 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):
            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<–