大多数游戏需要限定玩家可以做哪些事情、不可以做哪些事情。在Evennia中,这个功能由锁提供并负责检查。Evennia中的所有实体(命令物体脚本玩家帮助系统消息频道)都受锁的控制。

锁可以看作是一种“访问规则”用来限制Evennia实体的用途。当另一个实体想要来访问,锁会以指定的方式进行分析,并决定是否允许访问。Evennia所使用的“锁定”理念是:所有实体默认都是不可访问的,除非你明确定义了锁,允许部分或全部访问。

让我们举个例子:某个物体带有一把锁,限制什么人可以“删除”该对象。这个锁除了知道它限制的是删除功能,还知道只有具有特定id的玩家(比如id为34)可以将其删除。所以当玩家试图对这个物体运行 @delete 时,@delete 命令会检查这个玩家是否允许这样做。它会调用的锁,而锁会检查玩家的id是否为34,只有相符才会允许 @delete 命令继续自己的工作。


设置和检查锁

在游戏中,给物体上设置锁的命令是 @lock:
 > @lock obj = <锁字符串>
锁字符串是具有一定格式的字符串,用来定义锁的行为。在下一节中,我们会详细讨论锁字符串的格式。

在代码中,Evennia通过名为 locks 的处理程序来处理锁,在所有相关的实体上都有它。这个处理程序可以让你添加、删除和检查锁。
    myobj.locks.add(<锁字符串>)
你可以调用 locks.check() 来检查锁,但为了隐藏底层的细节,所有对象都还提供了更便捷的 access 方法,应该优先使用 access。在下面的例子中,accessing_obj是请求“删除”权限的对象,而 obj 是可能被删除的对象。这是 @delete 命令内部的样子:
     if not obj.access(accessing_obj, 'delete'):
         accessing_obj.msg("对不起,你不能删除它。")
         return


定义锁

想在Evennia中定义锁(即访问限制),可以通过使用 obj.locks.add() 给对象的 locks 属性添加锁定义字符串来实现。

以下是一些锁字符串的例子:
    delete:id(34)   # 只允许id为34的对象删除
    edit:all()      # 让所有人都可以编辑
    # 只有不是“very_weak”的人或者是巫师才可以拾起它
    get: not attr(very_weak) or perm(Wizard)
在形式上,锁字符串的语法如下:
    访问类型: [AND] 锁函数1([参数1,..]) [AND|OR] [NOT] 锁函数2([参数1,...]) [...]
其中,[]表示可选部分。AND、OR 和 NOT 不区分大小写,多余的空格会被忽略。锁函数1、锁函数2等是在锁系统中可用的锁函数。

因此,锁字符串由限制类型(访问类型)、冒号(:)和表达式组成,表达式中包含了一些函数,它们规定了通过锁需要哪些条件。每个函数的返回值都是 True 或 False。AND、OR 和 NOT 的作用和在Python中一样。如果总的结果是 True,则能够通过锁。

在一个锁字符串中,你可以通过分号(;)分隔来创建多个锁类型。以下字符串产生的效果和前面的例子一样:
delete:id(34);edit:all();get: not attr(very_weak) or perm(Wizard)

有效的访问类型
访问类型是锁字符串的第一部分,它规定了锁的控制范围,如“delete”或“edit”。原则上你可以给访问类型起任何名字,只要它在相关对象上是唯一的就行了。访问类型的名字是不区分大小写的。

如果你想确保锁会被使用,那你设定的访问类型名字应该是你(或默认命令集)会实际检查的名字,如上面例子中的 @delete 使用的访问类型是“delete”。

以下是默认命令集所检查的访问类型。
  • 命令
    • cmd —— 规定谁可以调用该命令。

  • 物体
    • control —— 谁是物体的“所有者”,可以对它设置锁、删除它等。默认是物体的创建者。
    • call —— 谁可以调用该物体上的命令。
    • examine —— 谁可以检查该物体的属性。
    • delete —— 谁可以删除该物体。
    • edit —— 谁可以编辑该物体的属性。
    • view —— look命令是否可以显示/列出该物体
    • get —— 谁可以拾起该物体并随身携带它。
    • puppet —— 谁可以“变成”该物体,并像控制“角色”一样控制它。
    • attrcreate —— 谁可以在该物体上创建新属性(默认为True)

  • 角色
    • 与物体相同。

  • 出口
    • traverse —— 谁可以通过出口。
    • 其他与物体相同。

  • 玩家
    • examine —— 谁可以查看该玩家的属性。
    • delete —— 谁可以删除该玩家。
    • edit —— 谁可以修改该玩家的属性。
    • msg —— 谁可以给该玩家发送邮件。
    • boot —— 谁可以踢出该玩家。

  • 通用属性:(只有 obj.secure_attr 会检查)
    • attrread —— 查看、读取该属性
    • attredit —— 更改、删除该属性

  • 频道
    • control —— 谁可以管理该频道,即可以删除该频道、踢走听众等。
    • send —— 谁可以向该频道发送消息。
    • listen —— 谁可以订阅、收听该频道。

  • 帮助条目
    • examine —— 谁可以查看该帮助条目(通常是所有人)
    • edit —— 谁可以编辑该帮助条目。

举个例子,不论何时穿越出口,都会检查 traverse 类型的锁。因此,要给出口设置合适的锁就需要用这样的锁字符串 traverse: <锁函数> 。

锁函数
你不能在锁字符串中使用任意函数,实际上你只能使用定义在 settings.LOCK_FUNC_MODULES 指定模块中的函数。这些模块中定义的所有函数都被自动视为有效的锁函数。默认的锁函数可以通过 ev.lockfuncs 或在 src/locks/lockfuncs.py 中找到。

锁函数必须至少接受两个参数:访问者(这是希望得到访问许可的对象)和被访问者(这是带着锁的对象)。在检查锁的时候,这两个对象会自动作为函数的头两个参数,而锁定义中给出的其他参数都将作为之后的附加参数。
    # 一个简单的锁函数例子。用如“id(34)”的方式调用。

    def id(accessing_obj, accessed_obj, *args, **kwargs):
        if args:
            wanted_id = args[0]
            return accessing_obj.id == wanted_id
        return False 
(使用*和**语法可以让Python把所有的额外参数放到元组 args 中,把所有的关键字参数放到字典 kwargs 中。如果你不熟悉 *args 和 **kwargs 的工作方式,请查询Python手册)。以下是一些有用的默认锁函数(更多内容请见 src/locks/lockfuncs.py):

  • true()/all() —— 允许任何人访问
  • false()/none()/superuser() —— 没有人可以访问。超级用户可以完全绕过访问检查,因此是唯一可以通过检查的人。
  • perm(perm) —— 检查是否和指定的权限级别匹配,首先匹配玩家权限,然后匹配角色权限。见下文。
  • perm_above(perm) —— 与perm类似,但需要比指定权限更“高”的权限。
  • id(num)/dbref(num) —— 检查访问者是否具有指定的id/dbref。
  • attr(attrname) —— 检查访问者是否具有指定的通用属性。
  • attr(attrname, value) —— 检查访问者是否有指定的通用属性且具有指定值。
  • attr_gt(attrname, value) —— 检查访问者的指定通用属性是否大于(>)指定值。
  • attr_ge、attr_lt、attr_le、attr_ne —— 分别对应 >=、<、<= 和 != 。
  • holds(objid) —— 检查访问者是否包含了具有指定名称或dbref的对象。
  • pperm(perm), pid(num)/pdbref(num) —— 与 perm、id/dbref 一样,不过它查找是玩家的权限或dbref,而不是角色的。


只检查锁字符串

有时你并不想检查完整的锁,而只想检查是否符合锁字符串。一个常见的用途是在命令内部检查用户是否具有特定权限。锁处理程序有一个方法 check_lockstring(accessing_obj, lockstring, bypass_superuser=False) 可以做这事。
    # 在命令定义的内部
     if not self.caller.locks.check_lockstring(self.caller, "dummy:perm(Wizards)"):
         self.caller.msg("你必须是巫师或有更高权限才能这么做!")
         return
请注意,这里的访问类型可以是一个虚假的值,因为这个方法实际上不会去检查它。


默认的锁

Evennia会为新的物体和玩家创建一些基本的锁(如果不这样做,那所有人在一开始都无法访问任何东西)。这都在相应实体的类型类基类的钩子方法 basetype_setup() 中定义(通常不需要修改它,除非你想更改一些基本的东西,如房间或出口存储内部变量的方式)。它只会在 at_object_creation 之前被调用一次,所以如果你想修改默认值,可以修改你的子类对象,把代码写在之后的方法中。创建命令,如@create,也会改变你创建的对象的锁,比如它会设置锁类型 control,让你(创建者)可以控制、删除对象。


权限

权限是一个简单的字符串列表,存储在物体和玩家的权限处理程序中。权限可以用来方便地构建访问级别。它由 @perm 命令设置。特别是上面所述的锁函数 perm() 和 pperm() 需要检查它。

比方说,我们有一个 red_key(红钥匙)对象。我们还有一些 red chests(红箱子),希望能用这个钥匙打开它们。
@perm red_key = unlocks_red_chests
这给了 red_key 对象“unlocks_red_chests(打开红箱子)”的权限。接下来,我们锁住红色箱子:
@lock red chest = unlock:perm(unlocks_red_chests)
这把锁需要的是钥匙对象。perm() 锁函数会检查钥匙的权限,只有权限和指定的值相符才会返回true。

最后,我们还要以某种方式来检查锁。比方说,在箱子上有一个命令:open <钥匙>。在命令代码的某个地方需要检测你使用的是什么钥匙,并且判断钥匙是否有正确的权限:
    # self.obj 是箱子
    # used_key 是提供给命令的钥匙参数
    # self.caller 是想要打开箱子的人
    if not self.obj.access(used_key, "unlock"):
        self.caller.msg("钥匙不适合!")
        return 
所有新玩家都会有一套默认的权限,默认权限在 settings.PERMISSION_PLAYER_DEFAULT 中设置。

可以从权限字符串中选出一组字符串,组成一套权限级别系统,级别系统在设置在 settings.PERMISSION_HIERARCHY 的元组中。Evennia默认的权限级别如下:
 Immortals(天神)         # 类似于超级用户,但是会受到锁的影响
 Wizards(巫师)           # 可以管理玩家
 Builders(建造者)        # 可以编辑游戏世界
 PlayerHelpers(玩家指导) # 可以编辑的帮助文件
 Players(玩家)           # 可以聊天、发送消息(默认级别)
权限级别的主要用途是,如果你使用前面所述的 perm() 锁函数设置了权限级别中的某个权限,那具有更高权限的人也可以通过检测。所以如果你拥有“Wizards”权限,那你也可以通过设为 perm(Builders) 的锁以及任何级别低于“Wizards”的锁。

当检查物体或玩家的访问权限时,perm() 锁函数会先检查连接到该物体的玩家的权限,然后再检查物体的权限。如果使用了权限级别(Wizards、Builders等),则只检查玩家的权限(这可以防止玩家通过操纵高等级的角色来提升自己的权限)。如果所需的权限不在权限级别中,则会做精确匹配,首先检查玩家的权限,如果没有找到(或没有连接到玩家),那么再检查物体本身的权限。

以下是如何用 @perm 命令给玩家添加权限:
 @perm/player Tommy = Builders
 @perm/player/del Tommy = Builders # 删除它
注意 /player 的用法。这表示你将权限分配给玩家Tommy,而不是任何名字同样为Tommy的角色。

把权限分配给玩家可以保证不论他们在操纵哪个角色,权限都会保持一致。当分配权限级别时,这一点更要牢记,因为如上面提到的,玩家权限级别会覆盖角色权限级别。所以为了避免混乱,你应该确保将权限级别赋给玩家,而不是他们的角色(请参见下文的降权)。

下面是一个没有连接到玩家的物体的例子
    obj1.permissions = ["Builders", "cool_guy"]
    obj2.locks.add("enter:perm_above(Players) and perm(cool_guy)")

    obj2.access(obj1, "enter") # 返回 True!
另一个被玩家操纵的物体的例子:
    player.permissions = ["Players"]
    puppet.permissions = ["Builders", "cool_guy"]
    obj2.locks.add("enter:perm_above(Players) and perm(cool_guy)")

    obj2.access(puppet, "enter") # 返回 False!


超级用户

通常只有一个超级用户帐户,这是在开始Evennia时首先创建的(用户#1),有时也被称为“所有者”或“上帝”用户。超级用户的权限级别超越了完全访问,它可以完全绕过所有的锁,所以根本不会做任何的检查。这可以让超级用户在紧急情况下始终能够访问所有的东西,但这也会隐藏掉锁定义中可能存在的错误。所以在测试游戏系统时,你要么降权(见下文),要么建立另一个天神级别的角色,这样你就可以让锁得到正确测试了。


降权

@quell 命令可以让 perm() 锁函数忽略玩家的权限而只使用角色的权限。这可以用于例如让工作人员使用较低的权限来作测试。使用 @unquell 可以返回正常的状态。需要注意的是,降权会使用玩家或角色上所有权限级别中最小的权限,因此不能通过 @quell 到高权限角色来提升自己的玩家权限。另外,超级用户也可以用这种降权方式使他们可以受到锁的影响。


更多锁定义的例子

examine: attr(eyesight, excellent) or perm(Builders)
如果你有 “excellent”eyesight(优秀的视力,即你拥有属性 eyesight,其值为 excellent)或者你有“Builders”权限字符串,则允许查看该对象。

open: holds('the green key') or perm(Builder)
这可以用在“门”对象的 open 命令上。如果你是 Builders 或在你的行囊中有正确的钥匙,则可以通过检查。

cmd: perm(Builders)
Evennia的命令处理程序会查找cmd类型的锁,以判断是否允许用户调用指定的命令。在你自定义指令时,这是必须设置的锁。在默认命令列表中有很多例子。如果角色或玩家不能通过 cmd 类型的锁,相关命令甚至不会出现在他们的帮助列表中。

cmd: not perm(no_tell)
“权限”也可以用来阻止用户或实现针对特定对象的禁令。上面这个例子可以设为 tell 命令的锁字符串。这可以让所有没有 no_tell “权限”的人使用tell命令。以后你就可以通过给玩家加上 no_tell “权限”,很方便地阻止他们使用该命令。

    dbref = caller.id
    lockstring = "control:id(%s);examine:perm(Builders);delete:id(%s) or perm(Wizards);get:all()" % (dbref, dbref)
    new_obj.locks.add(lockstring)
这是 @create 命令设置新对象的例子。权限字符串依次做了以下设置:将对象的创建者(即运行 @create 命令的人)设为所有者;Builders可以查看该对象,只有Wizards和创建者才能将它删除;每个人都可以拿起它。


在对象上设置锁的完整例子

假设我们有两个对象:一个是我们自己(不是超级用户),另一个是名为 box 的物体
 > @create/drop box
 > @desc box = "这是一个又大又重的箱子。"
我们想限制可以拿起这个沉重箱子的物体。比方说,需要有 strength(力量)属性并且值大于50才能拿起它。我们从设置自己开始。
 > @set self/strength = 45
好了,为了测试,我们让自己变强壮了,但还不够强壮。现在让我们看看当有人试图拿起箱子时会发生些什么。人们会使用 get 命令(在默认设置中),它在 game/gamesrc/commands/default/general.py 中定义,在它的代码中有以下片断:
    if not obj.access(caller, 'get'):
        if obj.db.get_err_msg:
            caller.msg(obj.db.get_err_msg)
        else:
            caller.msg("你不能拿起它!")
        return
所以 get 命令会检查 get 类型的锁(这并不奇怪)。它还会查找对象上名为 get_err_msg 的属性,以返回用户自定义的错误消息。这不错!让我们从设置箱子的这个属性开始:
 > @set box/get_err_msg = 你不够强壮,无法拿起箱子。
接下来,要在我们的箱子上制作一个 get 类型的锁。我们希望物体必须拥有 strength 属性且具有正确的值才能通过检查。为此,我们需要创建一个能够检查属性是否大于指定值的锁函数。幸运的是在evennia中已经有一个这样的函数了(参见 src/locks/lockfuncs.py),它名为attr_gt。

所以锁字符串应该是这个样子:get:attr_gt(strength, 50)。现在我们把它加到箱子上:
 > @lock box = get:attr_gt(strength, 50)
试试看拿起箱子,你应该能看到返回的消息说你不够强壮。把你的 strength 加到50以上,这样你就可以顺利拿起它了。完成了!一个沉重的箱子!

如果你在python代码中设置,它应该和以下代码类似:
    from ev import create_object

    box = create_object(None, key="box")
    box.locks.add("get:attr_gt(strength, 50)")

    # 或者我们可以立即锁住箱子
    box = create_object(None, key="box", locks="get:attr_gt(strength, 50)")

    # 设置属性
    box.db.desc = "这是一个又大又重的箱子。"
    box.db.get_err_msg = "你不够强壮,无法拿起箱子。"

    # 一个沉重的箱子,可以经受一切,除了最强的...


关于Django的权限系统

Django自身也实现了全面的许可/安全系统。我们不使用它的原因是它是以应用程序为中心的(Django意义上的应用程序)。它的权限字符串是“应用程序名.权限字符串”形式的,它会在应用程序的每个数据库模型中自动添加的三个权限字符串。举个例子,对于应用程序 src/object 添加的是“object.create”、“object.admin”和“object.edit”。这对网络应用程序很有用,对MUD则不然,尤其是当我们想隐藏尽可能多的底层架构的时候。

Django的权限也没有被完全弃用。我们用它来验证登录时的密码。它还被专门用于管理Evennia的基于网页的管理工具(它是Evennia数据库的图形化前端)。你可以在网页上直接编辑、分配这些权限。它是独立于前文所述的权限的。


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