协议

            Internet│ Protocol
            ┌─────┐ │ | 
┌──────┐    │Text │ │  ┌──────────┐    ┌────────────┐   ┌─────┐
│Client◄────┤JSON ├─┼──┤outputfunc◄────┤commandtuple◄───┤msg()│
└──────┘    │etc  │ │  └──────────┘    └────────────┘   └─────┘
            └─────┘ │
                    │Evennia

协议 描述了 Evennia 如何通过网络向客户端发送和接收数据。每种连接类型(如 telnet、ssh、webclient 等)都有其自己的协议。一些协议也可能有变体(如纯文本 Telnet 与 Telnet SSL)。

请参阅 Message Path 以了解数据如何在 Evennia 中流动的全貌。

在 Evennia 中,PortalSession 代表客户端连接。会话被告知使用特定的协议。发送数据时,会话必须提供一个“Outputfunc”来将通用的 commandtuple 转换为协议能够理解的形式。对于传入的数据,服务器还必须提供合适的 Inputfuncs 来处理发送到服务器的指令。

添加新协议

Evennia 有一个插件系统,可以将协议作为新的“服务”添加到应用程序中。

要向 Portal 或 Server 添加你自己的新服务(例如你自己的自定义客户端协议),请扩展 mygame/server/conf/server_services_pluginsportal_services_plugins

要扩展 Evennia 查找插件的位置,请使用以下设置:

# 添加到服务器
SERVER_SERVICES_PLUGIN_MODULES.append('server.conf.my_server_plugins')
# 或者,如果你想添加到 Portal
PORTAL_SERVICES_PLUGIN_MODULES.append('server.conf.my_portal_plugins')

添加新的客户端连接时,你很可能只需要向 Portal 插件文件中添加新内容。

插件模块必须包含一个函数 start_plugin_services(app),其中 app 参数指的是 Portal/Server 应用程序本身。这在服务器或 Portal 启动时被调用。它必须包含所有需要的启动代码。

示例:

# mygame/server/conf/portal_services_plugins.py

# 这里定义了新的 Portal Twisted 协议
class MyOwnFactory( ... ):
   # [...]

# 一些配置
MYPROC_ENABLED = True # 方便的关闭标志,避免每次都编辑设置
MY_PORT = 6666

def start_plugin_services(portal):
    "这是在启动期间由 Portal 调用的"
    if not MYPROC_ENABLED:
        return
    # 输出以在启动时列出其他服务
    print(f"  myproc: {MY_PORT}")

    # 一些设置(简单示例)
    factory = MyOwnFactory()
    my_service = internet.TCPServer(MY_PORT, factory)
    # 所有 Evennia 服务必须有唯一名称
    my_service.setName("MyService")
    # 添加到主 portal 应用程序
    portal.services.addService(my_service)

一旦定义了模块并在设置中定位,只需重新加载服务器,你的新协议/服务就应该与其他服务一起启动。

编写你自己的协议

重要

这被认为是一个高级主题。

从头编写一个稳定的通信协议不是我们在这里要讨论的内容,这不是一项简单的任务。好消息是 Twisted 提供了许多常见协议的实现,准备好进行适配。

在 Twisted 中编写协议实现通常涉及创建一个从已经存在的 Twisted 协议类和 evennia.server.session.Session(多重继承)继承的类,然后重载该特定协议使用的方法以将它们链接到 Evennia 特定的输入。

这里有一个示例来展示这个概念:

# 在稍后通过 PORTAL_SERVICE_PLUGIN_MODULES 添加到系统的模块中

# 伪代码
from twisted.something import TwistedClient
# 此类用于 Portal 和 Server 会话
from evennia.server.session import Session 

from evennia.server.portal.portalsessionhandler import PORTAL_SESSIONS

class MyCustomClient(TwistedClient, Session): 

    def __init__(self, *args, **kwargs): 
        super().__init__(*args, **kwargs)
        self.sessionhandler = PORTAL_SESSIONS

    # 这些是我们必须知道 TwistedClient 用于通信的方法。名称和参数可能因不同的 Twisted 协议而异。
    def onOpen(self, *args, **kwargs):
        # 假设这是客户端首次连接时调用的

        # 我们需要初始化会话并连接到 sessionhandler。工厂通过 Twisted 父类可用

        client_address = self.getClientAddress()  # 以某种方式获取客户端地址

        self.init_session("mycustom_protocol", client_address, self.factory.sessionhandler)
        self.sessionhandler.connect(self)

    def onClose(self, reason, *args, **kwargs):
        # 当客户端连接断开时调用
        # 链接到 Evennia 等效项
        self.disconnect(reason)

    def onMessage(self, indata, *args, **kwargs): 
        # 随着传入数据调用
        # 根据需要在此处转换        
        self.data_in(data=indata) 

    def sendMessage(self, outdata, *args, **kwargs):
        # 用于发送数据
        # 根据需要修改        
        super().sendMessage(self, outdata, *args, **kwargs)

     # 这些是 Evennia 方法。它们必须全部存在并且看起来完全像这样。上面的 twisted 方法调用它们,反之亦然。这将协议连接到 Evennia 内部。
     
     def disconnect(self, reason=None): 
         """
         连接关闭时调用。
         这也可以由 Evennia 直接调用以手动关闭连接。
         在此处进行任何清理。
         """
         self.sessionhandler.disconnect(self)

     def at_login(self): 
         """
         当此会话通过服务器进行身份验证时调用(如果适用)
         """    

     def data_in(self, **kwargs):
         """
         进入服务器的数据应通过此方法。它应将数据传递到 `sessionhandler.data_in`。这将由 sessionhandler 调用,并使用从协议中找到的适当 send_* 方法获取的数据。
         """
         self.sessionhandler.data_in(self, text=kwargs['data'])

     def data_out(self, **kwargs):
         """
         从服务器发出的数据应通过此方法。它应交给协议的发送方法,无论它叫什么。
         """
         # 我们假设我们有一个 'text' outputfunc
         self.onMessage(kwargs['text'])

     # 'outputfuncs' 被定义为 `send_<outputfunc_name>`。从代码中,它们被调用为 `msg(outfunc_name=<data>)`。

     def send_text(self, txt, *args, **kwargs): 
         """
         发送文本,例如使用 `session.msg(text="foo")`
         """
         # 我们利用了
         self.data_out(text=txt)

     def send_default(self, cmdname, *args, **kwargs): 
         """
         处理所有没有显式 `send_*` 方法来处理的 outputfuncs。
         """
         self.data_out(**{cmdname: str(args)})

这里的原则是重写 Twisted 特定的方法以将输入/输出重定向到 Evennia 特定的方法。

发送数据

要通过此协议发送数据,你需要获取其会话,然后你可以例如:

session.msg(text="foo")

消息将通过系统传递,以便 sessionhandler 将找出会话并检查它是否有 send_text 方法(它有)。然后它将把 “foo” 传递给该方法,在我们的例子中,这意味着通过网络发送 “foo”。

接收数据

仅仅因为协议存在,并不意味着 Evennia 知道如何处理它。必须存在一个 Inputfunc 来接收它。在上面示例的 text 输入的情况下,Evennia 已经处理了此输入——它会将其解析为命令名称及其输入。因此,要处理它,你只需在接收会话(和/或它所控制的对象/角色)上添加一个带有命令的 cmdset。如果没有,你可能需要添加你自己的 Inputfunc(请参阅 Inputfunc 页面以了解如何执行此操作)。

这些在所有协议中可能并不那么明确,但原则是存在的。这四个基本组件——无论它们如何访问——链接到 Portal Session,这是不同低级协议和 Evennia 之间的实际公共接口。