命令


命令是和命令集紧密联系的,想要了解命令系统如何工作需要同时阅读这两篇文档,将它们一分为二是为了便于阅读。

命令是用户与游戏交互的基本方式,有的命令与游戏世界直接相关,如look、get、drop等,有的命令用于管理,如examine和@dig。

Evennia的默认命令是“MUX风格”的,它使用@来标志管理命令,支持参数,带有'='号的语法等,但你也可以为自己的游戏建立一套完全不同的命令系统。你可以在 src/commands/default 找到默认的命令,你不应该在那里做直接修改 —— 如果Evennia团队添加了新功能,它们会被覆盖掉。你应该学习它们的设计思想,在你自己的设计中继承它们。

命令运行需要两种组件 —— Command类和命令集(命令集被分到一篇独立的文档中以便于阅读)。
  • 命令是一个python类,其中包含了实现命令的所有功能代码 —— 比如,get命令包含了拾起物体的代码。
  • 命令集像是一个容器,可以容纳一个或多个命令。一个命令可以放入任何多个不同的命令集中。只要把命令集添加到角色对象上,你就可以让该角色使用命令集中的所有命令。如果你希望用户能以特别的方式使用对象,你还可以将命令集添加在普通对象上。试想一下,一个“树”对象的命令集中带有攀爬和砍倒命令,或者一个“时钟”对象带有检查时间的命令。
本文档将全面、详细地介绍如何使用命令,要充分使用它们,你还必须阅读这篇文档中关于命令集的详细介绍。这里还有一篇教你按步骤添加命令的教程,可以让你快速上手,没有多余的解释。


定义命令

所有的命令都由继承自基类 Command(ev.Command)的普通Python类实现。你会发现这个基类是“空”的。Evennia的默认命令实际上是从其子类 MuxCommand 继承的,这个类知道 /开关、分开的“=”之类的所有MUX类语法。下面我们将避开和MUX有关的细节,直接使用基类Command。
# 基本的命令定义
from ev import Command
class MyCmd(Command):
    """
    这里是命令的帮助信息
    """
    key = "mycommand" 
    def parse(self):
        # 在这里解析命令行
    def func(self):
        # 在这里执行命令
你可以通过在继承的类中添加几个类全局属性、重载一两个钩子函数来定义新的命令。在文档的后面有关于命令幕后工作机制的详细介绍,现在你只需要知道命令处理程序会创建这个类的实例,每当你使用该命令都会调用这个实例,它还会给新的命令实例动态添加几个有用的属性,你可以假设它们是一直可用的。

谁在调用命令?
在Evennia中有三类物体可能会调用命令对象。知道这一点重要,因为会在运行中给命令体的caller、session、sessid和player属性设置相应的值。在多数情况下,调用者是会话。

  • 会话。当用户在客户端输入命令时,这是最常出现的情况。
    • caller —— 如果存在被操纵的物体,设为该物体。如果没有找到被操纵的物体,caller 则和 player 一样。只有当玩家也不能存在的时候(比如在登录之前),才会被设为会话对象自身。
    • session —— 指向会话对象自身。
    • sessid —— sessid.id,会话的唯一id(整型)。
    • player —— 与会话相关联的玩家对象,如果没有登录则为 None。

  • 玩家。这种情况只会出现在调用 player.execute_cmd() 的时候,此时无法获得会话信息。
    • caller —— 如果被操纵的对象可以确定,就设为该对象(因为没有会话信息,只有当 MULTISESSION_MODE=0或1 时可以确定)。如果没有找到被操纵的对象,则等于 player。
    • session —— None
    • sessid —— None
    • player —— 指向玩家对象。

  • 物体。这种情况只会出现在调用 object.execute_cmd() 的时候(比如被NPC调用)。
    • caller —— 指向调用它的物体。
    • session —— None
    • sessid —— None
    • player —— None

在运行时分配给命令实例的属性
假设玩家Bob的角色叫BigGuy,输入了命令look at sword。在系统识别出这是“look”命令并确定BigGuy可以使用名为look的命令之后,它把look命令的类从仓库中拿出来,然后要么从缓存中加载现成的命令实例,要么创建一个。在做过一些检查之后,给它分配以下属性:
  • caller —— 在这个例子中是角色BigGuy。它指向执行命令的对象。这个值取决于是什么类型的对象调用了命令,参见上一节。
  • session —— 连接到游戏并控制BigGuy的Bob会话(参见上一节)。
  • sessid —— self.session的唯一id,用于快速查找。
  • player —— 玩家Bob(参见上一节)。
  • cmdstring —— 与命令匹配的关键字,在我们的例子中是look。
  • args —— 除命令名之外的字符串其余部分。因此,如果输入的字符串是look at sword,args则是“at sword”。
  • obj —— 定义了命令的游戏物体。这不一定是调用者,但由于 look 是一个常用(默认)命令,它可能直接定义在 BigGuy 上,所以 obj 会指向的 BigGuy。在其它情况下,obj 可能是玩家或任何在上面定义了命令的交互式对象,比如例子中定义了“check time”命令的“时钟”对象,或是可以按动的红色按钮
  • cmdset —— 它指向与命令匹配的合并后的命令集(见下文)。这个变量很少使用,它的主要用途是用于自动帮助系统(高级注释:合并后的命令集不一定和 BigGuy.cmdset 一样,比如合并后的命令集可能包含了房间中其它对象的命令集)。
  • raw_string —— 这是用户的原始输入,不会去除前后的任何空白,唯一去除的是行末的换行符。

定义自己的命令类
除了运行时Evennia自动分配给命令的属性(如上所列),你还要定义以下的类属性:
  • key (字符串)—— 命令的标志符,如 look。(在理想情况下)这应该是唯一的。关键字可以由多个单词组成,如“press button”或“pull left lever”。注意,关键字和下面的别名都是用来确定命令的标识。所以只要有一个能匹配上,命令就会被选中。这对后面所述的合并命令集很重要。
  • aliases (可选,列表)—— 命令别名的列表(["glance", "see", "l"])。名字规则和关键字一样。
  • locks (字符串)—— 锁的定义,通常的形式是这样的: cmd:<锁函数> 。锁是一个相当大的话题,因此在你没有学习更多关于锁的内容之前,应该将锁字符串都赋为“cmd:all()”,这样每个人都可以使用它(如果你没有设置锁字符串,会自动为你设置这个值)。
  • help_category(可选,字符串)—— 设置这个值可以给自动帮助的内容归类。如果没有设置,会默认设为 General。
  • save_for_next(可选,布尔值)—— 默认值为False。如果为True,则该命令对象的副本(连同你所做的任何修改)会被系统存储起来,下一个命令可以通过 self.caller.ndb.last_cmd 访问它。运行下一个命令会清除或替换存储的内容。
  • arg_regex(可选,raw string 原始字符串)—— 它的值应该是原始的正则表达式字符串。系统会在运行时编译正则表达式。你可以在这里规定紧跟在命令名(或别名)之后的部分必须符合什么样子。通常命令解析器可以高效地识别出命令名,即使命令和后面的单词连在一起(只要连在一起的单词本身不是命令名)。因此,“lookme”会被解析为命令 look 并跟着参数 me 。举个例子,通过使用 arg_regex 你可以让解析器要求在命令名后面加一个空格(对应的正则表达式字符串是 r"\s.*?|$")。在这种情况下,只有“look me”可以工作,而“lookme”会报“命令无法找到”的错误。
  • auto_help(可选,布尔值)—— 默认为True。这个值允许对单独的命令关闭自动帮助系统。如果你想手工编写帮助条目或在帮助列表中隐藏这条命令,这会派上用处。

你还需要至少实现两个方法:parse() 和 func()(如果你想从更本上改变检测访问权限的工作方式,你还可以自己实现 perm(),但这不是必要的)。
  • parse() 是用来解析函数参数(self.args)的。你可以用任何你喜欢的方式来处理,然后将结果存入命令对象自身的变量中(比如存入 self)。举个例子,默认的类mux系统使用这个方法来检测“命令开关”,并将其存入 self.switches 列表中。由于在一套命令方案中,分析工作通常是非常相似的,你应该将 parse() 做得尽可能通用然后来继承它,而不是一遍又一遍地反复实现它。默认的 MuxCommand 类就是这样的,它实现了 parse(),所有子命令都使用它。
  • func() 紧接着 parse() 被调用,可以利用预解析的结果来实现命令所期望的效果。这是命令的主体。

最后,你应该始终在你类的顶部添加文档字符串(__doc__)。这个字符串会被帮助系统自动读取,用来创建该命令的帮助条目。你应该选定一种帮助信息的格式,并坚持使用它。

下面是定义一个简单的“微笑”命令的方法:
from ev import Command

class CmdSmile(Command):
    """
    A smile command

    Usage: 
      smile [at] []
      grin [at] [] 

    Smiles to someone in your vicinity or to the room
    in general.

    (This initial string (the __doc__ string)
    is also used to auto-generate the help 
    for this command)
    """ 

    key = "smile"
    aliases = ["smile at", "grin", "grin at"] 
    locks = "cmd:all()"
    help_category = "General"

    def parse(self):
        "Very trivial parser" 
        self.target = self.args.strip() 

    def func(self):
        "This actually does things"
        caller = self.caller
        if not self.target or self.target == "here":
            string = "%s smiles." % caller.name
            caller.location.msg_contents(string, exclude=caller)
            caller.msg("You smile.")
        else:
            target = caller.search(self.target)
            if not target: 
                # caller.search handles error messages
                return
            string = "%s smiles to you." % caller.name
            target.msg(string)
            string = "You smile to %s." % target.name
            caller.msg(string)
            string = "%s smiles to %s." % (caller.name, target.name)           
            caller.location.msg_contents(string, exclude=[caller,target])
将命令做成类以及将 parse() 和 func() 分开的好处在于可以继承相关功能,而不需要对每个命令都单独解析。例如像前面提到的,默认命令都继承自 MuxCommand。 MuxCommand 实现了自己的 parse(),能够解析类mux命令的所有细节。这样几乎所有的默认命令都不需要再实现 parse(),它们可以认为传入的字符串已经由其父类以适当的方式解析了。

想要在游戏中实际使用命令,你还需要将它放入命令集中。请参见命令集的文档。


系统命令

注:这是进阶内容。如果你是初次学习命令,可以跳过它。

有几种命令的情况受到服务器的特别关注。如果玩家输入了空字符串会怎么样?如果给出的“命令”与用户想发送消息的频道名发生冲突了会怎么样?或者,是否可能有重复的命令?

这些“特殊情况”是由系统命令来处理的。系统命令的定义方式和其它命令一样,除了它们的名字(关键字)必须被设为引擎的保留字(名字在 src/commands/cmdhandler.py 的顶部定义)。你可以在 src/commands/default/system_commands.py 找到系统命令(没被使用)的实现。由于(在默认情况下)这些命令没有包含在任何命令集中,它们不会被实际使用,只是用来演示。当发生特殊情况时,Evennia首先会去搜索所有有效的命令集,查找你自己定义的系统命令,然后才会去用自己的硬编码来实现。

下面是触发系统命令的特殊情况,你可以看到被它们用作 ev.syscmdkeys 属性的命令关键字。
  • 没有输入(syscmdkeys.CMD_NOINPUT) —— 玩家没有输入任何内容,只是按下了回车。默认是什么都不做,但在某些实现中也可以在这里做一些有用的事,比如行编辑器用来中断非命令的文本输入(编辑缓冲区中的空行)。
  • 命令无法找到(syscmdkeys.CMD_NOMATCH) —— 没有找到匹配的命令。默认是显示错误信息“Huh?”。
  • 找到了多个匹配的命令(syscmdkeys.CMD_MULTIMATCH) —— 默认是显示匹配命令的列表。
  • 用户不允许执行该命令(syscmdkeys.CMD_NOPERM) —— 默认是显示错误信息“Huh?”。
  • 频道(syscmdkeys.CMD_CHANNEL) —— 这是你订阅的频道的名字,默认是将命令的参数转发到该频道上。这些命令会由通讯系统按照你的订阅情况动态创建。
  • 新会话连接(syscmdkeys.CMD_LOGINSTART) —— 这条命令的名字放在 settings.CMDSET_UNLOGGEDIN 中,每当建立了一个新的连接,这条命令都会在服务器上被调用(默认是显示欢迎界面)。

下面的例子会重新定义当玩家没有任何输入(比如只按下回车)时所做的处理。当然,要让新的系统命令工作,还必须将它添加到一个命令集中。
from ev import syscmdkeys, Command

class MyNoInputCommand(Command):
    "Usage: Just press return, I dare you"
    key = syscmdkeys.CMD_NOINPUT
    def func(self):
        self.caller.msg("Don't just press return like that, talk to me!")


动态命令

注:这是进阶内容。

通常,命令是作为固定的类创建的,使用时不能修改。不过也有一些情况,要预先确定准确的关键字、别名或其它属性是不可能的或不切实际的。(出口就是这样的一个例子)。

要创建一个带有动态调用名的命令,应该先在类中定义一个普通的命令体(设置关键字、别名为默认值),然后使用下面的命令(假设你创建的命令类名为 MyCommand):
    cmd = MyCommand(key="newname", 
                    aliases=["test", "test2"], 
                    locks="cmd:all()",
                    ...)
你给命令构造函数的所有关键字参数都被存为命令对象的属性,这会覆盖父类中已经定义的属性。

通常你只需要在运行时定义你的类,并覆盖如 key(关键字)、aliases(别名)之类的信息。但原则上你也可以将方法对象(如 func)作为关键字参数传递,这样你的命令就可以完全在运行时定制了。


出口

注:这是进阶内容。

出口是使用动态命令的一个例子。

Evennia中出口的功能不是在引擎中硬编码的。相反,出口是普通的类型类对象,它们在加载时会在自己身上自动创建一个命令集。这个命令集中有一个动态创建的命令,它的属性(关键字、别名、锁等)与出口对象相同。当输入出口的名字时,这个动态出口命令会被触发,(在做过访问检查之后)将角色移动到出口的目的地。

虽然你可以自定义出口对象和它的命令来实现完全不同的功能,但通常你只需要在出口对象上使用适当的 traverse_* 钩子函数就可以了。如果你真的对改变幕后工作机制有兴趣,你可以看 src.objects.objects 来了解出口类型类是如何设置的。


命令是如何实际工作的

注:这是主要针对服务器开发者的进阶内容。

任何时候用户将文本发送到Evennia,服务器都会试着找出输入的文本是否和已知命令相对应。以下是命令处理程序查找已登录用户命令的流程:

  1. 用户输入文本字符串并按下回车键。
  2. 用户的会话判断出这段文本不是协议中的特定控制序列或相关命令,将它发送给命令处理程序。
  3. Evennia的命令处理程序分析会话,找到最终的玩家和最终控制的角色(之后这些信息会存在命令对象中)。将 caller(调用者)属性设为合适的值。
  4. 如果输入的是空字符串,则将命令重新发送给 CMD_NOINPUT。如果在命令集中没有 CMD_NOINPUT 命令,则忽略掉它。
  5. 如果 command.key 与 settings.IDLE_COMMAND 匹配,则只更新定时器,不再做其它事情。
  6. 命令处理程序找出当前 caller(调用者)可用的所有命令集:
    • 当前调用者自己的有效命令集。
    • 如果调用者是被操纵的物体,则查找当前玩家的命令集。
    • 会话本身的命令集。
    • 在同一地点其它对象的有效命令集(如果有的话)。这包括出口命令。
    • 当前通信动态创建的系统命令集。
  7. 所有优先级相同的命令集合并成一组。分组可以避免同优先级的命令集与低优先级命令集合并时的依赖问题。
  8. 所有分组的命令集按各自的合并规则以相反次序合并成一个命令集。
  9. Evennia的命令解析器取得合并后的命令集,从调用者输入的字符串的第一个字符开始,按照关键字和别名与每一条命令作匹配,产生一组候选命令。
  10. 命令解析器接下来会按照匹配字符的数量和匹配字符在命令中所占的百分比给它们打分。只有候选命令的得分相同才会返回多个匹配的命令。
    • 如果返回了多个匹配的命令,则重新发送 CMD_MULTIMATCH。如果在命令集中没有 CMD_MULTIMATCH 命令,则返回硬编码的匹配列表。
    • 如果没有找到匹配项,则重新发送 CMD_NOMATCH。如果在命令集中没有 CMD_NOMATCH 命令,则给出硬编码的错误信息。
  11. 如果解析器找到了一条匹配的命令,会将正确的命令对象从存储库中取出。它通常不会重新初始化。
  12. 它会通过验证锁字符串来检查调用者是否可以使用该命令。如果不行,它会被认作是不合适的匹配,CMD_NOMATCH 会被触发。
  13. 如果新的命令被标记为频道命令,则会重新发送给 CMD_CHANNEL。如果在命令集中没有 CMD_CHANNEL 命令,则用硬编码来实现。
  14. 将若干变量值赋给命令的实例(请参阅前面的部分)。
  15. 对命令实例调用 at_pre_command()。
  16. 对命令实例调用 parse()。这会传入字符串中命令名之后的剩余部分,这是为了将字符串预解析为 func() 方法可用的格式。
  17. 对命令实例调用 func()。这是命令实际工作的功能体。
  18. 对命令实例调用 at_post_command()。


其他事项

Command.func() 的返回值是 Twisted 的 Deferred 延迟对象。在默认情况下 Evennia 根本不使用这个返回值。如果你要用,你必须用以下方式使用回调来做到异步处理。
    # in command class func()
    def callback(ret, caller):
        caller.msg("Returned is %s" % ret)
    deferred = self.execute_command("longrunning")
    deferred.addCallback(callback, self.caller)
可能只有最先进、最独特的设计会用到它(比如有人可能会用它来建立“嵌套”的命令结构)。

类变量 save_for_next 可以用来实现命令状态的持久化。比如,这可以让一个命令对“某某”进行操作,而“某某”是之前的命令操作过的对象。


(原文:https://github.com/Evennia/evennia/wiki/Commands    翻译:卢铱俊)