Evennia 游戏中的 Python 系统,允许执行任意的 Python 代码,而只有很少的限制。这样一个系统非常的强力,但也会有潜在的危险,如果我们要使用这样一个系统,那么就要记住其可能出现的危险。但是,这系统的强大又让人需要使用它来实现一些东西。
警告
- 在这个系统中,不受信任玩家的可能会执行 Python 代码。所以要小心的限制谁可以使用这个系统。
- 我们完全可以在游戏外的 Python 中完成这些事情。in-game Python 系统并不是为了完全的替代我们的游戏特性。
最终逻辑
通过创建一个全局脚本,然后将所有的 (对象,事件,回调)都存储在 脚本的属性上:
根据对象的 ID,查事件,然后过滤回调,来执行代码。很给力啊。{
npc: {
'say': [{
'created_on': datetime.datetime(2020, 3, 6, 16, 15, 20, 346957),
'author': g,
'valid': True,
'code': 'character.location.msg_contents("{character} shrugs and says: \'well, yes, hello to you!\'", mapping=dict(character=character))',
'parameters': 'hello',
'updated_on': datetime.datetime(2020, 3, 6, 16, 15, 37, 899715),
'updated_by': g
}, {
'created_on': datetime.datetime(2020, 3, 6, 16, 17, 22, 634880),
'author': g,
'valid': True,
'code': 'character.location.msg_contents("{character} says: \'Bandits he?\'", mapping=dict(character=character))\ncharacter.location.msg_contents("{character} scratches his head, considering.", mapping=dict(character=character))\ncharacter.location.msg_contents("{character} whispers: \'Aye, saw some of them, north from here. No trouble o\' mine, but...\'", mapping=dict(character=character))\nspeaker.msg("{character} looks at you more closely.".format(character=character.get_display_name(speaker)))\nspeaker.msg("{character} continues in a low voice: \'Ain\'t my place to say, but if you need to find \'em, they\'re encamped some distance away from the road, I guess near a cave or something.\'.".format(character=character.get_display_name(speaker)))',
'parameters': 'bandit, bandits',
'updated_on': datetime.datetime(2020, 3, 6, 16, 18, 23, 497149),
'updated_by': g
}]
}
}
基本结构和概念
- in-game Pyhon 的基础是 events(事件)。一个事件 定义了我们想要调用一些 Python 代码上下文。例如,一个定义在 Exits 上的事件在每次有角色通过的时候触发。事件会在
typeclass
上进行描述。所有继承自此typeclass
的对象都可以访问这些事件。 - Callbacks(回调) 可以设置在不同的对象上,或者在代码定义的事件上。这些回调可能包含任意的代码和描述一个对象特定的行为。如果事件被触发,所有连接到此对象事件上的回调都会被执行。
我们在特定场景下观察这个系统,当一个对象被捡起的时候(使用 get
命令),一个特定的事件被触发:
- 事件 get 设置在对象上。(
Object
tyleclass) - 当使用
get
命令来拾取对象的时候,对象的at_get
hook 被调用。 - 一个修改过的 hook 被 事件系统 设置在 DefaultObject 上。这个 hook 会执行(或调用)这个对象上的
get
事件。 - 所有绑定到此
get
事件上的回调都会被按序执行。这些回调就像包含 Python 代码的函数一样工作,这些代码你可以在游戏内编写(使用在编辑回调本身时将列出的特定变量。) - 在不同的回调中,可以在添加多行的 Python 代码。在这个例子中,character 变量将会包含捡起此对象的角色,而 obj 包含的是被捡起的对象。
这样当我们在此对象上建立一个 get
回调,并把他放进来:
character.msg("You have picked up {} and have completed this quest!".format(obj.get_display_name(character))) |
那当我们捡起此对象的时候,就会收到消息:You pick up a sword.
You have picked up a sword and have completed this quest!
安装
这个系统是默认没有安装的。我们需要手动进行安装。步骤如下:
- 启动主脚本
@py evennia.create_script("evennia.contrib.ingame_python.scripts.EventHandler")
- 设置权限(可选):
EVENTS_WITH_VALIDATION
:一个用户组可以编辑回调,但是需要审批(默认是 None)EVENTS_WITHOUT_VALIDATION
:一个组拥有权限来编辑回调,而不需要验证。EVENTS_VALIDATING
:一个用户组可以验证回调。EVENTS_CALENDAR
:日历类型(Node, standard, custom
,默认是 Node)
- 添加
call
命令 - 从 in-game Python 继承我们自定义的
typeclass
:- evennia.contrib.ingame_python.typeclasses.EventCharacter
- evennia.contrib.ingame_python.typeclasses.EventExit
- evennia.contrib.ingame_python.typeclasses.EventObject
- evennia.contrib.ingame_python.typeclasses.EventRoom
下面的各节详细的描述了每个步骤。
注意,如果我们启动的时候没有启动主脚本(如重置了数据库),我们可以会在登录的时候收到错误,告知我们
callback
属性没有定义。这个时候我们重新执行一下主脚本就行了。
启动事件脚本
只需要一个命令就能启动事件脚本:
@py evennia.create_script("evennia.contrib.ingame_python.scripts.EventHandler") |
这个命令会建立一个全局脚本(也就是说,独立于任何对象的脚本)。这个脚本将会保留基本的设置,不同的回调等。我们可以直接访问它,不过我们更应该使用 callback handler(回调管理器)。建立这个脚本,将会在所有的对象上建立一个 callback
管理器。
编辑权限
这个系统添加了三个权限。我们可以在我们的配置文件中直接进行修改。在事件系统定义的配置如下:
EVENTS_WITH_VALIDATION
定义一个可以编辑回调的权限,但是需要审批。如果我们把这个值设置为wizards
,那么拥有wizards
权限的用户就能编辑回调了。这些回调不会被连接,需要被管理员进行检查和审批。这个配置可以包含None
,意味着没有用户可以经过验证来编辑回调。EVENTS_WITHOUT_VALIDATION
定义一个允许编辑回调而不需要验证的权限。默认情况下,这个设置是immortals
,意味着只有这个权限的用户才能进行编辑回调,同时不需要管理员验证就能连接到对象。EVENTS_VALIDATING
:定义谁能验证回调。默认情况下,这被设置为immortals
,只有他们才能看到需要验证的回调,能接受或者拒绝这些验证。
我们可以在我们的配置文件中覆盖这些设置:
# Event settings |
另外,如果我们想使用 时间相关的 事件的话,那么还有一个配置我们必须得设置。需要制定我们使用的日历类型。默认情况下,时间相关的事件是被禁用的。我们可以改变 EVENTS_CALENDAR
的设置来启用:
standard
标准日历,有标准的天,年,月等。custom
一个自定义的日历将会使用 自定义游戏时间 来调度时间
同时也定义了可以在独立的用户上设置的权限:
events_without_validation
这将会给予用户编辑回调,且不经过验证的权限。events_validating
这个允许用户执行需要验证的回调的有效性检查。
具体而言,如果我们想给某个用户编辑回调而不经过任何审批的权限,(如用户 kaldara),我们可能会像下面这样做:
@perm *kaldara = events_without_validation |
移除权限使用 del
:
@perm/del *kaldara = events_without_validation |
使用 @call
权限与这些设置相关:默认情况下,只有拥有events_without_validation
权限和在组 EVENTS_WITH_VALIDATION
内,或此组上的用户才能执行。
添加 @call 命令
from evennia import default_cmds |
改变 typeclass 的父类
如:
from evennia.contrib.ingame_python.typeclasses import EventCharacter |
使用 @call 命令
in-Game
Python 系统依赖于其 @call
命令。谁可以执行这个命令,谁可以对此命令做些什么,依赖于我们设置的权限。
@call
命令允许 添加,编辑和删除特定对象事件的回调。事件系统可以被使用在大多数 Evennia 的对象,基本上所有的 typeclass
对象(除了玩家)。@call
命令的第一个参数是我们想要编辑的对象的名字。其也可以用来知道特定对象上哪些事件是可用的。
测试回调和事件
为了查看某个对象上已连接的事件,可以使用 @call
命令,以其名字或者 ID 作为参数。如 @call here
来测试当前位置事件,或call self
来自己身上的事件。
这个命令会显示一个表,包含:
- 第一列是事件名字
- 第二列,此事件上挂了的回调的数量,及回调的总行数。
- 一个简单的帮助说明回调的用途。
+------------------+---------+-----------------------------------------------+ |
建立新的回调
/add
开关用来添加一个回调。其需要除对象的 名称/dbref 外的 两个参数:
- 在 = 号标志后,指定需要编辑的事件(如果没有指定,将会显示可能的事件列表,如上)
参数(可选)
我们将在后面看到使用参数的回调。但是现在,让我们来尝试组织一个角色通过当前房间的 north 出口。
@call north |
如果我们想要阻止一个角色通过这个出口,最好的事件应该是 can_traverse
。
为什么不是
traverse
,根据文档说明,traverse
是在角色已经通过了出口后调用的。这已经太晚了。
现在我们来编辑这个事件:
@call/add north = can_traverse |
当一个角色想要通过这个出口的时候就会调用此事件。我们可以用 deny()
事件函数来拒绝角色通过。
这个事件中我们可以使用的 变量:
- character: the character that wants to traverse this exit. |
后面的章节会详细的解释 eventfuncs
。不过现在,我们只需要这样做就行了。在打开的编辑器中我们可以输入如下的代码:
if character.id == 1: |
现在我们使用 :wq
来保存这个回调。
如果我们调用 @call north
就能看到有活跃的回调了。如果我们使用 @call north=can_traverse
可以看更详细的信息。
call north |
call north=can_traverse |
call north=can_traverse 1 |
编辑和删除回调
@call/edit
@call/del
用来编辑和删除回调。
使用事件
下面的章节描述了如何用事件来完成很多任务,从最简单的到最复杂的。
eventfuncs
为了使开发变得更容易, in-game Python 系统提供了 eventfuncs
来在回调中使用。我们可以不使用他们,他们只是简化而已。一个 eventfunc 只是一个我们可以在回调代码中使用的函数而已。
Function | Argument | Description | Example |
---|---|---|---|
deny | () |
Prevent an action from happening. | deny() |
get | (**kwargs) |
Get a single object. | char = get(id=1) |
call_event | (obj, name, seconds=0) |
Call another event. | call_event(char, "chain_1", 20) |
deny
用来中断回调的执行及相应的动作。在 can_*
事件中,这用来阻止动作的发生。
get
根据一个特定的标识来获取一个对象。这通常用来根据一个ID 获取对象。
call_event
某些回调会调用其他事件。这是链式事件的一部分。用来立即调用另外一事件,或者在定义的时间调用。
第一个参数是对象,第二个参数是事件的名称,第三个参数是调用事件的延时秒数(默认是0,立即调用)
回调变量
带参调用回调
时间相关的事件
链式事件
代码中使用事件
此节描述了从代码中进行回调和使用事件,如何创建新的事件,如何在命令中调用他们,及如何处理特定的情况(如参数)。
本节中,我们看到如何实现以下例子:我们将会创建一个 push
命令来压入对象。对象会对此命令做出反应,同时触发特定的事件。
添加新的事件
新的事件应该在 typeclass
里面完成。事件被包含在 _events
类变量中,这是一个字典,以事件名称为键,一个描述事件的元组作为值。同时我们需要注册此类来告诉 in-game Python 系统其包含事件。
这里我们在对象上添加 push
事件。在我们的 typeclasses/objects.py
文件中,如此写下:
from evennia.contrib.ingame_python.utils import register_events |
重启游戏,然后我们新建一个对象观察:
create box |
代码中调用事件
in-game Python 系统可以所有对象上都有的 handler 来访问。这个 hanlder 被命名为 callbacks
,且能从任何 typeclass
对象访问(角色,房间,出口等)。这个 handler 提供了几个方法来测试和调用此对象上的一个事件或回调。
想要调用一个事件,使用 callbacks.call
。它需要参数:
- 需要调用的事件名称
- 事件定义的位置参数。这需要按照事件定义的顺序传递过去。
现在,我们已经为所有的对象添加了事件 push
。这个事件当前永远不会被触发。我们应该添加一个 push
命令,使用对象的名称作为参数。如果此对象有效,他就能调用 push
事件。
from commands.command import Command |
这里,我们用以下参数调用 callbacks.call
:
"push"
:调用事件名称self.caller
:使用命令的对象obj
:被push
的对象。
在 push 回调中,我们可以使用 character 变量,和 obj 变量了。
观察效果
@create/drop rock |
我们执行 push rock
就会看到相关的输出了:
You push box(#7).
You push a rock… is… it… going… to… move?
添加新的 eventfuncs
事件函数,如 deny()
,定义在 contrib/events/eventfuncs.py。我们可以自己新建一个文件,命名叫 eventfuncs.py在我们的 world 目录中。这其中的函数将会被添加为辅助函数。
我们同样可以在其他地方进行保存这个文件。不过这样的话我们就需要在 server/conf/settings.py 中设置一个全局变量,只定要搜索的路径:
EVENTFUNCS_LOCATIONS = [ |
创建有参数的事件
当我们想要建立有参数的事件时,我们可以在 _events
中定义事件的时候,在代表值的元组中添加更多的参数。第三个参数必须包含一个在事件触发时会被调用来在回调中进行过滤的回调。经常使用的是两种参数:
- keyword参数:这个事件的回调将根据特定的 keyword 来过滤。
- Phrase 参数:事件的回调将会使用整个 phrase 来过滤,别使用其中的词来检查。
say
命令使用的是 phrase 参数。
from evennia.contrib.ingame_python.utils import register_events, phrase_event |
当我们调用某个事件时,obj.callbacks.call
,我们必须同时提供相应的参数:
obj.callbacks.call(..., parameters="<put parameters here>") |
不提供参数的话,系统俱乐部知道怎么样过滤回调列表。
禁止所有事件
obj.callbacks.call(..., parameters="<put parameters here>") |