6 脚本¶
6.1 所需知识基础¶
在PyMUD中,一切皆脚本。所有操作均可以通过脚本实现,且绝大多数情况下,仅能通过脚本实现。 由于PyMUD是一个基于控制台UI的MUD客户端,因此不能像 zmud/mushclient/mudlet 等客户端那样,在窗口中添加触发、别名等内容。 而PyMUD是基于Python语言的原生实现,因此其实现脚本也是使用 Python 语言。 PyMUD使用异步架构构建了整个程序的核心框架。当然,异步在脚本中不是必须的,它只是一种可选的实现方式而已。 其他客户端都不像PyMUD一样原生完全支持async/await语法下的异步操作,这也是本客户端与其他所有客户端的最核心差异所在。
要能在PyMUD中实现基本的脚本功能,需要掌握以下知识和概念:
Python基本语法与内置类型
面向对象开发(OOP)的核心概念:封装、继承与多态。 百度百科: 面向对象程序设计
函数式编程的基本概念。百度百科: 函数式编程
位置参数与关键字参数的基本概念
基于Python的正则表达式。 Python文档: 正则表达式操作
要在PyMUD中使用脚本的高级功能,则还需要掌握以下内容:
asyncio 包的熟练使用,包括 async/await 语法、coroutine、Task概念与运用、Event/Future 的使用、事件循环等。 Python文档: asyncio - 异步I/O
6.2 一些概念与定义¶
PyMUD 支持的脚本,可以分布在模块、插件中。而模块按其特点,又可以分为主配置模块和子配置模块。 模块的概念与 Python 中模块的概念基本相同。为有效区分PyMUD中的概念,在称谓前增加限定语标识。
- Python 模块:
Python 模块(Module),是一个 Python 文件,以 .py 结尾,包含了 Python 对象定义和Python语句。 模块能够有逻辑地组织你的 Python 代码段。 把相关的代码分配到一个模块里能让你的代码更好用,更易懂。 模块能定义函数,类和变量,模块里也能包含可执行的代码。
- PyMUD模块:
PyMUD模块本身是一个标准的Python模块。为了保证PyMUD应用的正常加载,模块文件应当放在当前目录下,可以以包的形式进行组织。PyMUD支持同时加载任意多个模块。
根据PyMUD模块文件中是否包含继承自 IConfig 的子类型,将PyMUD模块区分为主配置模块和子配置模块两类。
将PyMUD模块放在当前目录下之后,可以在命令行通过
#load xxx指令加载名为xxx的模块(xxx为模块不包括扩展名.py的文件名,下同),也可以将名称放在本地 pymud.cfg 中的 sessions 字典中以进行自动加载。当模块加载完毕之后,可以通过
session.modules[module_name]来访问该模块(一般情况下,不需要访问)。此对象是一个 ModuleInfo 类型,该类型有以下几个可用属性和方法:readonly property name: str # 模块名称,只读属性 readonly property module: ModuleType # 模块对应的Python的ModuleType类型属性, 只读属性 readonly property config: dict # 模块中包含的所有 IConfig 配置类型的实例对象,以类型名为字典key readonly property ismainmodule: bool # 模块是否为主模块(主模块定义是包含了配置类型的模块) load() # 加载模块对象 unload() # 卸载模块对象 reload() # 重新加载模块对象可以在命令行使用 #mods 命令列出本会话中已加载的所有模块清单。利用 #mods modulename1 modulename2 命令列出指定模块的所有配置清单。
- 特别说明:
区分PyMUD模块类型事实上并无绝对意义上的差异。也可以不通过 #load 加载子模块,而在主模块中直接通过 import xxx 加载该模块文件。
但由于PyMUD实现限制, 在脚本修改之后, #reload 仅可以重新加载由 PyMUD 进行管理的模块,也即是通过 #load 命令 或者 Session.load_module 所加载的模块。 因此可以使用PyMUD子配置模块来替代 import xxx 的使用。 对子模块的#reload,需要在引用该子模块的主模块也执行#reload之后才生效。经测试,目前看来,模块的#reload是生效的
- PyMUD子配置模块:
PyMUD模块中不包含 IConfig 子类型的PyMUD模块即属于PyMUD子配置模块。 加载子配置模块时,相当于在 Python 中调用 import xxx 指令。 加载子配置模块后, ModuleInfo 对象的 config 为 空字典
- PyMUD主配置模块:
模块中包含一个或多个 IConfig 子类型的PyMUD模块。该类型构造函数接受一个 pymud.Session 对象作为参数,还接受一个用于指定是否为重新加载的 reload 布尔值标记。 这种模块在加载时,除了如子配置模块执行相同操作之外,还将自动创建所有 IConfig 子类型的实例对象。 所有创建的对象,以其对象类型名称为 key ,保存在 ModuleInfo 对象的 config 字典中.
注: 一个主模块中,可以存在多个配置类型
以下是一个基本的主配置模块的示例:
# filename: mymainmodule.py from pymud import Session # 一个主模块配置类型 class MyConfig(IConfig): def __init__(self, session: Session, *args, **kwargs): super().__init__(session, *args, **kwargs) reload = kwargs.get('reload', False) # 当__unload__里仅包含 super().__unload__()时,该方法可以省略不写,下同 def __unload__(self): super().__unload__() # 另一个主模块配置类型,该类型既是一个自定义命令,也是一个配置类型 # 该配置在模块加载时也会自动创建,因此会话中会自动添加该命令对象 class MyCommand(Command, IConfig): def __init__(self, session: Session, *args, **kwargs): id = kwargs.get('id', 'my_default_triid') super().__init__(session, "myinput", *args, **kwargs) def __unload__(self): super().__unload__() async def execute(self, cmd, *args, **kwargs): self.session.exec("smile")- PyMUD插件:
PyMUD插件本身也是一个标准的 Python模块。插件应放在 pymud包目录的plugins子目录下,或者当前脚本目录的plugins子目录下,在PyMUD启动时自动加载。
插件有相应的插件规范,详细参见 插件
- 模块的unload与reload:
下面给出了测试生效的子模块与主模块的reload与unload的一个示例
# filename: submodule.py # 一个子模块的示例,定义了一个自定义的触发器 from pymud import Trigger, Session class MyTestTrigger(Trigger): def __init__(self, session, *args, **kwargs): super().__init__(session, r'^[>\s]*你嘻嘻地笑了起来.+', onSuccess = self._ontri) def _ontri(self, name, line, wildcards): self.session.exec('haha')# filename: mainmodule.py # 一个主模块的示例,调用了子模块中的触发器 from pymud import SimpleAlias, SimpleTimer, Session, IConfig from submodule import MyTestTrigger class MyConfig(IConfig): def __init__(self, session: Session, *args, **kwargs): super().__init__(session, *args, **kwargs) self.objs = [ SimpleAlias(session, r'^gta$', 'get all;xixi'), SimpleTimer(session, 'xixi', timeout = 10), TestTrigger(session) ] def __unload__(self): self.session.delObjects(self.objs) super().__unload__()
- 以下是测试步骤:
模块的加载与卸载:
在游戏中,通过
#load mainmodule加载该主模块之后,别名、定时器、自定义触发器均生效。此时,子模块是通过import而非load_module加载到当前会话的然后通过
#unload mainmodule卸载该主模块之后,别名、定时器、自定义触发器全部被清除。模块的重新加载
在游戏中,通过
#load mainmodule加载该主模块之后,别名、定时器、自定义触发器均生效。此时,子模块是通过import而非load_module加载到当前会话的此时,修改 submodule.py 的内容,例如将触发后的命令 haha 改为 hehe,保存文件
然后在游戏中,先使用
#load submodule加载该子模块,然后#reload submodule重新加载该子模块,再#reload mainmodule重新加载主模块,此时,子模块的修改会生效。
6.3 变量¶
6.3.1 变量概览¶
从被管理的情况以及访问的范围划分,PyMUD可以使用的变量可以包括三大类:
- Python 变量
即在脚本中,自己定义的 Python 变量对象。此类对象不受 PyMUD 应用管理,当应用退出、会话关闭、脚本重新加载后,变量的结果由脚本代码自行设定,其定义、使用请按照 Python 的语法要求执行。 Python 变量请参考 Python 语言有关文档,此处不再详细展开。
- 单会话访问的变量
即 Session 所属的 Variable 对象。此类对象包括了系统提供的部分变量,以及自行定义的变量。自行定义的变量在会话的所有脚本中都可以直接访问使用,并且可以通过 pymud.cfg 设置(默认已设置),在应用退出、会话关闭、脚本重新加载时,进行了持久化存储操作。 Variable 对象,通过会话对象的属性字典实现和保存。PyMUD 规定,字典的键key作为变量名,必须为 str 类型,值 value 为变量的值,可以为任意 Python 类型,但仍建议采用可以持久化的类型。
- 跨会话访问的变量
即 PyMUD 所属的 Global 对象。此类对象与 Variable 对象区别为,这些对象可以在不同的会话之间进行访问,共享同一个变量对象。 Global 对象通过 PyMudApp 对象的属性字典实现和保存。该对象不会被持久化,字典的键key作为变量名,必须为 str 类型。值可以为任何 Python 支持的类型。
在设计自己脚本的时候,要根据上述不同类型变量的特点,选择合适的类型。 个人建议,默认首选 Variable 类型,若有跨会话访问需求,请选择 Global 类型。对于某些函数或方法中的临时变量,再使用 Python 变量。
6.3.2 单会话访问的变量 (Variable)¶
PyMUD 应用系统本身提供了部分 Variable 变量,这些变量均用 % 开头。其中,部分为单个函数中使用的局部变量,部分为可全局访问使用的变量。 系统提供的 Variable 变量包括:
- %1 ~ %9:
在触发器、别名的同步响应函数中,使用正则匹配的匹配组。 类似于 mushclient 与 zmud 中的 %1 ~ 9%。
- %line:
在触发器、别名的同步响应函数中,匹配的行本身(经ANSI转义处置后的纯文本)。对于多行触发器, %line会返回多行。
- %raw:
在触发器的同步响应函数中,匹配的行本身的原始代码(未经ANSI转义处置)。
- %copy:
使用PyMUD复制功能(非系统复制功能)复制到当前剪贴板中的内容。
变量可以使用 Session 对象提供的方法以及 Session 对象提供的快捷点访问器在脚本中进行操作。也可以使用 #var 命令来进行操作。
会话的变量可以使用 #save 命令保存到会话名对应的.mud文件。当配置中设置了 var_autosave 为 True 时,当会话从远程断开连接时会自动保存。 会话的保存使用了 Python 的 pickle 类型进行处理,因此虽然会话变量的值支持任意 Python类型, 但仍然强烈建议使用可序列化类型。 会话变量保存的一个例外是,若一个变量名是以下划线开头的,则该变量被认为是临时变量,不会被保存到 .mud 文件中。
创建变量/修改变量值的方法:
可以使用 setVariable, setVariables, vars 来创建变量(当变量不存在时)或修改变量值(当变量存在时)。
可以使用 getVariable, getVariables, vars 来读取变量值。
可以使用 delVariable 来移除一个变量。
具体使用示例如下:
from pymud import IConfig, Session, Trigger, SimpleAlias, SimpleTrigger class MyConfig(IConfig): def __init__(self, session: Session, *args, **kwargs): super().__init__(session, *args, **kwargs) self._opVariables() def __unload__(self): super().__unload__() def _opVariables(self): # 系统变量 %line 的使用,直接在 SimpleTrigger 中使用 tri = SimpleTrigger(self.session, r".+告诉你:.+", "#message %line") self.session.addTrigger(tri) # Variable 使用,值类型为 dict 的 Variable money = {'cash': 0, 'gold': 1, 'silver': 50, 'coin': 77} # 将 money 变量值设置为上述字典 self.session.setVariable("money", money) # 在使用时,则这样获取 money = self.session.getVariable("money") # Variable 使用,同时设置多个变量,要求键,值数量相同 money_key = ('cash', 'gold', 'silver', 'coin') money_count = (0, 1, 50, 77) # 以下代码将同时设置4个变量,分别为 cash = 0, gold = 1, silver = 50, coin = 77 self.session.setVariables(money_key, money_count) # 在使用时,则这样获取单个变量 silver = self.session.getVariable("silver") # 也可以同时获取多个变量,并自动使用元组解包 cash, gold = self.session.getVariables(("cash", "gold")) # 可以直接使用快捷点访问器.vars来访问变量,读写均可 self.session.vars.gold = 2 mygold = self.session.vars.gold # 当某个变量不再使用,也不希望保留在变量列表中时,可以用 delVariable 删除 self.session.delVariable('gold') # 以下划线开头的变量,会被视作临时变量,在 #save 时,不会保存到 .mud 文件 self.session.setVariable('_tempVar', 'a TempVar startswith _ will Not Be Saved In .mud File') # 将变量保存到 .mud 文件,此时 _tempVar 这个变量不会被保存 self.session.exec('#save')
6.3.3 跨会话访问的变量 (Global)¶
Global变量用在需要跨多个会话应用相互访问的情况,其使用与 Variable 变量基本相同。一点差异在于,#save 命令存储会话状态时,Global 变量状态不会被保存:
Global变量可以使用 Session 对象提供的方法以及 Session 对象提供的快捷点访问器在脚本中进行操作。也可以使用 #global 命令来进行操作。
创建Global变量/修改Global变量值,可以使用Session类对象的以下方法:
可以使用 session.setGlobal, session.globals 来创建Global变量(当Global变量不存在时)或修改Global变量值(当Global变量存在时)。
可以使用 session.getGlobal, session.globals 来读取Global变量值。
可以使用 session.delGlobal 来移除一个变量。
也可以使用PyMudApp对象的以下方法:
可以使用 app.set_globals, app.globals 来创建Global变量, 用法与 session.setGlobal 和 session.globals 相同。
可以使用 app.get_globals, app.globals 来读取Global变量值, 用法与 session.getGlobal 和 session.globals 相同。
可以使用 app.del_globals, 来移除Global变量, 用法与 session.delGlobal 相同。
具体使用示例如下:
# 文件名: chathook.py (非完整代码,仅用于展示 global 的应用) # 定义一个chathook插件,并供全局各Session使用 from pymud import PyMudApp, Session, Alias class ChatHook: def __init__(self, app: PyMudApp) -> None: self.app = app # 使用 PyMudApp.set_globals 设置一个布尔型全局变量 hooked,指示是否已与chat服务器连接 self.app.set_globals("hooked", False) # 使用 快捷点访问器 将本类型的实例赋值给全局变量 hook,用于各会话中使用该对象并调用对象函数 app.globals.hook = self def start_webhook(self): try: # 使用 PyMudApp.get_globals 获取全局变量 hooked判断是否已与服务器连接 hooked = self.app.get_globals("hooked") if not hooked: asyncio.ensure_future(self.start_webserver()) except Exception as e: # 此处省略 pass def stop_webhook(self): try: # 使用 PyMudApp.get_globals 获取全局变量 hooked 判断是否已与服务器连接 hooked = self.app.get_globals("hooked") if hooked: asyncio.ensure_future(self.stop_webserver()) except Exception as e: # 此处省略 pass async def start_webserver(self): try: # 其他代码省略 # 使用 PyMudApp.set_globals 函数设置 hooked 变量的值 self.app.set_globals("hooked", True) except Exception as e: # 此处省略 pass async def stop_webserver(self): try: if isinstance(self.site, web.TCPSite): # 其他代码省略 # 使用 PyMudApp.set_globals 函数设置 hooked 变量的值 self.app.set_globals("hooked", False) except Exception as e: # 此处省略 pass def sendFullme(self, session, link, extra_text = "FULLME", user = 5): # 此处省略 pass# 文件名: main.py (非完整代码,仅用于展示 global 的应用) # 主脚本函数,调用hook来向远程服务器发送信息 import webbrowser from pymud import Session, IConfig, trigger class MyConfig(IConfig): def __init__(self, session: Session, *args, **kwargs): super().__init__(session, *args, **kwargs) def __unload__(self): super().__unload__() @trigger(id = 'tri_webpage', patterns = r'^http://fullme.pkuxkx.net/robot.php.+$', group = "sys") def ontri_webpage(self, id, line, wildcards): # 使用 session.getGlobal 来获取全局变量 hooked 的值。当不存在该变量时,返回给定默认值False hooked = self.session.getGlobal("hooked", False) if not hooked: webbrowser.open(line) else: user = self.session.getVariable("chat_hook_user", 5) # 使用 session.globals 点访问器来快捷访问全局变量 hook 对象,并直接调用其函数 sendFullme self.session.globals.hook.sendFullme(self.session, line, user = user)
6.4 定时器¶
6.4.1 定时器概览¶
要周期性的执行某段代码,会使用到定时器(Timer)。PyMUD支持多种特性的定时器,并内置实现了 Timer 和 SimpleTimer 两个基础类。 同时,PyMUD还提供了装饰器 @timer 用于快速定义一个定时器。
要在会话中使用定时器,可以:
使用PyMUD提供的 @timer 装饰器快速定义一个定时器
构建一个Timer类(或其子类)的实例。SimpleTimer是系统提供的Timer的子类,用于简单定时器创建。
也可以自定义一个类型,继承自 Timer 类,并同时继承 IConfig 类型,在调用子类构造函数之前指定其他参数默认值。系统在加载该模块文件时,会自动创建该自定义定时器类型实例。
6.4.2 类型定义与构造函数¶
Timer 是定时器的基础类,继承自 BaseObject 类。 SimpleTimer 继承自 Timer ,可以直接用命令而非函数来实现定时器超时的操作。
二者的构造函数分别如下:
class Timer(BaseObject): def __init__(self, session, *args, **kwargs): pass class SimpleTimer(Timer): def __init__(self, session, code, *args, **kwargs): pass除重要的参数session(指定会话)、code(SimpleTimer指定执行代码之外), 其余所有定时器的参数都通过命名参数在kwargs中指定。定时器支持和使用的命名参数、默认值及其含义如下:
id: 唯一标识符。不指定时,默认生成session中此类的唯一标识。
group: 触发器所属的组名,默认为空。支持使用session.enableGroup来进行整组对象的使能/禁用。组的使用方法,详见 6.9 分组对象管理 一节。
enabled: 使能状态,默认为True。标识是否使能该定时器。
timeout: 超时时间,即定时器延时多久后执行操作,默认为10s
oneShot: 单次执行,默认为False。当为True时,定时器仅响应一次,之后自动停止。否则,每隔timeout时间均会执行。
onSuccess: 函数的引用,默认为空。当定时器超时时自动调用的函数,函数类型应为func(id)形式。
code: SimpleTimer独有,定时器到达超时时间后执行的代码串。该代码串类似于zmud的应用,可以用mud命令、别名以分号(;)隔开,也可以在命令之中插入PyMUD支持的#指令。
6.4.3 定时器使用示例¶
下列代码中实现了3个定时器,均用于在莫高窟冥想时,每隔5s发送一次mingxiang命令。 其中一个使用SimpleTimer实现,另一个使用标准Timer实现,并增加了仅在会话连接状态下发送的判断,第三个使用 @timer 装饰器快捷创建。
# examples for Timer and SimpleTimer from pymud import IConfig, Timer, SimpleTimer, Session, timer # 定义一个配置类型 class TimerTest(IConfig): def __init__(self, session: Session, *args, **kwargs): # 调用父类构造函数,传入session参数,以支持装饰器对象的自动创建 super().__init__(session, *args, **kwargs) self._objs = [ # 使用SimpleTimer定义一个默认10s超时的定时器, id自动生成, 超时执行代码 mingxiang. 创建时,系统将自动将该实例加入会话,后通 SimpleTimer(session, code = 'mingxiang'), # 使用Timer定义一个5秒超时的定时器, id为timer2, 并指定本类型的onTimerMX2方法为超时执行函数,创建时默认不使能 Timer(session, timeout = 5, id = 'timer2', enabled = False, onSuccess = self.onTimer2) ] # 在脚本中,可以对指定id的定时器通过 点访问器快速访问 self.session.timers.timer2.enabled = True # 也可以通过标准字典关键字形式访问,并且 timeout 参数还可以动态调整 self.session.timers['timer2'].timeout = 10 def __unload__(self): # 卸载时通过 delObjects 将由代码创建的对象删除 self.delObjects(self._objs) # 调用父类的super().__unload__(),确保装饰器创建的对象也能成功卸载 super().__unload__() # timer2的超时回调函数,该函数由系统自动调用,并传递定时器的 id 作为参数 def onTimer2(self, id, *args, **kwargs): # 定时器超时时若本会话处于连接状态, 则执行代码 mingxiang if self.session.connected: self.session.exec('mingxiang') # timer3直接在其回调函数上使用 @timer 装饰器。除了不传递 session 之外,其他参数均与 Timer 相同 @timer(5, id = 'timer3') def onTimer3(self, id, *args, **kwargs): # 定时器超时时若本会话处于连接状态, 则执行代码 mingxiang if self.session.connected: self.session.exec('mingxiang') # 命令行中,可以使用 #ti, #timer 操作定时器,比如 # #ti timer2 off -> 停止上面创建的定时器2 # #ti timer2 on -> 启动上面创建的定时器2 # #ti timer2 del -> 删除上面创建的定时器2 # #ti timer2 -> 查看定时器2的详细信息 # #ti -> 列出所有会话中的定时器
6.5 别名¶
6.5.1 别名概览¶
当要简化一些输入的MUD命令,或者代入一些参数时,会使用到别名(Alias)。PyMUD支持多种特性的别名,并内置实现了 Alias 和 SimpleAlias 两个基础类。 同时,PyMUD还提供了装饰器 @alias 用于快速定义一个别名。
要在会话中使用别名,可以:
使用PyMUD提供的 @alias 装饰器快速定义一个别名。
构建一个Alias类(或其子类)的实例。SimpleAlias是系统提供的Alias的子类,用于创建简单别名。
也可以自定义一个类型,继承自 Alias 类,并同时继承 IConfig 类型,在调用子类构造函数之前指定其他参数默认值。系统在加载该模块文件时,会自动创建该自定义类型实例。
6.5.2 类型定义与构造函数¶
Alias 是别名的基础类,继承自 MatchObject 类(事实上就是除简写差异外,完全相同)。 SimpleAlias 继承自 Alias ,可以直接用命令而非函数来实现别名触发时的操作。
二者的构造函数分别如下:
class Alias(MatchObject): def __init__(self, session, patterns, *args, **kwargs): pass class SimpleAlias(Alias): def __init__(self, session, patterns, code, *args, **kwargs): pass别名的基础类型 MatchObject 类也是继承自 BaseObject 类,因此,别名通过 kwargs 指定的关键字参数许多都和 Timer 定时器相同。 别名支持和使用的关键字参数、默认值及其含义如下:
- id:
唯一标识符。不指定时,默认生成session中此类的唯一标识。
- group:
别名所属的组名,默认为空。支持使用session.enableGroup来进行整组对象的使能/禁用。组的使用方法,详见 6.9 分组对象管理 一节。
- priority:
优先级,默认100。在对键入命令进行别名触发时会按优先级排序执行,越小优先级越高。
- enabled:
使能状态,默认为True。标识是否使能该别名。
- keepEval:
持续匹配,默认为False。当为True时,别名触发后,不会立即停止匹配,而是继续匹配。
- oneShot:
单次执行,默认为False。当为True时,别名触发后,将删除自身。
- onSuccess:
函数的引用,默认为空。当别名被触发时自动调用的函数,函数类型应为func(id, line, wildcards)形式。
- ignoreCase:
忽略大小写,默认为False。别名模式匹配时是否忽略大小写。
:isRegExp:是否正则表达式,默认为True。即指定的别名模式匹配模式patterns是否为正则表达式。
构造函数中的位置参数含义如下:
- session:
指定的会话对象,必须有
- patterns:
匹配模式,应传递字符串(正则表达式或原始数据)。
- code:
SimpleAlias独有,即别名模式匹配成功后,执行的代码串。该代码串类似于zmud的应用,可以用mud命令、别名以分号(;)隔开,也可以在命令之中插入PyMUD支持的#指令,如#wait(缩写为#wa)
6.5.3 别名使用示例¶
下列代码中实现了多个别名,展示了SimpleAlias, Alias的各种用法
# examples for Alias and SimpleAlias from pymud import IConfig, Alias, SimpleAlias, Session, alias class AliasTest(IConfig): def __init__(self, session: Session, *args, **kwargs): super().__init__(session, *args, **kwargs) self._objs = [ # 使用 SimpleAlias 建立一个简单别名,以 yz_xy 将从扬州中央广场到信阳小广场的路径设置为别名,可以如此建立: SimpleAlias(self.session, "^yz_xy$", "#4 w;nw;#5 w"), # 使用 SimpleAlias 建立一个带参数的简单别名,之后可以使用 gp silver, gp gold, gp letter 等代替 get silver/gold/letter from corpse SimpleAlias(self.session, "^gp\s(.+)$", "get %1 from corpse"), # 使用 Alias 建立一个标准别名,可以扩展 gp 别名的用法,此时,可以使用 gp2 gold 代替 get gold from corpse 2 命令 Alias(self.session, "^gp(\d+)?\s(.+)$", id = "ali_get", onSuccess = self.onali_getfromcorpse) ] # 在脚本中,可以对指定id的别名通过 点访问器快速访问 self.session.alis.ali_get.enabled = False # 也可以通过标准字典关键字形式访问,并且 patterns 参数也还可以动态调整 (别名一般不这样使用) self.session.alis['ali_get'].patterns = "new_patterns" def __unload__(self): # 卸载时通过 delObjects 将由代码创建的对象删除 self.session.delObjects(self._objs) # 调用父类的super().__unload__(),确保装饰器创建的对象也能成功卸载 super().__unload__() # 别名ali_get的成功回调调函数,该函数由系统自动调用,并传递别名的 id、键入的整行 line, 匹配的结果数组 wildcards 作为参数 # 假设键入的命令为 gp2 gold, 则系统调用该函数时,id, line, wildcards 三个参数分别为: # id: 'ali_get' -> 别名的id属性,str类型 # line: 'gp2 gold' -> 键入的完整命令,str类型 # wildcards: ['2', 'gold'] -> 匹配的捕获数据形成的列表(数组),由str类型构成的list类型 def onali_getfromcorpse(self, id, line, wildcards): "别名get xxx from corpse xxx" index = wildcards[0] item = wildcards[1] if index: cmd = f"get {item} from corpse {index}" else: cmd = f"get {item} from corpse" self.session.writeline(cmd) # 也可以通过在回调函数上使用装饰器实现同样的功能 @alias("^gs(\d+)?\s(.+)$") def onali_getfromskeleton(self, id, line, wildcards): "别名get xxx from skeleton xxx" index = wildcards[0] item = wildcards[1] if index: cmd = f"get {item} from skeleton {index}" else: cmd = f"get {item} from skeleton" self.session.writeline(cmd) # 命令行中,可以使用 #ali, #alias 操作别名,比如 # #ali ali_get off -> 停止上面创建的别名 # #ali ali_get on -> 启动上面创建的别名 # #ali ali_get del -> 删除上面创建的别名 # #ali ali_get -> 查看别名的详细信息 # #ali -> 列出会话中的所有别名
6.6 触发器¶
6.6.1 触发器概览¶
当要针对服务器的响应执行对应的操作,则要使用到触发器(Trigger)。PyMUD支持多种特性的触发器,并内置实现了 Trigger 和 SimpleTrigger 两个基础类。
要在会话中使用触发器,需要:
使用PyMUD提供的 @trigger 装饰器快速定义一个触发器。
构建一个Trigger类(或其子类)的实例。SimpleTrigger是系统提供的Trigger的子类,用于创建简单触发器。
也可以自定义一个类型,继承自 Trigger 类,并同时继承 IConfig 类型,在调用子类构造函数之前指定其他参数默认值。系统在加载该模块文件时,会自动创建该自定义类型实例。
6.6.2 类型定义与构造函数¶
Trigger 是触发器的基础类,同 Alias 一样,也是继承自 MatchObject 类。 SimpleTrigger 继承自 Trigger ,可以直接用命令而非函数来实现触发时的操作。
二者的构造函数分别如下:
class Trigger(MatchObject): def __init__(self, session, patterns, *args, **kwargs): pass class SimpleTrigger(Alias): def __init__(self, session, patterns, code, *args, **kwargs): pass触发器也是继承的基础类型 MatchObject ,与别名存在很多相似性。一个是对输入的内容进行匹配后触发相应的操作,另一个时对收到的服务器内容进行匹配后触发响应的操作。 因此,触发器通过 kwargs 指定的关键字参数许多都和 Alias 别名相同。触发器支持和使用的关键字参数、默认值及其含义如下:
与Alias定义基本类似的关键字参数包括:
- id:
唯一标识符。不指定时,默认生成session中此类的唯一标识。
- group:
触发器所属的组名,默认为空。支持使用session.enableGroup来进行整组对象的使能/禁用
- priority:
优先级,默认100。在对收到服务器内容触发时会按优先级排序执行,越小优先级越高。
- enabled:
使能状态,默认为True。标识是否使能该触发器。
- onSuccess:
函数的引用,默认为空。当触发器被触发时自动调用的函数,函数类型应为func(id, line, wildcards)形式。
- ignoreCase:
忽略大小写,默认为False。触发器进行模式匹配时是否忽略大小写。
:isRegExp:是否正则表达式,默认为True。即指定的触发器模式匹配模式patterns是否为正则表达式。
触发器额外生效的关键字参数包括:
keepEval: 匹配成功后持续进行后续匹配,默认为False。当有两个满足相同匹配模式的触发器时,要设置该属性为True,否则第一次匹配成功后,该行不会进行后续触发器匹配(意味着只有最高优先级的触发器会被匹配)
raw: 原始代码匹配,默认为False。当为True时,对MUD服务器的数据原始代码(含ANSI字符、VT100控制指令等)进行匹配。在进行颜色匹配的时候使用。
另外,构造函数中的位置参数含义如下:
- session:
指定的会话对象,必须有
- patterns:
匹配模式,应传递字符串(正则表达式或原始数据)。多行触发时,传递一个匹配模式的列表。
- code:
SimpleAlias独有,即别名模式匹配成功后,执行的代码串。该代码串类似于zmud的应用,可以用mud命令、别名以分号(;)隔开,也可以在命令之中插入PyMUD支持的#指令,如#wait(缩写为#wa)
6.6.3 触发器基本使用示例¶
下列代码中实现了多个触发器,展示了SimpleTrigger, Trigger,以及装饰器 @trigger 的各种用法
# examples for Trigger and SimpleTrigger import webbrowser from pymud import IConfig, Trigger, SimpleTrigger, Session, trigger HP_KEYS = ( "combat_exp", "potential", "max_neili", "neili", "max_jingli", "jingli", "max_qi", "eff_qi", "qi", "max_jing", "eff_jing", "jing", "vigour/qi", "vigour/yuan", "food", "water", "fighting", "busy" ) REGX_HPBRIEF = [ r'^[> ]*#(\d+.?\d*[KM]?),(\d+),(\d+),(\d+),(\d+),(\d+)$', r'^[> ]*#(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)$', r'^[> ]*#(\d+),(\d+),(-?\d+),(-?\d+),(\d+),(\d+)$' ] REGX_WEAR = r"^.+□(?:\x1b\[[\d;]+m)?(身|脚)\S+一[双|个|件|把](?:\x1b\[([\d;]+)m)?([^\x1b\(\)]+)(?:\x1b\[[\d;]+m)?\(.+\)" class TriggerTest(IConfig): def __init__(self, session: Session, *args, **kwargs): super().__init__(session, *args, **kwargs) self._trisList = [ # 简单触发器使用示例: # 在新手任务(平一指配药)任务中,要在要到任务后,自动n一步,并在延时500ms后进行配药;配药完成后自动s,并提交配好的药,并再次接下一个任务,则可以使用SimpleTrigger如此建立触发器: SimpleTrigger(self.session, "^[> ]*你向平一指打听有关『工作』的消息。", "n;#wa 500;peiyao"), SimpleTrigger(self.session, "^[> ]*不知过了多久,你终于把药配完。", "s;#wa 500;give ping yao;#wa 500;ask ping about 工作"), # 标准触发器使用示例: # 当收到有关fullme或者其他图片任务的链接信息时,自动调用浏览器打开该网址,则可以建立一个标准触发器(示例中同时指定了触发器id),并使用lambda函数来作为成功回调: Trigger(self.session, id = 'tri_webpage', patterns = r'^http://fullme.pkuxkx.net/robot.php.+$', onSuccess = lambda id, line, wildcards: webbrowser.open(line)), # 多行触发器使用示例 # 对hpbrief命令的long模式建立三行触发器,获取hpbrief内容并保存到对应的变量中 Trigger(self.session, id = 'tri_hpbrief', patterns = REGX_HPBRIEF, group = "sys", onSuccess = self.ontri_hpbrief), ] # 可以直接使用点访问器操纵触发器对象 self.session.tris.tri_hpbrief.enabled = False # 也可以使用字典访问,还可以动态调整触发器的 patterns 属性 self.sessions.tris['tri_hpbrief'].patterns = ['xxx', 'xxx'] # 如从long的三行模式改为两行触发模式 def __unload__(self): # 通过delObjects从会话中移除所有触发器 self.session.delObjects(self._trisList) # delObjects 支持对象列表形式 # 调用父类的super().__unload__(),确保装饰器创建的对象也能成功卸载 super().__unload__() # hpbrief触发器的成功回调调函数,该函数由系统自动调用,并传递别名的 id、键入的整行 line (多行触发模式下,会返回拼接的多行), 匹配的结果数组 wildcards 作为参数 def ontri_hpbrief(self, id, line, wildcards): "hpbrief自动保存属性变量参数" self.session.setVariables(HP_KEYS, wildcards) # 使用@trigger装饰器定义ANSI触发器使用示例。如果要捕获文字中的颜色、闪烁等特性,则可以使用触发器的raw属性,即使用ANSI触发器。 # 在长安爵位任务中,要同时判断路人身上的衣服和鞋子的颜色和类型时,可以使用如下触发: # 身上穿着look时的成功回调 @trigger(REGX_WEAR, raw = True) def ontri_wear(self, name, line, wildcards): buwei = wildcards[0] # 身体部位,身/脚 color = wildcards[1] # 颜色,30,31,34,35为深色,32,33,36,37为浅色 wear = wildcards[2] # 着装是布衣/丝绸衣服、凉鞋/靴子等等 # 对捕获结果的进一步判断,此处省略 # 现在@trigger装饰器也可以对async def的异步函数进行装饰了(0.22.0新增)。 @trigger(r"^[> ]*你向平一指打听有关『工作』的消息。") async def peiyao(self, id, line, wildcards): self.session.exec("n") await asyncio.sleep(0.5) self.session.exec("peiyao") # 命令行中,可以使用 #tri, #trigger 操作触发器,比如 # #ali tri_hpbrief off -> 停止上面创建的触发器 # #ali tri_hpbrief on -> 启动上面创建的触发器 # #ali tri_hpbrief del -> 删除上面创建的触发器 # #ali tri_hpbrief -> 查看指定触发器详细信息 # #ali -> 列出所有会话中的触发器
6.6.4 异步触发器¶
PyMUD的触发器同时支持同步模式和异步模式。异步触发一般仅用在自定义Command中。
Trigger类的triggered方法是一个async定义的异步函数。可以直接使用await来异步等待触发器的执行。使用异步触发器时,可以不设置onSuccess同步回调函数。
使用异步触发器时,应该使用标准的Trigger类或自定义子类,而不要使用SimpleTrigger,因为其code代码的执行是包含在触发器类的定义中。
当一个触发器同时设置了 onSuccess 回调,并且也使用 await 来异步等待其结果时,其同步回调onSuccess一定在await异步返回之前发生。
以下以一个打坐触发的异步使用为示例说明异步触发器的用法。 在该示例中,dazuo/eat/drink代码不是放在Trigger的触发中的,而且该代码逻辑阅读简便,因为async/await是以同步思维进行的异步实现。 另外,此代码仅用来说明异步触发器的使用示例,若不通过Command进行实现的话,该代码事实上在实际过程中是无法被调用触发的
from pymud import IConfig, Trigger class AsyncTriggerTest(IConfig): def __init__(self, session, *args, **kwargs): super().__init__(session, *args, **kwargs) self._mytri = Trigger(self.session, r"^[> ]*你运功完毕,深深吸了口气,站了起来。", id = "tri_dazuo") def __unload__(self): self.session.delObject(self._mytri) super().__unload__() async def dazuo_always(self): # 本函数仅用来说明异步触发器的使用示例,若不通过Command进行实现的话,该函数在实际过程中无法被调用触发 # 此处仅为了说明异步触发器的使用,假设气是无限的,可以无限打坐 # 目的是每打坐100次,吃干粮,喝酒袋 time = 0 while True: # 永久循环 self.session.writeline("dazuo 10") # 发送打坐命令 # 此处使用了几个技巧 # 1. 使用 tris 快捷访问器 + 触发器 id 来实现获取触发器对象 # 2. 使用 session.create_task而不是asyncio.create_task将触发器的异步触发包装成一个任务。好处时该任务会纳入会话的管理中 # 使用任务包裹async函数,其目的是为了后续可以对任务进行取消,当没有取消需求,也不需要会话管理时,也可以不使用任务包裹 # 即,下面代码也可直接写成: # await self.session.tris.tri_dazuo.triggered() await self.session.create_task(self.session.tris.tri_dazuo.triggered()) # 等待dazuo触发 times += 1 if times > 100: self.session.writeline("eat liang") self.session.writeline("drink jiudai") times = 0
6.7 GMCP触发器 (GMCPTrigger)¶
6.7.1 GMCP触发器概览¶
当要针对服务器的GMCP消息响应执行对应的操作,则要使用到GMCP触发器(GMCPTrigger)。PyMUD内置实现了 GMCPTrigger 来处理GMCP消息的响应。 GMCP触发器调用时通过其id来进行判断的,当存在与服务器数据相同id的GMCPTrigger时,该触发器会被执行。当没有找到匹配id的GMCPTrigger时,会调用默认的打印命令,将收到的GMCP数据打印到当前会话中。 为保持通用性和一致性,GMCPTrigger许多定义与触发器Trigger相同,比如回调函数接受的参数数量与类型相同,也支持异步模式 triggered 函数,可以在命令Command中统一使用。
要在会话中使用GMCP触发器,需要:
使用PyMUD提供的 @gmcp 装饰器快速定义一个GMCP触发器。
创建一个GMCPTrigger类(或其子类)的实例, 并将其 id (参数名为 name) 指定为服务器的GMCP消息的标识(区分大小写)
6.7.2 类型定义与构造函数¶
GMCPTrigger 是GMCP触发器的基础类,继承自 BaseObject 类。 其构造函数如下:
class GMCPTrigger(BaseObject): def __init__(self, session, name, *args, **kwargs): pass构造函数参数中,session 用于指定会话对象, name 指定该GMCP触发对应的服务器名称。 其余参数都通过命名参数在kwargs中指定。支持和使用的命名参数、默认值及其含义如下:
group: GMCP触发器所属的组名,默认为空。支持使用session.enableGroup来进行整组对象的使能/禁用
enabled: 使能状态,默认为True。标识是否使能该定时器。
onSuccess: 函数的引用,默认为空。当定时器超时时自动调用的函数,函数类型应为func(id, line, wildcards)形式。
6.7.3 GMCP触发器使用示例¶
下列代码中展示了GMCPTrigger的用法,对北侠服务器中的 GMCP.Status 类型的GMCP数据进行处理。 北侠服务器 GMCP.Status 类型的GMCP原始数据大概是这样的: GMCP.Status = {"is_busy":"false","is_fighting":"false","fighter_spirit":100,"int":18,"per":18,"dex":11,"potential":63206,"con":33,"str":30}
# examples for GMCPTrigger from pymud import IConfig, GMCPTrigger, Session, gmcp class GMCPTest(IConfig): def __init__(self, session, *args, **kwargs): super().__init__(session, *args, **kwargs) self._gmcp_status = GMCPTrigger(self.session, "GMCP.Status", group = "sys", onSuccess = self.ongmcp_status) def __unload__(self): self.session.delObject(self._gmcp_status) # 调用父类的super().__unload__(),确保装饰器创建的对象也能成功卸载 super().__unload__() ### GMCP处理函数 ### # 系统调用该函数时,会传递三个参数,id 为该GMCP的id, line 为GMCP收到的原始数据, wildcards 为经 eval处理后的数据。 # 比如,对应 GMCP.Status = {"is_busy":"false","is_fighting":"false","fighter_spirit":100,"int":18,"per":18,"dex":11,"potential":63206,"con":33,"str":30} 的这一行数据,三个参数为: # id -> GMCP.Status , str 类型 # line -> {"is_busy":"false","is_fighting":"false","fighter_spirit":100,"int":18,"per":18,"dex":11,"potential":63206,"con":33,"str":30} , str类型 # wildcards -> {"is_busy":"false","is_fighting":"false","fighter_spirit":100,"int":18,"per":18,"dex":11,"potential":63206,"con":33,"str":30} , 此处会被解析成dict类型 def ongmcp_status(self, id, line, wildcards): # 自己的Status和敌人的Status均会使用GMCP.Status发送 # 区别在于,敌人的Status会带有id属性。但登录首次自己也会发送id属性,但同时有很多属性,因此增加一个实战经验属性判定 if isinstance(wildcards, dict): # 正常情况下,GMCP.Status 应该是一个dict,但为保险起见,此处增加一个类型判断 if ("id" in wildcards.keys()) and (not "combat_exp" in wildcards.keys()): # 说明是敌人的状态, 不进行处理 pass else: # 说明个人状态是GMCP Status方式,此时hpbrief将不能使用,设置标识供其他地方判断使用 self.session.setVariable("status_type", "GMCP") # 将收到的数据中的字符串 "true" 和 "false" 转换为布尔类型的 True 和 False,并将数据保存到会话变量中 for key, value in wildcards.items(): if value == "false": value = False elif value == "true": value = True self.session.setVariable(key, value) @gmcp("GMCP.Buff", group = "sys") def ongmcp_buff(self, name, line, wildcards): if isinstance(wildcards, dict): buff = self.session.getVariable("buff", list()) if wildcards["is_end"] == "false": if not wildcards["name"] in buff: buff.append(wildcards["name"]) elif wildcards["name"] in buff: buff.remove(wildcards["name"]) self.session.setVariable("buff", buff) else: self.session.info(line, name)
6.8 命令 (Command)¶
6.8.1 命令概览¶
命令是 PyMUD 的最大特色,也是PyMUD与其他MUD客户端的最大差异所在。它是一组归纳了同步/异步执行、等待响应、处理的集成对象。 可以这么理解,PyMUD的命令就是将MUD的命令输入、返回响应等封装在一起的一种对象。 基于命令可以实现从最基本的MUD命令响应,到最复杂的完整的任务辅助脚本。
Command 基类仅是提供了一个命令的框架,PyMUD应用基于该框架来在运行中调用和处理各类命令。
要在PyMUD中使用命令,不能直接使用 Command 类型,应总是设计自己的命令子类型,继承自 Command 基类,并覆盖基类的 execute 方法。
当对继承Command的自定义命令足够熟悉后,对于某些特定应用场景,可以使用 SimpleCommand 子类来简化代码写法。
要在会话中使用命令,需要:
设计一个 Command 类型的子类类型,并创建一个该子类类型的实例。
也可以将设计的该子类型同时继承 IConfig, 系统将在加载本文件时自动创建该类型。需要注意的是,子类型构造函数中,只能有session一个必须指定参数。
此时,调用该命令,只需在命令行与输入该命令匹配模式(patterns) 匹配的文本即可,也可以在脚本中调用 session.exec 系列方法来调用该命令
6.8.2 类型定义与常用方法¶
Command 也继承自 MatchObject 类。 其构造函数及使用的参数,与Alias完全相同,此处不再列举。
与Alias、Trigger的差异是,Command中包含几个新的会经常被使用的方法调用,如下。
create_task : 实际是session.create_task的包装,在创建任务的同时,除将其加入了session的task清单外,也加入到本Command的Task清单,可以保证执行,也可以供后续操作使用
reset : 复位该任务。复位除了清除标识位之外,还会清除所有未完成的task。在Command的多次调用时,可以手动调用reset方法,以防止同一个命令被多次触发。
unload 或 __unload__ : 卸载方法,子类应该覆盖该方法并在其中清理命令自己添加的各类对象。该方法在Command从会话中移除时自动调用。
execute : async定义的异步方法,子类必须覆盖该方法。该方法在Command被执行时自动调用。
6.8.3 命令使用示例一:CmdMove¶
以下代码设计了一个CmdMove命令,用来处理执行北侠游戏中的移动命令。该命令加入了移动重试功能,当由于某种原因导致行走失败时,可以自动重试5次。
import asyncio from pymud import IConfig, Session, Command, Trigger, GMCPTrigger # 房间名匹配正则表达式 REGX_ROOMNAME = r'^[>]*(?:\s)?(\S.+)\s-\s*(?:杀戮场)?(?:\[(\S+)\]\s*)*(?:㊣\s*)?[★|☆|∞|\s]*$' # 移动命令中的各种方位清单 DIRECTIONS = ( "n","s","w","e","ne","nw","se","sw", "u","d","nu","su","wu","eu","nd","sd","wd","ed", "north", "south", "west", "east", "northeast", "northwest", "southeast", "southwest", "up", "down","northup","southup","westup","eastup","northdown","southdown","westdown","eastdown", "enter(\s\S+)?", "out", "zuan(\s\S+)?", "\d", "leave(\s\S+)?", "jump\s(jiang|out)", "climb(\s(ya|yafeng|up|west|wall|mount))?", "sheshui", "tang", "act zuan to mao wu", "wander", "xiaolu", "cai\s(qinyun|tingxiang|yanziwu)", "row mantuo", "leave\s(\S+)" ) # 移动失败(无法移动)的描述正则匹配清单 MOVE_FAIL = ( r'^[> ]*哎哟,你一头撞在墙上,才发现这个方向没有出路。$', r'^[> ]*这个方向没有出路。$', r'^[> ]*守军拦住了你的去路,大声喝到:干什么的?要想通过先问问我们守将大人!$', ) # 本次移动失败(但可以重新再走的)的描述正则匹配清单 MOVE_RETRY = ( r'^[> ]*你正忙着呢。$', r'^[> ]*你的动作还没有完成,不能移动。$', r'^[> ]*你还在山中跋涉,一时半会恐怕走不出这(六盘山|藏边群山|滇北群山|西南地绵绵群山)!$', r'^[> ]*你一脚深一脚浅地沿着(\S+)向着(\S+)方走去,虽然不快,但离目标越来越近了。', r'^[> ]*你一脚深一脚浅地沿着(\S+)向着(\S+)方走去,跌跌撞撞,几乎在原地打转。', r'^[> ]*你小心翼翼往前挪动,遇到艰险难行处,只好放慢脚步。$', r'^[> ]*山路难行,你不小心给拌了一跤。$', r'^[> ]*你忽然不辨方向,不知道该往哪里走了。', r'^[> ]*走路太快,你没在意脚下,被.+绊了一下。$', r'^[> ]*你不小心被什么东西绊了一下,差点摔个大跟头。$', r'^[> ]*青海湖畔美不胜收,你不由停下脚步,欣赏起了风景。$', r'^[> ]*(荒路|沙石地|沙漠中)几乎没有路了,你走不了那么快。$', r'^[> ]*你小心翼翼往前挪动,生怕一不在意就跌落山下。$', ) # 直接继承Command和IConfig,这样在加载模块时就会自动创建该类型 class CmdMove(Command, IConfig): def __init__(self, session: Session, *args, **kwargs): # 将所有移动相关命令拼接为匹配的正则表达式。 # 当命令行输入命令,或者用exec系列函数调用发送的命令,与本Command的patterns(也就是此处的pattern)匹配时, # 会触发该命令的execute函数执行,并将实际输入的命令传入execute的cmd参数 # 因为移动命令的pattern目前就是这些,因此在代码里将其写死,那么创建CmdMove实例对象是就无需重复指定 patterns kwargs.setdefault("id", "cmd_move") pattern = r"^({0})$".format("|".join(DIRECTIONS)) super().__init__(session, patterns = pattern, *args, **kwargs) # 所有触发器都相同的公共参数,减少后面创建触发器时的代码输入 tris_kwargs_default = { "enabled" : False, "keepEval" : True, "priority" : 90, "timeout" : 5, } # 将所有命令对象放到 _objs 数组中,用于 __unload__ 时卸载 # 由于在本命令中的触发器全部使用异步模式,因此所有触发器都没有配置 onSuccess 函数,保留默认即可。 # 因为后续所有的判断也无需使用触发器 id ,因此所有的id 都使用系统自动设置的默认值,不再配置。 # 异步触发器获取是否触发使用 await tri.triggered() 的方式处理 self._objs = [ # 当移动命令成功之后,正常应该收到房间名称,因此将房间名称作为成功的触发匹配。将匹配成功的触发器组名设置为 moving.move.success 用于后续判断 Trigger(self.session, REGX_ROOMNAME, group = "moving.move.success", **tris_kwargs_default) ] # 当移动失败(没路,无需重试)之后,可能收到一个表示失败的消息,目前梳理的所有失败消息都在 MOVE_FAIL 定义中列举。将所有这些消息都分别设置为触发器,表示移动失败。将所有移动失败的触发器组名设置为 moving.move.fail 用于后续判断 # 将创建的所有表示失败的触发器都放入 self._objs 数组,以便后面 __unload__ 能正常卸载 # 这些触发器也一样,都是用异步模式,因此无需配置 onSuccess 函数 for s in MOVE_FAIL: self._objs.append(Trigger(self.session, patterns = s, group = "moving.move.fail", **tris_kwargs_default)) # 当移动失败(有路,但由于各种原因未移动成功)之后,可能收到一个表示失败(可以重试)的消息,目前梳理的所有消息都放在 MOVE_RETRY 中。 # 与上面类似,这种失败的触发器组名设置为 moving.move.retry 一共后续判断。 for s in MOVE_RETRY: self._objs.append(Trigger(self.session, patterns = s, group = "moving.move.retry", **tris_kwargs_default)) def __unload__(self): # 卸载函数中,将所有 _objs 中的对象从会话中移除 self.session.delObjects(self._objs) # 以下内容为该Command的主执行函数。当正常触发了该命令时,pymud会自动调用该函数,并将实际命令通过cmd参数传递到函数中 # 因此,移动动作的所有处理均放在此函数中。 # 假设输入一个命令之后,服务器可能的响应有以下几种可能: # 1. 该方向有出路,且移动成功,成功走到一个新的房间,因此可以收到服务器的 房间名 一行信息,此时, moving.move.success 组的这个触发器会被触发; # 2. 该方向没有出路,移动失败,服务器会返回类似『你一头撞在墙上』的表示失败的信息,此时, moving.move.fail 组中的某一个触发器会被触发; # 3. 该方向有出路,但由于busy或其他导致移动失败,服务器会返回类似『你正忙着呢』表示失败(但实际可以走过去)的消息,此时, moving.move.retry 组中的某一个触发器会被触发; # 4. 由于角色处于昏迷状态,或者网络延迟原因,等待好长一段时间(此处给定默认值为 self.timeout = 5秒)后,都没有收到上述3中情况的任意反馈,此时,我们认为命令执行超时。 # 下面的处理,就是在送出命令之后,识别到底是哪一种情况,再根据情况判断后续执行操作。 # 因为是异步函数,增加一个 async_exception 异常处理的装饰器,在这里如果代码运行错误后,会打印到session中 @async_exception async def execute(self, cmd, *args, **kwargs): # type: ignore # 复位本命令,请保留,暂时无需关注细节 self.reset() # 定义一个重试的次数参数 retry_times = 0 # 使能本命令创建的所有触发器。使用 subgroup 参数配置,让所有组名以 moving.move开头的组内的所有对象均开启 enabled self.session.enableGroup(group = "moving.move", enabled = True, subgroup = True) # 先将结果状态设置为 NOTSET,表示 未设置 result = self.NOTSET # 最多循环 MAX_RETRY_TIMES 次,用于处理 retry 情况 while retry_times < MAX_RETRY_TIMES: # 将所有触发器的异步触发状态 triggered() 生成任务,供异步触发判断使用。有关 tri.triggered() ,可以把鼠标放在下面的 tri.triggered() 上,查看文档字符串帮助 # 此处使用了 Python 的列表推导语句,简化代码输入 # 实际内容就是将上面 self._objs 中的每一个触发器,都调用 tri.triggered() 以生成协程对象,再使用 create_task 包裹成任务,供后续使用 # 相当于这么写的代码: # tasklist = [] # for tri in self._objs: # tasklist.append(self.create_task(tri.triggered())) tasklist = [self.create_task(tri.triggered()) for tri in self._objs] # 下面这一句是关键,表示向服务器发出 cmd 命令,然后等待 tasklist 里涉及的所有触发器中的第一个被触发,或者等待时间达到 timeout 秒 # self.session.waitfor 是为了简化写法。实际相当于三步命令的整合: # await asyncio.sleep(0.05) # 将CPU的执行时间从本函数中断0.05秒,暂时不需要关注此处细节 # self.session.writeline(cmd) # 向服务器发送 cmd 命令 # done, pending = await asyncio.wait(tasklist, timeout = self.timeout, return_when = "FIRST_COMPLETED") # 等待 tasklist 中的任务第一个完成(也就是触发器被触发),或者超时。此处 FIRST_COMPLETED 就是指示等待第一个完成后结束 # 上面有关 asyncio.wait 的详细信息,可以参考 Python 的官方文档, asyncio 库的说明 done, pending = await self.session.waitfor(cmd, asyncio.wait(tasklist, timeout = self.timeout, return_when = "FIRST_COMPLETED")) # type: ignore # 上述代码执行完毕后,返回两个 set, done表示已完成的任务列表, pending 表示还在等待状态的任务列表 # 当执行到此处时,首先,将所有还在等待状态的任务列表取消掉,因为到这里都还没有被触发,那么这些触发器在本次命令执行过程中不可能再被触发了。 tasks_pending = list(pending) for t in tasks_pending: self.remove_task(t) # 获取已经完成的任务列表。由于set不能以下标访问内容,因此先转换为 list tasks_done = list(done) # 如果 task_done 里的任务数大于0 (即被触发的触发器数量>0)。根据北侠逻辑,被触发的触发器最多只可能有1个(或者超时的话,就1个都没有) if len(tasks_done) > 0: # 那么,从完成的任务中取出第1个任务,即为实际被触发的触发器 task = tasks_done[0] # 通过对任务调用 task.result(),可以获取该触发器的触发结果,即 tri.triggered() 的返回结果。结果包括4个数值,分别为 state, id, line, wildcards。 # 其中,触发器成功触发后,返回的 state 一定为 SUCCESS,因此此处将第一个结果丢弃,仅去后3个结果,即 id, line, wildcards # id, line, wildcards三个参数,和 onSuccess 回调时,函数里收到的这三个参数内容完全一致 # 因此,后面就可以通过对这3个参数的解析,判断到底是哪一个触发器被成功触发了。 _, id, line, wildcards = task.result() # 先通过返回的 id 获取实际被触发的触发器 tri = self.session.tris[id] # 对触发器进行判断,看是哪一个 # 如果该触发器的组名为 moving.move.success,表示收到了新的房间标题内容,即移动成功 # 成功后,就不再执行 while 循环内容了,返回 SUCCESS 状态,并通过 break 中止循环 if tri.group == "moving.move.success": result = self.SUCCESS break # 如果该触发器的组名为 moving.move.fail,表示收到了 MOVE_FAIL 中的某一个内容的触发 # 因为这种情况表示是该方向没有路,因此 self.error 打印出来该信息,并且返回 FAILURE # 没有路,也不需要再执行 while 循环的内容了,直接通过 break 中止循环 elif tri.group == "moving.move.fail": self.error(f'执行{cmd},移动失败,错误信息为{line}', '移动插件') result = self.FAILURE break # 如果该触发器的组名为 moving.move.retry,表示收到了 MOVE_RETRY 中的某一个内容的触发 # 因为这种情况表示是该方向有路,但本次移动失败,因此重试次数加一,并延迟2秒,然后会回到 while 循环处,再执行下一轮次 # 因为有路,也不需要再执行 while 循环的内容了,直接通过 break 中止循环 elif tri.group == "moving.move.retry": retry_times += 1 await asyncio.sleep(2) # 如果 task_done 里的任务为0,表示没有任何触发器被触发,此时就是超过了等待的 timeout 时间,表示超时 # 当超时时,设置 TIMEOUT 标记,然后break中止循环。因为超时后,也不需要重试了。 else: self.warning(f'执行{cmd}超时{self.timeout}秒', '移动插件') result = self.TIMEOUT break # 执行到这里,本次命令全部执行完毕,此时将所以触发器都关掉,减轻对其他命令或触发器判断的干扰 self.session.enableGroup(f"{PLUGIN_NAME}.move", False) # 返回前面设置的的 result 值。此处的返回值,是让本 Command 被其他地方调用时,判断命令执行完后状态的标记 # 如果其他地方有类似 result = self.session.exec_async("w") 的命令, 返回的 result 就是此处数值 return result这种命令设计方式能带来很多益处。 其中一个是,使用这种 Command 方式可以确保该命令被执行完成,而且还可以根据命令的返回值来判定下一步该执行操作。 另外,这种 Command 不需要额外记忆其他命令,直接使用MUD中的命令即可触发该 Command 对象。 在上述CmdMove命令创建完成之后,在命令行中键入任意方向(DIRECTIONS中列出的所有可能匹配项)行走,都会触发调用该命令的execute方法。 另外,在代码中也可以使用以下方式来调用该命令:
# 方式一: 直接使用session方法同步调用。由于同步调用会立即返回,因此该调用方法无发获取返回值 self.session.exec('e') self.session.exec('s;#wa 100;e;#wa 100;s') # 还可以在调用中同时指定多个命令。通过 CmdMove 设计中的重试机制,可以确保三步行走到对应的位置 # 方式二: 直接使用session方法异步调用, 该调用方法可以获取返回值, 但这样使用由于需要搜索命令,因此会存在一些性能损失 result = await self.session.exec_async('e') # 此处 e 会被匹配为 CmdMove 运行,因此其返回值即为 CmdMove 的 execute 方法运行的返回值。若未被匹配为某个 Command 对象,则返回 None result = await self.session.exec_async('s;e;s') # 异步调用中也可以同时指定多个命令,但此时返回值为最后一个命令的返回值。 # 方式三: 直接调用该命令的execute方法, 该调用方法也可以获取返回值,这种性能损失最小,并且也可以延迟到对象调用时刻再获取 # 这种方式下,execute 只能接受一条指令,不能像前面一样传入 "s;e;s" 这种连续指令。 result = await self.session.cmds.cmd_move.execute("w") result = await self.session.cmds["cmd_move"].execute("w") # 与上面一行等价 # 上面建议使用方式三来进行命令调用,因为这种调用将获取命令对象实例延迟到调用的时刻。如果修改了模块配置需要 #reload 的时候,引用此命令的模块不需要重新 #reload。方式二虽然有相同效果,但是方式二存在 # 在确保命令执行完毕后,并根据返回结果判断下一步处置: if result == self.SUCCESS: # 成功后的代码 self.session.exec('buy jiudai') pass elif result == self.FAILURE: # 失败后的代码 pass elif result == self.TIMEOUT: # 超时之后的代码 pass
6.8.4 命令使用示例二:CmdDazuoto¶
以下代码设计了一个CmdDazuoto命令,用来处理执行北侠游戏中的打坐有关的事项。 要使用该命令,也应该在创建一个命令的实例,并添加到会话中。有关代码此处省略。 之后,可以通过命令行键入 dzt xxx 来执行不同的打坐 并且,'dzt;e;s;n' 这种键入方式也可以确保移动是在打坐完成之后才进行。
import re, traceback, math from pymud import IConfig, Command, Session, Trigger # 本Command引用了其他三个设计好的Command,分别用于处理 'jifa/enable'命令, 'hpbrief' 命令, 以及各类生活命令(吃、喝) class CmdDazuoto(Command, IConfig): """ 各种打坐的统一命令, 使用方法: dzt 0 或 dzt always: 一直打坐 dzt 1 或 dzt once: 执行一次dazuo max dzt 或 dzt max: 持续执行dazuo max,直到内力到达接近2*maxneili后停止 dzt dz: 使用dz命令一直dz dzt stop: 安全终止一直打坐命令 """ def __init__(self, session, *args, **kwargs): id = kwargs.get("id", "cmd_dazuoto") # 配置id默认值供自动加载使用 super().__init__(session, "^(dzt)(?:\s+(\S+))?$", *args, **kwargs) self._triggers = {} self._initTriggers() self._force_level = 0 # 内功激发后有效等级 self._dazuo_point = 10 # 每次打坐点数,默认为10 self._halted = False def _initTriggers(self): self._triggers["tri_dz_done"] = self.tri_dz_done = Trigger(self.session, r'^[> ]*(..\.\.)*你运功完毕,深深吸了口气,站了起来。', id = "tri_dz_done", keepEval = True, group = "dazuoto") self._triggers["tri_dz_noqi"] = self.tri_dz_noqi = Trigger(self.session, r'^[> ]*你现在的气太少了,无法产生内息运行全身经脉。|^[> ]*你现在气血严重不足,无法满足打坐最小要求。|^[> ]*你现在的气太少了,无法产生内息运行小周天。', id = "tri_dz_noqi", group = "dazuoto") self._triggers["tri_dz_nojing"] = self.tri_dz_nojing = Trigger(self.session, r'^[> ]*你现在精不够,无法控制内息的流动!', id = "tri_dz_nojing", group = "dazuoto") self._triggers["tri_dz_wait"] = self.tri_dz_wait = Trigger(self.session, r'^[> ]*你正在运行内功加速全身气血恢复,无法静下心来搬运真气。', id = "tri_dz_wait", group = "dazuoto") self._triggers["tri_dz_halt"] = self.tri_dz_halt = Trigger(self.session, r'^[> ]*你把正在运行的真气强行压回丹田,站了起来。', id = "tri_dz_halt", group = "dazuoto") self._triggers["tri_dz_finish"] = self.tri_dz_finish = Trigger(self.session, r'^[> ]*你现在内力接近圆满状态。', id = "tri_dz_finish", group = "dazuoto") self._triggers["tri_dz_dz"] = self.tri_dz_dz = Trigger(self.session, r'^[> ]*你将运转于全身经脉间的内息收回丹田,深深吸了口气,站了起来。|^[> ]*你的内力增加了!!', id = "tri_dz_dz", group = "dazuoto") def __unload__(self): self.session.delObjects(self._triggers) # 各种打坐的具体逻辑处理 async def dazuo_to(self, to): # 开始打坐 dazuo_times = 0 # 记录次数,用于到次数补充食物和水 self.tri_dz_done.enabled = True # 首次执行时,调用 jifa命令以获取有效内功等级 if not self._force_level: await self.session.exec_async("enable") # 此处调用了其他模块中设计的 cmdEnable 命令 force_info = self.session.getVariable("eff-force", ("none", 0)) self._force_level = force_info[1] # 根据有效内功等级,设置每次打坐的点数。具体为:有效等级-5后除以10圆整,最小为10 self._dazuo_point = (self._force_level - 5) // 10 if self._dazuo_point < 10: self._dazuo_point = 10 # 通过hpbrief命令获取当前的各种状态。若状态模式使用GMCP时,自动从GMCP中获取 if self.session.getVariable("status_type", "hpbrief") == "hpbrief": await self.session.exec_async("hpbrief") # 此处调用了其他模块中设计的 cmdHpbrief命令 # 根据hpbrief命令或者自动从GMCP中获取的数据,取出当前内力、最大内力 neili = int(self.session.getVariable("neili", 0)) maxneili = int(self.session.getVariable("max_neili", 0)) # 设置触发器等待超时时间,一般情况下10秒,当执行dz 或者 dazuo max 时,需要等待的时间都可能超过10s,因此设置一个大值 TIMEOUT_DEFAULT = 10 TIMEOUT_MAX = 360 timeout = TIMEOUT_DEFAULT # 根据不同参数,设置不同的相关命令和提示 if to == "dz": cmd_dazuo = "dz" timeout = TIMEOUT_MAX self.tri_dz_dz.enabled = True self.info('即将开始进行dz,以实现小周天循环', '打坐') elif to == "max": cmd_dazuo = "dazuo max" timeout = TIMEOUT_MAX need = math.floor(1.90 * maxneili) self.info('当前内力:{},需打坐到:{},还需{}, 打坐命令{}'.format(neili, need, need - neili, cmd_dazuo), '打坐') elif to == "once": cmd_dazuo = "dazuo max" timeout = TIMEOUT_MAX self.info('将打坐1次 {dazuo max}.', '打坐') else: cmd_dazuo = f"dazuo {self._dazuo_point}" self.info('开始持续打坐, 打坐命令 {}'.format(cmd_dazuo), '打坐') # 各类打坐命令的主循环 while (to == "dz") or (to == "always") or (neili / maxneili < 1.90): if self._halted: self.info("打坐任务已被手动中止。", '打坐') break waited_tris = [] waited_tris.append(self.create_task(self.tri_dz_done.triggered())) waited_tris.append(self.create_task(self.tri_dz_noqi.triggered())) waited_tris.append(self.create_task(self.tri_dz_nojing.triggered())) waited_tris.append(self.create_task(self.tri_dz_wait.triggered())) waited_tris.append(self.create_task(self.tri_dz_halt.triggered())) if to != "dz": waited_tris.append(self.create_task(self.tri_dz_finish.triggered())) else: waited_tris.append(self.create_task(self.tri_dz_dz.triggered())) done, pending = await self.session.waitfor(cmd_dazuo, asyncio.wait(waited_tris, timeout = timeout, return_when = "FIRST_COMPLETED")) for t in list(pending): self.remove_task(t) tasks_done = list(done) if len(tasks_done) == 0: # 这里表示超时了 self.info('打坐中发生了超时问题,将会继续重新来过', '打坐') elif len(tasks_done) == 1: task = tasks_done[0] _, name, _, _ = task.result() # 若完成的触发器任务是 tri_dz_done 或者 tri_dz_dz, 根据to的不同判断如何进行后续 if name in (self.tri_dz_done.id, self.tri_dz_dz.id): if (to == "always"): dazuo_times += 1 if dazuo_times > 100: # 此处,每打坐100次,补满水食物 self.info('该吃东西了', '打坐') await self.session.exe_async("feed") # 此处调用了其他模块设计的吃喝命令 dazuo_times = 0 elif (to == "dz"): dazuo_times += 1 if dazuo_times > 50: # 此处,每打坐50次,补满水食物 self.info('该吃东西了', '打坐') await self.session.exe_async("feed") # 此处调用了其他模块设计的吃喝命令 dazuo_times = 0 elif (to == "max"): # 当执行max后,如果有效内功大于161级,吸个气 if self._force_level >= 161: self.session.writeline("exert recover") await asyncio.sleep(0.2) elif (to == "once"): self.info('打坐1次任务已成功完成.', '打坐') break # 若捕获到 noqi 的触发器(你的气不足),根据有效内功等级判断处理。当161以上使用正循环,即吸气后继续;当小于时,等待(发呆)15秒后继续打坐 elif name == self.tri_dz_noqi.id: if self._force_level >= 161: await asyncio.sleep(0.1) self.session.writeline("exert recover") await asyncio.sleep(0.1) else: await asyncio.sleep(15) # 若捕获到 nojing 的触发器(你的精不足),直接吸气 elif name == self.tri_dz_nojing.id: await asyncio.sleep(1) self.session.writeline("exert regenerate") await asyncio.sleep(1) # 若捕获触发器为 dz_wait (处于exert qi/exert jing过程中),等待5秒 elif name == self.tri_dz_wait.id: await asyncio.sleep(5) # 若捕获到人工halt命令输入后,终止本循环 elif name == self.tri_dz_halt.id: self.info("打坐已被手动halt中止。", '打坐') break # 若捕获到最大内力触发器,终止本循环 elif name == self.tri_dz_finish.id: self.info("内力已最大,将停止打坐。", '打坐') break self.info('已成功完成', '打坐') self.tri_dz_done.enabled = False self.tri_dz_dz.enabled = False self._onSuccess() return self.SUCCESS async def execute(self, cmd, *args, **kwargs): try: self.reset() if cmd: m = re.match(self.patterns, cmd) if m: cmd_type = m[1] param = m[2] self._halted = False if param == "stop": self._halted = True self.info('已被人工终止,即将在本次打坐完成后结束。', '打坐') return self.SUCCESS elif param in ("dz",): return await self.dazuo_to("dz") elif param in ("0", "always"): return await self.dazuo_to("always") elif param in ("1", "once"): return await self.dazuo_to("once") elif not param or param == "max": return await self.dazuo_to("max") except Exception as e: self.error(f"异步执行中遇到异常, {e}, 类型为 {type(e)}") self.error(f"异常追踪为: {traceback.format_exc()}")
6.8.5 SimpleCommand示例¶
在已经理解了 Command 用法之后,在某些特定情况下,可以使用 SimpleCommand 来简化代码。 SimpleCommand 类型的构造函数如下:
class SimpleCommand(Command) def __init__(self, session, patterns, succ_tri, *args, **kwargs): pass可以看出,相对于Command, SimpleCommand 的位置参数中多了一个 succ_tri,用于指定表示成功的触发器。位置参数意味着必须指定该属性。 另外,在 kwargs 的关键字参数中, SimpleCommand 多出了 fail_tri 和 retry_tri 两个字段,用于指定失败和重试的触发器。在关键字参数中意味着可以不指定。
以上面示例一, CmdMove 命令来讲解如何使用 SimpleCommand 简化代码:
aSimpleMove = SimpleCommand( session, "^({0})$".format("|".join(DIRECTIONS)), succ_tri = Trigger(self.session, REGX_ROOMNAME, id = "tri_move_succ", group = "cmdmove", keepEval = True, enabled = False), fail_tri = [Trigger(self.session, patterns = s, id = f"tri_move_fail{idx}", group = "cmdmove", enabled = False") for s in MOVE_FAIL], retry_tri = [Trigger(self.session, patterns = s, id = f"tri_move_retry{idx}", group = "cmdmove", enabled = False")], timeout = 10 )其 execute 方法有一个默认调用,当命令输入后,若触发的是succ_tri中的对象(例中只有一个),则返回 SUCCESS, 若触发的是 fail_tri 中的对象,则返回 FAIL, 若触发的是 retry_tri 中的对象,则重试 SimpleCommand.MAX_RETRY 次数(20)。若超过 timeout 指定的超时时间(未指定时默认10s),则返回 TIEMOUT。
从上面的示例可以看出, SimpleCommand 只是简化了代码写法,因此应该被翻译为“简化的Command”,而不是“简单的Command” :)
SimpleCommand 使用局限性太大,除了极少数懒得写代码的场景,我个人已经不使用 SimpleCommand 而是使用自定义类继承 Command 来解决一切问题了。
6.9 状态栏与状态窗口¶
状态栏是指命令行下面的灰色背景的栏目,其左边部分可以通过代码设置显示纯文本信息。设置代码为:
session.application.set_status('您要显示的信息')可以通过 pymud.cfg 文件中的 status_display , status_width, status_height 的组合使用设置状态窗口的显示位置和尺寸,可以显示在下方、右方或不显示。
状态栏通过脚本定制状态窗口内容。要定制状态窗口的显示内容,将session.status_maker属性赋值为一个返回支持显示结果的函数即可。可以支持标准字符串或者prompt_toolkit所支持的格式化显示内容。
有关prompt_toolkit的格式化字符串显示,可以参见该库的官方帮助页面: https://python-prompt-toolkit.readthedocs.io/en/master/pages/printing_text.html
以下是一个实现状态窗口的示例,使用了 FormattedTextTuple 形式展示了带格式并且可以支持鼠标操作的状态窗口。显示效果见下图
from pymud import exception, async_exception, Session class MyStatusWindowConfig(IConfig): def __init__(self, session, *args, **kwargs): super().__init__(session, *args, **kwargs) self.session.status_maker = self.status_window def __unload__(self): super().__unload__() # 创建自定义的健康条用作分隔符 def create_status_bar(self, current, effective, maximum, barlength = 20, barstyle = "—"): from wcwidth import wcswidth barline = list() stylewidth = wcswidth(barstyle) filled_length = int(round(barlength * current / maximum / stylewidth)) # 计算有效健康值部分的长度 effective_length = int(round(barlength * effective / maximum / stylewidth)) # 计算剩余部分长度 remaining_length = barlength - effective_length # 构造健康条 barline.append(("fg:lightcyan", barstyle * filled_length)) barline.append(("fg:yellow", barstyle * (effective_length - filled_length))) barline.append(("fg:red", barstyle * remaining_length)) return barline # 自定义状态栏窗口 def status_window(self): from pymud.settings import Settings try: formatted_list = list() # line 0. hp bar jing = self.session.getVariable("jing", 0) effjing = self.session.getVariable("eff_jing", 0) maxjing = self.session.getVariable("max_jing", 0) jingli = self.session.getVariable("jingli", 0) maxjingli = self.session.getVariable("max_jingli", 0) qi = self.session.getVariable("qi", 0) effqi = self.session.getVariable("eff_qi", 0) maxqi = self.session.getVariable("max_qi", 0) neili = self.session.getVariable("neili", 0) maxneili = self.session.getVariable("max_neili", 0) barstyle = "━" screenwidth = self.session.application.get_width() barlength = screenwidth // 2 - 1 span = screenwidth - 2 * barlength qi_bar = self.create_status_bar(qi, effqi, maxqi, barlength, barstyle) jing_bar = self.create_status_bar(jing, effjing, maxjing, barlength, barstyle) formatted_list.extend(qi_bar) formatted_list.append(("", " " * span)) formatted_list.extend(jing_bar) formatted_list.append(("", "\n")) # line 1. char, menpai, deposit, food, water, exp, pot formatted_list.append((Settings.styles["title"], "【角色】")) formatted_list.append((Settings.styles["value"], "{0}({1})".format(self.session.getVariable('name'), self.session.getVariable('id')))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【食物】")) food = int(self.session.getVariable('food', '0')) max_food = self.session.getVariable('max_food', 350) if food < 100: style = Settings.styles["value.worst"] elif food < 200: style = Settings.styles["value.worse"] elif food < max_food: style = Settings.styles["value"] else: style = Settings.styles["value.better"] formatted_list.append((style, "{}".format(food))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【饮水】")) water = int(self.session.getVariable('water', '0')) max_water = self.session.getVariable('max_water', 350) if water < 100: style = Settings.styles["value.worst"] elif water < 200: style = Settings.styles["value.worse"] elif water < max_water: style = Settings.styles["value"] else: style = Settings.styles["value.better"] formatted_list.append((style, "{}".format(water))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【经验】")) formatted_list.append((Settings.styles["value"], "{}".format(self.session.getVariable('combat_exp')))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【潜能】")) formatted_list.append((Settings.styles["value"], "{}".format(self.session.getVariable('potential')))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【门派】")) formatted_list.append((Settings.styles["value"], "{}".format(self.session.getVariable('family/family_name')))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【存款】")) formatted_list.append((Settings.styles["value"], "{}".format(self.session.getVariable('deposit')))) formatted_list.append(("", " ")) # line 2. hp # a new-line formatted_list.append(("", "\n")) formatted_list.append((Settings.styles["title"], "【精神】")) if int(effjing) < int(maxjing): style = Settings.styles["value.worst"] elif int(jing) < 0.8 * int(effjing): style = Settings.styles["value.worse"] else: style = Settings.styles["value"] if maxjing == 0: pct1 = pct2 = 0 else: pct1 = 100.0*float(jing)/float(maxjing) pct2 = 100.0*float(effjing)/float(maxjing) formatted_list.append((style, "{0}[{1:3.0f}%] / {2}[{3:3.0f}%]".format(jing, pct1, effjing, pct2))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【气血】")) if int(effqi) < int(maxqi): style = Settings.styles["value.worst"] elif int(qi) < 0.8 * int(effqi): style = Settings.styles["value.worse"] else: style = Settings.styles["value"] if maxqi == 0: pct1 = pct2 = 0 else: pct1 = 100.0*float(qi)/float(maxqi) pct2 = 100.0*float(effqi)/float(maxqi) formatted_list.append((style, "{0}[{1:3.0f}%] / {2}[{3:3.0f}%]".format(qi, pct1, effqi, pct2))) formatted_list.append(("", " ")) # 内力 formatted_list.append((Settings.styles["title"], "【内力】")) if int(neili) < 0.6 * int(maxneili): style = Settings.styles["value.worst"] elif int(neili) < 0.8 * int(maxneili): style = Settings.styles["value.worse"] elif int(neili) < 1.2 * int(maxneili): style = Settings.styles["value"] else: style = Settings.styles["value.better"] if maxneili == 0: pct = 0 else: pct = 100.0*float(neili)/float(maxneili) formatted_list.append((style, "{0} / {1}[{2:3.0f}%]".format(neili, maxneili, pct))) formatted_list.append(("", " ")) # 精力 formatted_list.append((Settings.styles["title"], "【精力】")) if int(jingli) < 0.6 * int(maxjingli): style = Settings.styles["value.worst"] elif int(jingli) < 0.8 * int(maxjingli): style = Settings.styles["value.worse"] elif int(jingli) < 1.2 * int(maxjingli): style = Settings.styles["value"] else: style = Settings.styles["value.better"] if maxjingli == 0: pct = 0 else: pct = 100.0*float(jingli)/float(maxjingli) formatted_list.append((style, "{0} / {1}[{2:3.0f}%]".format(jingli, maxjingli, pct))) formatted_list.append(("", " ")) return formatted_list except Exception as e: return f"{e}" try: formatted_list = list() ins_loc = self.session.getVariable("ins_loc", None) tm_locs = self.session.getVariable("tm_locs", None) ins = False if isinstance(ins_loc, dict) and (len(ins_loc) >= 1): ins = True loc = ins_loc elif isinstance(tm_locs, list) and (len(tm_locs) == 1): ins = True loc = tm_locs[0] # line 1. char, menpai, deposit, food, water, exp, pot formatted_list.append((Settings.styles["title"], "【角色】")) formatted_list.append((Settings.styles["value"], "{0}({1})".format(self.session.getVariable('name'), self.session.getVariable('id')))) formatted_list.append(("", " ")) # fullme time fullme = int(self.session.getVariable('%fullme', 0)) delta = time.time() - fullme formatted_list.append((Settings.styles["title"], "【FULLME】")) if delta < 30 * 60: style = Settings.styles["value"] elif delta < 60 * 60: style = Settings.styles["value.worse"] else: style = Settings.styles["value.worst"] if fullme == 0: formatted_list.append((Settings.styles["value.worst"], "从未")) else: formatted_list.append((style, "{}".format(int(delta // 60)))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【食物】")) food = int(self.session.getVariable('food', '0')) max_food = self.session.getVariable('max_food', 350) if food < 100: style = Settings.styles["value.worst"] elif food < 200: style = Settings.styles["value.worse"] elif food < max_food: style = Settings.styles["value"] else: style = Settings.styles["value.better"] formatted_list.append((style, "{}".format(food))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【饮水】")) water = int(self.session.getVariable('water', '0')) max_water = self.session.getVariable('max_water', 350) if water < 100: style = Settings.styles["value.worst"] elif water < 200: style = Settings.styles["value.worse"] elif water < max_water: style = Settings.styles["value"] else: style = Settings.styles["value.better"] formatted_list.append((style, "{}".format(water))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【经验】")) formatted_list.append((Settings.styles["value"], "{}".format(self.session.getVariable('combat_exp')))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【潜能】")) formatted_list.append((Settings.styles["value"], "{}".format(self.session.getVariable('potential')))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【门派】")) formatted_list.append((Settings.styles["value"], "{}".format(self.session.getVariable('family/family_name')))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【存款】")) formatted_list.append((Settings.styles["value"], "{}".format(self.session.getVariable('deposit')))) formatted_list.append(("", " ")) # line 2. hp #(jing, effjing, maxjing, jingli, maxjingli, qi, effqi, maxqi, neili, maxneili) = self.session.getVariables(("jing", "eff_jing", "max_jing", "jingli", "max_jingli", "qi", "eff_qi", "max_qi", "neili", "max_neili")) jing = self.session.getVariable("jing", 0) effjing = self.session.getVariable("eff_jing", 0) maxjing = self.session.getVariable("max_jing", 0) jingli = self.session.getVariable("jingli", 0) maxjingli = self.session.getVariable("max_jingli", 0) qi = self.session.getVariable("qi", 0) effqi = self.session.getVariable("eff_qi", 0) maxqi = self.session.getVariable("max_qi", 0) neili = self.session.getVariable("neili", 0) maxneili = self.session.getVariable("max_neili", 0) #if jing and effjing and maxjing and effqi and maxqi and qi and jingli and maxjingli and neili and maxneili: # a new-line formatted_list.append(("", "\n")) formatted_list.append((Settings.styles["title"], "【精神】")) if int(effjing) < int(maxjing): style = Settings.styles["value.worst"] elif int(jing) < 0.8 * int(effjing): style = Settings.styles["value.worse"] else: style = Settings.styles["value"] if maxjing == 0: pct1 = pct2 = 0 else: pct1 = 100.0*float(jing)/float(maxjing) pct2 = 100.0*float(effjing)/float(maxjing) formatted_list.append((style, "{0}[{1:3.0f}%] / {2}[{3:3.0f}%]".format(jing, pct1, effjing, pct2))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【气血】")) if int(effqi) < int(maxqi): style = Settings.styles["value.worst"] elif int(qi) < 0.8 * int(effqi): style = Settings.styles["value.worse"] else: style = Settings.styles["value"] if maxqi == 0: pct1 = pct2 = 0 else: pct1 = 100.0*float(qi)/float(maxqi) pct2 = 100.0*float(effqi)/float(maxqi) formatted_list.append((style, "{0}[{1:3.0f}%] / {2}[{3:3.0f}%]".format(qi, pct1, effqi, pct2))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【精力】")) if int(jingli) < 0.6 * int(maxjingli): style = Settings.styles["value.worst"] elif int(jingli) < 0.8 * int(maxjingli): style = Settings.styles["value.worse"] elif int(jingli) < 1.2 * int(maxjingli): style = Settings.styles["value"] else: style = Settings.styles["value.better"] if maxjingli == 0: pct = 0 else: pct = 100.0*float(jingli)/float(maxjingli) formatted_list.append((style, "{0} / {1}[{2:3.0f}%]".format(jingli, maxjingli, pct))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【内力】")) if int(neili) < 0.6 * int(maxneili): style = Settings.styles["value.worst"] elif int(neili) < 0.8 * int(maxneili): style = Settings.styles["value.worse"] elif int(neili) < 1.2 * int(maxneili): style = Settings.styles["value"] else: style = Settings.styles["value.better"] if maxneili == 0: pct = 0 else: pct = 100.0*float(neili)/float(maxneili) formatted_list.append((style, "{0} / {1}[{2:3.0f}%]".format(neili, maxneili, pct))) formatted_list.append(("", " ")) # a new-line formatted_list.append(("", "\n")) formatted_list.append((Settings.styles["title"], "【任务】")) formatted_list.append((Settings.styles["value"], "{}".format(self.jobmanager.currentJob))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【状态】")) formatted_list.append((Settings.styles["value"], "{}".format(self.jobmanager.currentStatus))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【持续】")) formatted_list.append((Settings.styles["value"], "{}".format("开启" if self.jobmanager.always else "关闭"))) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【范围】")) formatted_list.append((Settings.styles["value"], "{}".format(self.jobmanager.activeJobs))) # a new-line formatted_list.append(("", "\n")) # line 3. GPS info formatted_list.append((Settings.styles["title"], "【惯导】")) if ins: formatted_list.append((Settings.styles["value"], "正常")) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【位置】")) formatted_list.append((Settings.styles["value"], f"{loc['city']} {loc['name']}({loc['id']})")) else: formatted_list.append((Settings.styles["value.worst"], "丢失")) formatted_list.append(("", " ")) formatted_list.append((Settings.styles["title"], "【位置】")) formatted_list.append((Settings.styles["value"], f"{self.session.getVariable('room')}")) if self.session.getVariable("is_busy", False): formatted_list.append((Settings.styles["value.worse"], "【忙】")) else: formatted_list.append((Settings.styles["value"], "【不忙】")) if self.session.getVariable("is_fighting", False): formatted_list.append((Settings.styles["value.worse"], "【战斗】")) else: formatted_list.append((Settings.styles["value"], "【空闲】")) if self.session.idletime > 60: formatted_list.append((Settings.styles["value.worse"], f"【发呆{self.session.idletime // 60:.0f}分钟】")) else: formatted_list.append((Settings.styles["value"], "【正常】")) formatted_list.append((Settings.styles["title"], "【BUFF】")) buff = self.session.getVariable("buff", list()) #formatted_list.append((Settings.styles["value"], f"{' '.join(buff)}")) #formatted_list.append(to_formatted_text(ANSI(f"{' '.join(buff)}"))) buff_styled = to_formatted_text(ANSI(f"{' '.join(buff)}")) formatted_list.extend(buff_styled) # a new-line formatted_list.append(("", "\n")) def go_direction(dir, mouse_event: MouseEvent): if mouse_event.event_type == MouseEventType.MOUSE_UP: self.session.exec_command(dir) if ins: formatted_list.append((Settings.styles["title"], "【路径】")) # formatted_list.append(("", " ")) links = self.mapper.FindRoomLinks(loc['id']) for link in links: dir = link.path dir_cmd = dir if dir in DIRS_ABBR.keys(): dir = DIRS_ABBR[dir] else: m = re.match(r'(\S+)\((.+)\)', dir) if m: dir_cmd = m[2] formatted_list.append((Settings.styles["link"], f"{dir}: {link.city} {link.name}({link.linkto})", functools.partial(go_direction, dir_cmd))) formatted_list.append(("", " ")) return formatted_list except Exception as e: self.session.error(f"状态窗口发生错误!错误信息为: {e}") return f"{e}"
6.9 分组对象管理¶
分组对象管理是指,通过分组对象管理,可以将多个对象归为一类,便于统一管理。 分组对象管理的基本原理是,通过创建一个对象,该对象包含多个其他对象,从而实现分组管理。 分组对象管理的优点是,可以统一管理多个对象,便于维护。
PyMUD提供的各类对象的,包括Timer, Alias, Trigger, GMCPTrigger, Command等均具有group属性来表示其分组。 组可以包括子组,子组可以再包括子组。组名以点号.分隔,例如:group1.subgroup1.subsubgroup1。组的层级没有限制。 组的概念可以用于快速处理多个对象,例如启用/禁用一组对象、删除一组对象等。例如,以下几个组的关系:
mygroup1
mygroup1.subgroup1 # 属于 mygroup1的子组
mygroup1.subgroup2 # 属于 mygroup1的子组
mygroup1.subgroup2.subsubgroup1 # 属于 mygroup1.subgroup2的子组,也同样属于更高层级mygroup1的子组
mygroup2
mygroup2.subgroup1 # 属于 mygroup2的子组
在脚本中,成组操作对象主要提供两个函数,分别是 enableGroup 和 deleteGroup。其中,enableGroup 用于启用/禁用一组对象,deleteGroup 用于删除一组对象。 这两个函数都可以通过组名来指定要操作的对象组,同时也可以指定是否包含子组,以及要操作的对象类型范围。 以下是 enableGroup 和 deleteGroup 函数的详细说明:
``` Python
- def enableGroup(self, group: str, enable: bool, subgroup = True, types: Union[Type, Union[Tuple, List]] = (Alias, Trigger, Command, Timer, GMCPTrigger)):
pass
# 各参数含义: # group: 要操作的组名,可以是完整的组名,也可以是部分组名。例如:"group1" 或 "group1.subgroup1" 等。 # enable: 是否启用对象。如果为True,则启用指定的对象。如果为False,则禁用指定的对象。 # subgroup: 是否包含子组。如果为True,则操作包括当前组及其所有子组的对象。如果为False,则仅操作当前组的对象,不包括子组。 # types: 要操作的对象类型范围。可以是单个类型,也可以是类型的元组或列表。例如:Trigger, Alias, Command, Timer, GMCPTrigger 等。 # 示例代码:
- objs = [
Trigger(session, "tri1", group = "group1"), Trigger(session, "tri2", group = "group1.subgroup1"), Trigger(session, "tri3", group = "group1.subgroup2"), Alias(session, "alias1", group = "group1"), Alias(session, "alias2", group = "group1.subgroup1"), Timer(session, 5, group = "group1.subgroup1")
]
#以下调用可以同时禁用group1及其子组的所有对象,因为 group1.subgroup1 和 group1.subgroup2 都属于 group1 的子组 session.enableGroup("group1", False) #以下调用可以同时仅启用触发器tri1和别名alias1,因为通过subgroup参数限定了不传递到子组 session.enableGroup("group1", True, subgroup = False) # 以下调用可以同时禁用对应发器和别名,但不禁用定时器,因为通过types参数指定了有效范围: session.enableGroup("group1.subgroup1", False, types = [Trigger, Alias])
- def deleteGroup(self, group: str, subgroup = True, types: Union[Type, Union[Tuple, List]] = (Alias, Trigger, Command, Timer, GMCPTrigger)):
pass
# 各参数含义: # group: 要删除的组名,可以是完整的组名,也可以是部分组名。例如:"group1" 或 "group1.subgroup1" 等。 # subgroup: 是否包含子组。如果为True,则删除指定组及其所有子组的对象。如果为False,则仅删除指定组的对象,不包括子组。 # types: 要删除的对象类型范围。可以是单个类型,也可以是类型的元组或列表。例如:Trigger, Alias, Command, Timer, GMCPTrigger 等。 # 示例代码: # 删除所有属于group1的Trigger和Alias对象,包括子组如 group1.subgroup1 和 group1.subgroup2 等 self.session.deleteGroup("group1", True, [Trigger, Alias]) # 删除所有属于group1的Trigger对象,但不包括子组 self.session.deleteGroup("group1", False, [Trigger])
在命令行中,#trigger, #alias, #timer, #gmcp, #command, #t+, #t- 等命令均可以进行组处理,用于对整组对象进行处理。各命令的语法格式类似。 处理组时,组名应以大于号>或者等于号=开头,紧跟组名(无空格)。当使用>时,表示操作针对当前组及所有所属子组,当使用=时,表示操作仅针对当前组。 例如下面代码: