指令树

厌倦了手动拆分参数、解析指令?厌烦了复杂的判断条件?快来尝试 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 指令时,以下过程将会发生:

  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. 至此,指令解析完成

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

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

构建指令树的方法

如果你熟悉 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_emailremove_emailsend_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<–