烦恼一般都是想太多了。

0%

Evennia中看命令操纵内容的形式与过程

在 Evennia 中,我们可以通过命令的形式来进行对象的创建,移动等等,构造我们的世界也完全可以用命令的形式,设置,我们可以在游戏内执行 Python 命令等操作。这又是如何操作的呢?

我们大体可以知道,输入,数据的来回路径是如何样的:

MessagePath

Evennia 官方文档 MessagePath 中说明了数据的一个流动路径。

输入:

Client ->
PortalSession ->
PortalSessionhandler ->
(AMP) ->
ServerSessionHandler ->
ServerSession ->
Inputfunc

输出:

msg ->
ServerSession ->
ServerSessionHandler ->
(AMP) ->
PortalSessionHandler ->
PortalSession ->
Client

对于任何玩家用户的输入(封装在 inputfunc)内,经由我们的 cmdhandler 进行处理后,就会执行具体的命令。

比如我们的 create 命令。

最终就是根据我们设置的 class 来实例化一个对象,然后存储到数据库中。

Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
account, errors = Account.create(
username=username, password=password, ip=address, session=session
)

在 DefaultAccount 中,create 方法则会默认把角色一起创建了:

@classmethod
def create(cls, *args, **kwargs):
"""
Creates an Account (or Account/Character pair for MULTISESSION_MODE<2)
with default (or overridden) permissions and having joined them to the
appropriate default channels.

Kwargs:
username (str): Username of Account owner
password (str): Password of Account owner
email (str, optional): Email address of Account owner
ip (str, optional): IP address of requesting connection
guest (bool, optional): Whether or not this is to be a Guest account

permissions (str, optional): Default permissions for the Account
typeclass (str, optional): Typeclass to use for new Account
character_typeclass (str, optional): Typeclass to use for new char
when applicable.

Returns:
account (Account): Account if successfully created; None if not
errors (list): List of error messages in string form

"""

account = None
errors = []

username = kwargs.get("username")
password = kwargs.get("password")
email = kwargs.get("email", "").strip()
guest = kwargs.get("guest", False)

permissions = kwargs.get("permissions", settings.PERMISSION_ACCOUNT_DEFAULT)
typeclass = kwargs.get("typeclass", cls)

ip = kwargs.get("ip", "")
if ip and CREATION_THROTTLE.check(ip):
errors.append(
"You are creating too many accounts. Please log into an existing account."
)
return None, errors

# Normalize username
username = cls.normalize_username(username)

# Validate username
if not guest:
valid, errs = cls.validate_username(username)
if not valid:
# this echoes the restrictions made by django's auth
# module (except not allowing spaces, for convenience of
# logging in).
errors.extend(errs)
return None, errors

# Validate password
# Have to create a dummy Account object to check username similarity
valid, errs = cls.validate_password(password, account=cls(username=username))
if not valid:
errors.extend(errs)
return None, errors

# Check IP and/or name bans
banned = cls.is_banned(username=username, ip=ip)
if banned:
# this is a banned IP or name!
string = (
"|rYou have been banned and cannot continue from here."
"\nIf you feel this ban is in error, please email an admin.|x"
)
errors.append(string)
return None, errors

# everything's ok. Create the new account.
try:
try:
account = create.create_account(
username, email, password, permissions=permissions, typeclass=typeclass
)
logger.log_sec(f"Account Created: {account} (IP: {ip}).")

except Exception as e:
errors.append(
"There was an error creating the Account. If this problem persists, contact an admin."
)
logger.log_trace()
return None, errors

# This needs to be set so the engine knows this account is
# logging in for the first time. (so it knows to call the right
# hooks during login later)
account.db.FIRST_LOGIN = True

# Record IP address of creation, if available
if ip:
account.db.creator_ip = ip

# join the new account to the public channel
pchannel = ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"])
if not pchannel or not pchannel.connect(account):
string = f"New account '{account.key}' could not connect to public channel!"
errors.append(string)
logger.log_err(string)

if account and settings.MULTISESSION_MODE < 2:
# Load the appropriate Character class
character_typeclass = kwargs.get(
"character_typeclass", settings.BASE_CHARACTER_TYPECLASS
)
character_home = kwargs.get("home")
Character = class_from_module(character_typeclass)

# Create the character
character, errs = Character.create(
account.key,
account,
ip=ip,
typeclass=character_typeclass,
permissions=permissions,
home=character_home,
)
errors.extend(errs)

if character:
# Update playable character list
if character not in account.characters:
account.db._playable_characters.append(character)

# We need to set this to have @ic auto-connect to this character
account.db._last_puppet = character

except Exception:
# We are in the middle between logged in and -not, so we have
# to handle tracebacks ourselves at this point. If we don't,
# we won't see any errors at all.
errors.append("An error occurred. Please e-mail an admin if the problem persists.")
logger.log_trace()

# Update the throttle to indicate a new account was created from this IP
if ip and not guest:
CREATION_THROTTLE.update(ip, "Too many accounts being created.")
SIGNAL_ACCOUNT_POST_CREATE.send(sender=account, ip=ip)
return account, errors

OOB

实际上这种命令,被 Evennia 称作 OOB,Out-Of-Band ,也就说在用户的客户端和 Evennia 间的传输数据而不进行相关的提示。比较常见的用途就是用来更新客户端的生命条,控制客户端的按钮按下或在一些地方显示不同的文字。

OOB 数据有一个标准的格式(一个字符串,一个元组,一个字典):

("cmdname", (args), {kwargs})

这通常会被叫做 输入命令 或者 输出命令,根据数据的流向来定。一个 输入命令 的最终端点会是一个与之匹配的 inputfunc。这个函数的格式一般是 cmdname(session, *args, **kwargs),其中 session 指的是这个命令的源 ServerSession。

输出命令被一个匹配的 outfunc 来进行处理。其责任是将Evennia 内部的表示转换为一个适合发送到客户端的格式。它是硬编码的。选择哪个 outfunc 以及如何处理传出数据取决于所连接客户端的性质。只有在开发一个支持 Evennia 协议的时候需要添加 outfunc

text

实际上,大多数时候我们的交互都是使用 text 输入函数的。对于我们的输入,如果是使用 telent 连接,那么会由 telnet 服务器封装成 {'text': [['co g g \n'], {'options': {}}]} 的形式交给SessionHandler,经 AMPSERVER 最终到达 ServerSessionHandler 。照这个格式,就会匹配到我们的 text 输入函数:

if session:
input_debug = session.protocol_flags.get("INPUTDEBUG", False)
for cmdname, (cmdargs, cmdkwargs) in kwargs.items():
cname = cmdname.strip().lower()
try:
cmdkwargs.pop("options", None)
if cname in _INPUT_FUNCS:
_INPUT_FUNCS[cname](session, *cmdargs, **cmdkwargs)
else:
_INPUT_FUNCS["default"](session, cname, *cmdargs, **cmdkwargs)
except Exception as err:
if input_debug:
session.msg(err)
log_trace()

最终,就会将 cmdargs 解析成命令来执行。

输出

我们的数据,一般都是调用 msg() 方法来发送消息的。在 Object, Channel, Account 都有实现这个方法。

  • 对于,Object,Account 没有说的,就是找到此 Object 连接上的 Session 进行发送。
  • 对于 Channel,则是将消息发送给 Account。

对于 msg() 方法,我们所传递给需要发送的数据,不限于是 string,其他任何格式都是可以进行发送的了

if text is not None:
if not (isinstance(text, str) or isinstance(text, tuple)):
# sanitize text before sending across the wire
try:
text = to_str(text)
except Exception:
text = repr(text)
kwargs["text"] = text

但是,转换前,都是试图将其转换成 string 格式的,如果是 string,或者是 tuple 的话,就不用进行转换。

实际上我们可以看到,对于我们要发送的消息,其实也是放在 kwargs 中的 text 键内的。最终,会将由 PortalSessionHandler,根据键中的 text,在寻找一个 send_text 方法。对于 telnet 来说,其只会发送出 cmdargs 中的内容。

而对于 WebSocket ,则是会格式化成为 JSON,来发送

self.sendLine(json.dumps([cmd, args, kwargs]))

比如,我们要发送消息:

obj.msg('你好',{'a':'npc', 'b':'users'})

最终会转成:

["text", "\u8fd9\u91cc\u6709\u4e2a\u8001\u677f", {"a": "npc", "b": "users"}]

发送到 WebSocket 客户端。

而只会发送 字符串到 telnet,对于 kwars 中指定的参数,一般是用来指定相关的设置,通过其他命令发送下去。

Muddery 封装成 Json格式返回

本来,Evennia 中的格式, ("cmdname",(args),{kwargs})是很好的,但是 Muddery全封装成 JSON 格式了,然后呢为了让 WebSocket 使用。事实上,没有必要做这么大的转换,我们完全可以根据 Session 中记录的,是使用的何种客户端来进行不同的返回格式。

如使用 的 Telnet 那么就返回文字,如果返回的 WebSocket 就返回给 JSON 格式,所有的数据都放在 kwargs 里面,这样不是就能做到很好的兼容么。

世界上 muddery ,是将所有的数据,先转换成 JSON 字符串,然后放到 args 中。这就比较蛋疼了。

其在 ServerSession 中重写了 data_out 方法,所以说呢,传输逻辑改变了。

out_text = json.dumps({"data": text, "context": context})
return super(ServerSession, self).data_out(text=out_text, **kwargs)