-
Notifications
You must be signed in to change notification settings - Fork 4
Home
最早想要自己写MUD客户端的念头,还是在几年前。但前几年事情太多,人太忙,我记得自20年疫情之后,到今年年初就没有再登陆过北侠了。23年春节之后空闲一些,于2023年2月19日重启MUD客户端的计划,5月29日形成第一个发布版(0.05b),目前发布的最新版为0.15c。
在自己写客户端之前,我主要用过zmud和mushclient两个客户端,北大侠客行一直是用mushclient(玩的那会儿还没有mudlet)。我认为mushclient是一个功能非常强大的客户端,唯一缺点是不支持跨平台。由于工作原因,上班的地方不能上网,手机玩的话,确实没有特别适合的跨平台客户端(tintint++倒是支持,但一直不想重学然后重写我在mushclient里的所有python脚本),加上我是一个程序爱好者,所以决定自己干起,正好在游戏之中学习了。
因为我要综合平衡工作、生活、写代码、当然还有自己玩,所以整个更新节奏不会很快,但我认为我会一直更新下去的。感谢北大侠客行巫师团队的努力,北侠吸引我玩的动力,也是我不断更新完善客户端的动力!
PyMUD具有以下特点:
- 原生Python开发,除prompt-toolkit及其依赖库(wcwidth, pygments, pyperclip)外,不需要其他第三方库支持
- 基于控制台的全屏UI界面设计,支持鼠标操作(Android上支持触摸屏操作)
- 支持分屏显示,在数据快速滚动的时候,上半屏保持不动,以确保不错过信息
- 解决了99%情况下,北大侠客行中文对不齐,也就是看不清字符画的问题(因为我没有走遍所有地方,不敢保证100%)
- 真正的支持多session会话(也支持鼠标切换会话)
- 原生支持多种服务器端编码方式,不论是GBK、BIG5、还是UTF-8
- 支持NWAS、MTTS协商,支持GMCP、MSDP、MSSP协议, MXP待开发。
- 一次脚本开发,多平台运行。只要能在该平台上运行python,就可以运行PyMUD客户端
- 脚本所有语法均采用Python原生语法,因此你只要会用Python,就可以自己写脚本,免去了再去学习lua、熟悉各类APP的使用的难处
- Python拥有极为强大的文字处理能力,用于处理文本的MUD最为合适
- Python拥有极为丰富的第三方库,能支持的第三方库,就能在PyMUD中支持
- 我自己还在玩,所以本客户端会持续进行更新:)
PyMUD是一个原生基于Python语言的MUD客户端,因此最基本的环境是Python环境而非操作系统环境。理论上,只要你的操作系统下可以运行Python,就可以运行PyMUD。另外,本客户端的UI设计是基于控制台的,因此也不需要有图形环境的支持。
-
操作系统需求:不限,能运行Python是必要条件。
-
版本需求:0.15b 要求>=3.10(部分声明使用了仅3.10开始支持的语法),0.15c 要求 >=3.7(已测试3.7.9,更旧的版本不确定能否使用,请自行尝试),32位/64位随意,但建议用64位版,可以支持4G以上的内存访问
-
包需求:prompt-toolkit 3.0(代码在 https://github.com/prompt-toolkit/python-prompt-toolkit ), 以及由prompt-toolkit所依赖的wcwidth、pygment、pyperclip
-
安装Python,这个就不讲了。如果不会python语言可以学,如果不会任何编程语言,用这个客户端可能还是有点难度。所以假设用户是会python的
-
安装支持包,pip安装即可, pip install prompt-toolkit pyperclip。理论上wcwidth和pygment包在安装prompt-toolkit时会自动安装(因为是依赖项),如果没有就自行安装。pyperclip是跨平台剪贴板的处理包,prompt-toolkit中默认使用了自己的InMemoryClipboard,但这个只能在程序内部复制,所以我使用了pyperclip,可以在操作系统中的程序间复制(此处备注:经测试,在华为鸿蒙系统的termux下,pyperclip包无法复制,因此在手机上的复制,请直接使用termux的复制功能)
-
获取PyMUD程序本体:可以直接使用git从github上clone。命令行执行 git clone https://github.com/crapex/pymud.git 也可以从github页面下载release版本。
在命令行下,PyMUD目录内,直接运行python pymud.py即可。linux下有时候python链接到python2,此时需要执行python3 pymud.py即可。
git clone/论坛下载下来的程序本体一共10个文件(0.10b版),一个文件夹。
其中,screen_shot文件夹,是我在windows、ubuntu、以及华为手机(鸿蒙3.0)下的运行截图; LICENSE是GNU GPL V3协议;README.md是说明文件,都不是程序本体内容。
-
settings.py 程序应用的配置文件(如何配置将在后面详述)
-
pymud.py 主入口对象,PyMudApplication的实现,主要处理界面、菜单、快捷键、鼠标、会话管理等功能;
-
extras.py 对prompt-toolkit库里某些类的继承修改,以适应东方字符的宽度、北大侠客行字符对齐、鼠标操作、显示的分屏处理等;
-
dialogs.py 调用的各种对话框的实现(ui界面内容)
-
protocol.py 处理Telnet协议,以及MUD的各种扩展协议(如MTTS、GMCP等)
-
session.py 会话管理对象,每一个会话,就是一个角色的世界,所有会话处理,包括与protocol之间的交互、触发器、别名、变量等的处理均在此实现
-
objects.py MUD基本对象,触发器、别名、定时器、命令等的实现类
-
pkuxkx.py 这个文件不是程序的组成部分,只是使用pymud写的角色脚本的一个示例(如何写脚本将在后面详述)
下图是主要界面,用1-7标注出7个主要内容,含义如下:
- 菜单栏,可以鼠标操作(手机上是触控),目前只有3个1级菜单
- 多会话的显示和切换标签。多个会话时,每一个会话的名称会在此处,灰底亮字的是当前会话。颜色为绿色的表示当前会话处于连接状态,未处于连接状态的为白色(灰色)。可以直接单击会话名称切换会话。键盘的话,Ctrl+左右箭头可以切换
- 中间分隔线。当向上滚动或翻页的时候,会产生中间分隔线,此时上半部分不再滚动,下半部分持续滚动显示最新的消息。取消分隔的话,可以向下滚动到底,或者Ctrl-Z快捷键取消(仿zmud操作)。上下翻页只能使用鼠标滚轮,或者PageUp\PageDown按键。鼠标滚轮是一次一行,键盘是一次一页。
- 状态窗口。可以使用函数自定义状态窗口显示内容,图例是我自己定义的状态窗口显示内容。
- 命令输入行。命令输入行支持历史记录、以及基于历史记录的自动建议和自动完成。键盘上下方向键可以在历史记录中选择,键盘右键实现历史记录建议的自动完成。比如前面输入过ask pu about job,后面输入ask的时候,后面的 pu about job会以灰色显示(表示自动建议),只需键盘右箭头,即可完成输入。
- 状态栏,显示状态信息,目前只有会话切换、复制内容的信息会显示,可以通过app.set_status来设置显示的文字
- 状态栏(右),显示连接状态和连接时间。因为基于控制台的ui不是时刻刷新,因此链接的时间有时会滞后显示,看上去就是秒在跳动,不影响时间记录。
连接到服务器有以下三种方式:
- 使用“世界”->“创建新会话菜单”。如果是连接北侠,修改会话名称为你想要的即可(不改也可以连接)。
- 使用命令行命令:在命令行内输入:
#session {session_name} {host} {port} {encoding}
大括号内容为,分别代表会话名称、服务器地址、端口、编码方式(编码方式可不指定,此时默认为utf-8编码)。例如,使用下列命令可以创建一个名为newstart的会话并连接到北侠。
#session newstart mud.pkuxkx.net 8081
- 在settings.py中加入常用的角色会话。此时会自动生成菜单,鼠标选择对应的菜单可自动连接到会话。
sessions = {
"pkuxkx" : { # 一个服务器作为一个一级菜单项,所有角色放在下面
"host" : "mud.pkuxkx.net", # 服务器地址
"port" : "8081", # 服务器端口
"encoding" : "utf8", # 服务器编码
"autologin" : "{0};{1}", # 自动登录命令,此处{0}、{1}是两个参数,实际运用时会自动指代下面的角色id和密码进行替换。分号指示两次命令输入。当不使用自动登录时,该参数可以设置为None,或空字符串""
"default_script": "pkuxkx", # 角色登录后默认加载的脚本(省略.py扩展名)。此处指默认加载当前目录下的pkuxkx.py文件
"chars" : {
"newstart" : ("newstart", "mypassword"), # 创建的角色菜单,会话名、角色ID、角色密码
"your_session" : ("char_id", "char_pass"), # 可保存多个角色,使用,隔开,即在字典中使用多个key。
}
},
"evennia" : { # 也可以创建多个服务器的菜单,按规则依次填写即可。
"host" : "192.168.220.128",
"port" : "4000",
"encoding" : "utf8",
"autologin" : "connect {0} {1}",
"default_script": None,
"chars" : {
"evennia" : ("admin", "admin"),
}
}
}
- 可以支持同时打开多个会话,此时在顶部中央会显示不同的会话名称和当前会话。可以使用鼠标切换会话,或者使用快捷键Ctrl+左右箭头实现会话向左和向右切换。
- 当使用quit命令退出北侠账号、或者由于网络异常,或者操作菜单“会话”->“断开连接”后,服务器会断开连接,但此时会话窗口并不会关闭。在这种情况下,可以使用命令#connect(或缩写#con)重新连接到服务器。也可以使用菜单“会话”->“连接/重新连接到服务器”执行同样功能。
- 关闭会话窗口可以使用#close命令。也可以使用菜单“会话”->“关闭当前会话”进行关闭。
- Python基本语法与内置类型;
- 面向对象开发(OOP)的核心概念:封装、继承与多态;
- 函数式编程的基本概念;
- 位置参数与命名参数的基本概念;
- 基于Python的正则表达式;
- Python asyncio包的熟练使用,包括async/await语法、coroutine、Task概念与运用、Event/Future的使用、事件循环等
PyMUD使用#load命令加载脚本文件,调用了importlib库的import_module方法。除此之外,为了满足会话的使用要求,对脚本文件还有以下约定:
- 加载的脚本文件中,必须有一个命名为:Configuration的类,构造函数必须接受一个Session类型的参数(传递的是当前会话)。
一个可加载的基本的脚本文件内容参考如下:
class Configuration:
def __init__(self, session):
self.session = session
脚本中所需使用的有关基本对象已经内置在objects.py中,可以直接import使用。基本对象含义如下:
类型 | 简介 | 用途 |
---|---|---|
Alias | 别名的基本类型 | 可以用来实现基本/复杂别名,必须全代码实现 |
Trigger | 触发器的基本类型 | 可以用来实现基本/复杂触发器,必须全代码实现 |
Timer | 定时器的基本类型 | 可以用来实现基本/复杂定时器,必须全代码实现 |
SimpleAlias | 简单别名 | 可以使用mud命令和脚本而非代码的方法来实现的别名,类似zmud别名 |
SimpleTrigger | 简单触发器 | 可以使用mud命令和脚本而非代码的方法来实现的触发器,类似zmud触发器 |
SimpleTimer | 简单定时器 | 可以使用mud命令和脚本而非代码的方法来实现的定时器,类似zmud定时器(注:0.12b版增加) |
Command | 命令的基本类型 | 命令是PyMUD的一大原创特色,在高级脚本中会详细讲到 |
SimpleCommand | 简单命令 | 命令是PyMUD的一大原创特色,在高级脚本中会详细讲到 |
GMCPTrigger | GMCP触发器 | 专门用于处理MUD服务器GMCP数据,操作方法类似于触发器 |
在脚本中要使用上述类型的import:
from objects import Alias, Trigger, Timer, SimpleTrigger, SimpleAlias, SimpleTimer, Command, SimpleCommand, GMCPTrigger
与MUD服务器的所有交互操作,均由Session类所实现,因此,其与服务器的交互方法是编写脚本所需要的必要内容。Session类的常用方法如下。
方法名称 | 参数:类型 | 返回值 | 简介 |
---|---|---|---|
writeline | line:str | None | 向服务器中写入一行。如果参数使用;分隔(使用Settings.client.seperator指定),将分行后逐行写入 |
exec_command | line:str | None | 执行一段命令,并将数据发送到服务器。本函数和writeline的区别在于,本函数会先进行Command和Alias解析,若不是再使用writeline发送。当line不包含Command和Alias时,等同于writeline |
exec_command_after | wait:float, line:str | None | 等待wait秒后,再执行line所代表的命令 |
exec_command_async | line:str | None | exec_command的异步(async)版本。在每个命令发送前会等待上一个执行完成。 |
getUniqueNumber | 无 | int | 生成一个本会话的唯一序号 |
getUniqueID | prefix | str | 生成一个类似{prefix}_{uniqueID}的唯一序号 |
enableGroup | group:str, enabled=True | None | 使能/禁用指定组名为group的所有对象,包括Alias、Trigger、Timer、Command、GMCPTrigger |
addAliases | alis:dict | None | 向会话中添加一组Alias对象,对象由字典alis包含 |
addAlias | ali:Alias | None | 向会话中添加一个Alias对象 |
delAlias | ali:str | None | 删除会话中id为ali的Alias对象 |
addTriggers | tris:dict | None | 添加一组Trigger对象 |
addTrigger | tri:Trigger | None | 添加一个Trigger |
delTrigger | tri:str | None | 删除id为tri的Trigger |
addCommands | cmds:dict | None | 添加一组Command对象 |
addCommand | cmd:Command | None | 添加一个Command |
delCommand | cmd:str | None | 删除id为cmd的Command |
addTimers | tis:dict | None | 添加一组Timer对象 |
addTimer | ti:Timer | None | 添加一个Timer |
delTimer | ti:str | None | 删除id为ti的Timer |
addGMCPs | gmcp:dict | None | 添加一组GMCPTrigger对象 |
addGMCP | gmcp:GMCPTrigger | None | 添加一个GMCPTrigger |
delGMCP | gmcp:str | None | 删除id为gmcp的GMCPTrigger |
setVariable | name:str,value | None | 设置(或新增)一个变量,变量名为name,值为value(注:value可以为任意Python类型) |
setVariables | names,value | None | 设置(或新增)一组变量,变量名为names(元组或列表),值为values(元组或列表) |
updateVariables | kvdict:dict | None | 更新一组变量,与setVariables的区别为,接受的参数为dict键值对 |
getVariable | name:str,default=None | 任意类型 | 获取名为name的变量值,当name不存在时,返回default |
getVariables | names | tuple | 获取names列表中所有的变量值。对每一个,当name不存在时,获取内容为None值 |
info | msg,title,style | None | 向Session窗体中输出一串信息,会以[title] msg的形式输出,title默认PYMUD INFO,颜色(style指定)默认绿色 |
warning | msg,title,style | None | 向Session窗体中输出一串信息,会以[title] msg的形式输出,title默认PYMUD WARNING,颜色(style指定)默认黄色 |
error | msg,title,style | None | 向Session窗体中输出一串信息,会以[title] msg的形式输出,title默认PYMUD ERROR,颜色(style指定)默认红色 |
进入MUD游戏,第一个要使用的必然是触发器(Trigger)。PyMUD支持多种特性的触发器,并内置实现了Trigger和SimpleTrigger两个基础类。 要在会话中使用触发器,要做的两件事是:
- 构建一个Trigger类(或其子类)的实例。SimpleTrigger是Trigger的子类,你也可以构建自己定义的子类。
- 将该实例通过session.addTrigger方法增加到会话的触发器清单中。
Trigger是触发器的基础类,继承自MatchObject类(可以不用理会)。SimpleTrigger继承自Trigger,可以直接用命令而非函数来实现触发器触发时的操作。 Trigger与SimpleTrigger的构造函数分别如下:
class Trigger:
def __init__(self, session, patterns, *args, **kwargs):
pass
class SimpleTrigger:
def __init__(self, session, patterns, code, *args, **kwargs):
pass
为了使用统一的函数语法,除重要的参数session(指定会话)、patterns(指定匹配模式)、code(SimpleTrigger)的执行代码之外,其余所有触发器的参数都通过命名参数在kwargs中指定。触发器支持和使用的命名参数、默认值及其含义如下:
- id: 唯一标识符。不指定时,默认生成session中此类的唯一标识。
- group: 触发器所属的组名,默认未空。支持使用session.enableGroup来进行整组对象的使能/禁用
- priority: 优先级,默认100。在触发时会按优先级排序执行,越小优先级越高。
- enabled: 使能状态,默认为True。标识是否使能该触发器。
- oneShot: 单次触发,默认为False。当该值为True时,触发器被触发一次之后,会自动从会话中移除。
- onSuccess: 函数的引用,默认为空。当触发器被触发时自动调用的函数,函数类型应为func(id, line, wildcards)形式。
- ignoreCase: 忽略大小写,默认为False。触发器匹配时是否忽略大小写。
- isRegExp:是否正则表达式,默认为True。即指定的触发匹配模式patterns是否为正则表达式。
- keepEval: 匹配成功后持续进行后续匹配,默认为False。当有两个满足相同匹配模式的触发器时,要设置该属性为True,否则第一次匹配成功后,该行不会进行后续触发器匹配(意味着只有最高优先级的触发器会被匹配)
- raw: 原始代码匹配,默认为False。当为True时,对MUD服务器的数据原始代码(含VT100控制指令)进行匹配。在进行颜色匹配的时候使用。
构造函数中的其他参数含义如下:
- session: 指定的会话对象,必须有
- patterns: 匹配模式。当为单行匹配时,传递字符串(正则表达式或原始数据)。当需要进行多行匹配时,使用元组或者列表传递多个匹配行即可。
- code: SimpleTrigger独有,即匹配成功后,执行的代码串。该代码串类似于zmud的应用,可以用mud命令、别名以分号(;)隔开,也可以在命令之中插入PyMUD支持的#指令,如#wait(缩写为#wa)
例如,在新手任务(平一指配药)任务中,要在要到任务后,自动n一步,并在延时500ms后进行配药;配药完成后自动s,并提交配好的药,并再次接下一个任务,则可以如此建立触发器:
tri1 = SimpleTrigger(self.session, "^[> ]*你向平一指打听有关『工作』的消息。", "n;#wa 500;peiyao")
self.session.addTrigger(tri1)
tri2 = SimpleTrigger(self.session, "^[> ]*不知过了多久,你终于把药配完。", "s;#wa 500;give ping yao;#wa 500;ask ping about 工作")
self.session.addTrigger(tri2)
例如,当收到有关fullme或者其他图片任务的链接信息时,自动调用浏览器打开该网址,则可以建立一个标准触发器(示例中同时指定了触发器id):
def initTriggers(self):
tri = Trigger(self.session, id = 'tri_webpage', patterns = r'^http://fullme.pkuxkx.net/robot.php.+$', onSuccess = self.ontri_webpage)
self.session.addTrigger(tri)
def ontri_webpage(self, name, line, wildcards):
webbrowser.open(line) # 在开头之前已经 import webbrowser
例如,在set hpbrief long的情况下,hpbrief显示三行内容。此时进行多行触发(3行),并将触发中获取的参数值保存到variable变量中:
HP_KEYS = (
"exp", "pot", "maxneili", "neili", "maxjingli", "jingli",
"maxqi", "effqi", "qi", "maxjing", "effjing", "jing",
"zhenqi", "zhenyuan", "food", "water", "fighting", "busy"
)
def initTriggers(self):
tri = Trigger(self.session, id = "tri_hpbrief", patterns = (r'^[> ]*#(\d+.?\d*[KM]?),(\d+),(\d+),(\d+),(\d+),(\d+)$', r'^[> ]*#(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)$', r'^[> ]*#(\d+),(\d+),(-?\d+),(-?\d+),(\d+),(\d+)$',), onSuccess = self.ontri_hpbrief)
self.session.addTrigger(tri)
def ontri_hpbrief(self, name, line, wildcards):
self.session.setVariables(self.HP_KEYS, wildcards)
如果要捕获文字中的颜色、闪烁等特性,则可以使用触发器的raw属性。例如,在长安爵位任务中,要同时判断路人身上的衣服和鞋子的颜色和类型时,可以使用如下触发:
def initTriggers(self):
tri = Trigger(self.session, patterns = r"^.+□(?:\x1b\[[\d;]+m)?(身|脚)\S+一[双|个|件|把](?:\x1b\[([\d;]+)m)?([^\x1b\(\)]+)(?:\x1b\[[\d;]+m)?\(.+\)", onSuccess = self.judgewear, raw = True)
self.session.addTrigger(tri)
def judgewear(self, name, line, wildcards):
buwei = wildcards[0] # 身体部位,身/脚
color = wildcards[1] # 颜色,30,31,34,35为深色,32,33,36,37为浅色
wear = wildcards[2] # 着装是布衣/丝绸衣服、凉鞋/靴子等等
# 对捕获结果的进一步判断,此处省略
当要简化一些输入的MUD命令,或者代入一些参数时,会使用到别名(Alias)。PyMUD支持多种特性的别名,并内置实现了Alias和SimpleAlias两个基础类。 要在会话中使用别名,要做的两件事是:
- 构建一个Alias类(或其子类)的实例。SimpleAlias是Alias的子类,你也可以构建自己定义的子类。
- 将该实例通过session.addAlias方法增加到会话的别名清单中。
Alias是别名的基础类,与触发器一样,也是继承自MatchObject类。事实上,在PyMUD设计时,我认为别名和触发器是完全相同的东西,只不过一个对输入的命令进行匹配处理(别名),而另一个对MUD服务器端的消息进行匹配处理(触发器),因而在代码上,这两个类除了名称不同之外,差异也是极小的。主要差异是在于触发器增加了异步触发功能(高级内容中会讲到)。SimpleAlias继承自Alias,可以直接用命令而非函数来实现别名触发时的操作。 Alias与SimpleAlias的构造函数与Trigger和SimpleTrigger完全相同,此处不再列举: 别名支持的命名参数、默认值及其含义与上一节触发器的完全相同,但以下命名参数在别名中虽然支持,但不使用:
- oneShot: 别名不存在只用一次,因此设置这个无实际意义;
- raw: 别名不存在ANSI版本和纯文本版本,因此设置这个也无实际意义。
例如,要将从扬州中央广场到信阳小广场的路径设置为别名,可以如此建立别名:
ali = SimpleAlias(self.session, "yz_xy", "#4 w;nw;#5 w")
self.session.addAlias(ali)
例如,每次慕容信件任务完成后都要从尸体中取出信件,另外还有可能有黄金、白银,每次都输入get letter fromc corpse等等命令太长,想进行缩写,则可以如此建立别名:
ali = SimpleAlias(self.session, "^gp\s(.+)$", "get %1 from corpse")
self.session.addAlias(ali)
建立别名之后,可以使用gp silver, gp gold, gp letter代替 get silver/gold/letter from corpse
例如,要将gan che to 方向建立成别名,并且在方向既可以使用缩写(e代表east之类),也可以使用全称,则可以建立一个标准别名:
DIRS_ABBR = {
"e": "east",
"w": "west",
"s": "south",
"n": "north",
"u": "up",
"d": "down",
"se": "southeast",
"sw": "southwest",
"ne": "northeast",
"nw": "northwest",
"eu": "eastup",
"wu": "westup",
"su": "southup",
"nu": "northup",
"ed": "eastdown",
"wd": "westdown",
"sd": "southdown",
"nd": "northdown",
}
def initAliases(self):
ali = Alias(self.session, "^gc\s(.+)$", onSuccess = self.ganche)
self.addAlias(ali)
def ganche(self, name, line, wildcards):
dir = wildcards[0]
if dir in self.DIRS_ABBR.keys():
cmd = "gan che to {}".format(self.DIRS_ABBR[dir]
else:
cmd = f"gan che to {dir}"
self.session.writeline(cmd)
要周期性的执行某段代码,会使用到定时器(Timer)。PyMUD支持多种特性的定时器,并内置实现了Timer和SimpleTimer两个基础类。 要在会话中使用定时器,要做的两件事是:
- 构建一个Timer类(或其子类)的实例。SimpleTimer是Timer的子类,你也可以构建自己定义的子类。
- 将该实例通过session.addTimer方法增加到会话的定时器清单中。
Timer与SimpleTimer的构造函数分别如下:
class Timer:
def __init__(self, session, *args, **kwargs):
pass
class SimpleTimer:
def __init__(self, session, code, *args, **kwargs):
pass
定时器支持以下几个命名参数,其默认值及其含义为:
- id: 唯一标识符。不指定时,默认生成session中此类的唯一标识。
- group: 触发器所属的组名,默认未空。支持使用session.enableGroup来进行整组对象的使能/禁用
- enabled: 使能状态,默认为True。标识是否使能该定时器。
- timeout: 超时时间,即定时器延时多久后执行操作,默认为10s
- oneShot: 单次执行,默认为False。当为True时,定时器仅响应一次,之后自动停止。否则,每隔timeout时间均会执行。
- onSuccess: 函数的引用,默认为空。当定时器超时时自动调用的函数,函数类型应为func(id)形式。
- code: SimpleTimer中的code,与之前的SimpleAlias、SimpleTrigger相同用法。
例如,在莫高窟冥想时,每隔5s发送一次mingxiang命令,则可以这样实现定时器
tm = SimpleTimer(self.session, timeout = 5, id = "tm_mingxiang", code = "mingxiang")
self.session.addTimers(tm)
上述定时器的标准实现版本如下
def initTimers(self):
tm = Timer(self.session, timeout = 5, id = "tm_mingxiang", onSuccess = self.timer_mingxiang)
self.session.addTimers(tm)
def timer_mingxiang(self, id, *args, **kwargs):
self.session.writeline("mingxiang")
由于PyMUD需要完整的Python脚本进行实现,在许多情况下,如zmud/mushclient里面的变量是可以完全使用Python自己的变量来进行替代的。但PyMUD保留了变量这一功能,我设计脚本的思路为,只有单个模块使用的变量,多使用Python变量实现,而需要跨模块调用的变量,才使用PyMUD变量进行存储。 事实上,PyMUD变量本质上仍是Python的变量,只不过赋值给了对应的session会话所有。也基于此特点,PyMUD的变量支持任意值类型,不是一定为简单类型,也可以为列表、字典,也可以为复杂的对象类型。 在触发器、别名的使用过程中,存在以下系统变量: %line:即触发的行本身。对于多行触发,%line会返回多行 %1 ~ %9: 触发器、别名使用时的正则匹配的匹配组
变量可以直接在SimpleTrigger、SimpleAlias、SimpleTimer中调用。系统变量直接调用(如%line),自定义变量前面加@调用。 例如,在收到“xxx告诉你:”之类的消息时,使用#message弹窗显示可以:
tri = SimpleTrigger(self.session, r".+告诉你:.+", "#message %line")
self.session.addTrigger(tri)
例如,通过触发器捕获到身上的钱的数量,可以将数量与类型联合为字典,再添加到赋值给1个变量
money = {'cash': 0, 'gold': 1, 'silver': 50, 'coin': 77}
self.session.setVariable("money", money)
# 在使用时,则这样获取
money = self.session.getVariable("money")
也可以将上述变量分别存在不同的名称中,并一同读写:
money_key = ('cash', 'gold', 'silver', 'coin')
money_count = (0, 1, 50, 77)
self.session.setVariables(money_key, money_count)
# 在使用时,则这样获取
silver = self.session.getVariable("silver")
PyMUD的高级脚本使用,必须要掌握基于asyncio库的async/await使用方法。PyMUD使用异步架构构建了整个程序的核心框架。当然,异步在脚本中不是必须的,它只是一种可选的实现方式而已,其他任何客户端都不像PyMUD一样原生完全支持异步操作,这也是PyMUD客户端与其他所有客户端的最核心差异所在。
可以先在以下帖子看看我和HellClient客户端大佬jarlyyn的讨论帖: https://www.pkuxkx.net/forum/thread-47989-2-1.html
另以下有几篇参考文档可供学习。
python的asyncio库介绍:https://docs.python.org/zh-cn/3.10/library/asyncio.html
Python异步协程(asyncio详解):https://www.cnblogs.com/Red-Sun/p/16934843.html
由于MUD是基于网咯IO的程序,在程序运行的过程中,大部分时间是空闲在等待服务器数据或客户端输入的。也正是如此,同步模式下的等待都会造成阻塞。
基于async/await的异步可以带来不少的好处,包括:
- 可以使用同步编程的思维来构建异步程序,这也是其中最大的一个好处。
- 复用异步Trigger,可以极大的增加代码的可读性。
- 可以更便捷的实现程序代码的高内聚,低耦合。特别是降低耦合性。
试想以下场景:当我们在北侠中完成一次dazuo之后,正常的服务器回应消息是:你运功完毕,深深吸了口气,站了起来。 那么,我们有可能是想持续打坐修炼内力,也可能是只要执行一次dazuo max指令;也可能时tuna过程中yun regenerate之后的打坐;在持续打坐过程中,也考虑要每隔多少次补满食物饮水; 要在一个服务器回应消息下进行不同种类的处理,在以往同步模式下,可以有很多种实现,例如可以:
- 设置不同变量标识状态,在该触发器下根据变量确定状态再判断后续执行;
- 设置多个完全相同的Trigger,并分属不同的group,根据需要只使能所需的组
上述第1种方式下,多个模块功能的代码都集中在同一个触发器下,一个出错会影响其他,要新增功能则又要对其进行修改; 第2中方式下,同样模式匹配的触发过多,代码会显得臃肿。
如果使用异步,则可以仅使用一个触发器,我们在等待该触发器触发事件后再执行对应代码。此时,触发器的触发结果与执行的内容结果是解耦的,即触发器本身不包含触发器被触发后应该执行的代码,这部分代码由功能实现函数进行完成。这也是异步触发器的由来。
PyMUD的Trigger类同时支持同步和异步模式。当使用异步触发器时,有以下两个建议:
- 不要使用SimpleTrigger。因为其code代码的执行是包含在触发器类的定义中。
- 不要指定Trigger的onSuccess调用,因为该函数调用是同步的。
Trigger类的triggered方法是一个async定义的协程函数。在不指定code、不指定onSuccess时,其默认的触发函数仅设置Trigger的Event标识,该标识是一个asyncio.Event对象。可以使用下面代码来异步等待触发器的执行。
await tri.triggered()
以之前打坐的触发示例:
class Configuration:
def __init__(self, session):
self.session = session
self.initTriggers()
def initTriggers(self):
self._triggers = {}
self._triggers['tri_dazuo'] = Trigger(self.session, r"^[> ]*你运功完毕,深深吸了口气,站了起来。", id = "tri_dazuo")
self.session.addTriggers(self._triggers)
async def dazuo_always(self):
# 此处仅为了说明异步触发器的使用,假设气是无限的,可以无限打坐
# 每打坐100次,吃干粮,喝酒袋
time = 0
while True: # 永久循环
self.session.writeline("dazuo 10") # 发送打坐命令
await self._triggers["tri_dazuo"].triggered() # 等待dazuo触发
times += 1
if times > 100:
self.session.writeline("eat liang")
self.session.writeline("drink jiudai")
times = 0
从上面的异步示例中可以看出,dazuo/eat/drink代码不是放在Trigger的触发中的,而且该代码逻辑一目了然(因为是以同步思维实现的异步)。当然,上面的代码仅是一个异步触发的使用示例,实际dazuo远比此复杂。
有了异步Trigger之后,命令就有了实现的基础。命令是什么?可以这么理解,PyMUD的命令,就是将MUD的命令输入、返回响应等封装在一起的一种对象,基于Command可以实现从最基本的MUD命令响应,到最复杂的完整的任务辅助脚本。
要在会话中使用触发器,要做的事是:
- 构建一个Command的子类(或直接构建一个SimpleCommand类,后面会讲到),实现并覆盖基类的execute方法
- 创建该Command子类的实例。一个子类应该只构建一个实例。
- 将该实例通过session.addCommand方法增加到会话的命令清单中
此时,调用该Command,只需在命令行与输入该Command匹配模式匹配的命令即可
Command与Trigger、Alias一样,也是继承自MathObject,也是通过模式匹配进行调用。因此,Command的构造函数与Trigger、Alias相同:
class Command:
def __init__(self, session, patterns, *args, **kwargs):
pass
与Alias、Trigger的差异是,Command包含几个新的会经常被使用的方法调用,见下表。
方法 | 参数 | 返回值 | 含义 |
---|---|---|---|
create_task | coro, args, name | asyncio.Task | 实际是asyncio.create_task的包装,在创建任务的同时,将其加入了session的task清单和本Command的Task清单,可以保证执行,也可以供后续操作使用 |
reset | 无 | 无 | 复位该任务。复位除了清除标识位之外,还会清除所有未完成的task。在Command的多次调用时,要手动调用reset方法,以防止同一个命令被多次触发。 |
execute | cmd, *args, **kwargs | 无 | async定义的异步方法,在Command被执行时会自动调用该方法 |
先用一个简单的示例来说明Command的应用。在张金敖任务(机关人起源)中,当到达对应节点时,需要使用walk命令行走指定步数然后等待线索。现在想在行走指定步数之后自动使用walk -p停止下来,可以使用以下一个Command类来实现。此处所有逻辑在CmdWalk类override的execute方法中实现,对已经行走步数的技术是通过while循环实现。这种实现方式代码可读性好,其逻辑思维符合正常同步思维模式。
class CmdWalk(Command):
"北侠节点处的Walk指令,控制指定步数"
_help = """
定制walk命令使用参考,可接受正常walk命令,也可以使用诸如walk yangzhou 8来控制从节点向yangzhou节点行走,8步之后停下来
正常指令 含义
walk xxx: 标准walk指令
walk -c xxx: 显示路径
walk -p: 暂停行走
walk: 继续行走
特殊指令:
walk xxx 3: 第二个参数的数字,控制行走的步数,用于张金敖任务
"""
def __init__(self, session, *args, **kwargs):
self.tri_room = Trigger(self.session, id = 'tri_room', patterns = r'^[>]*(?:\s)?(\S.+)\s-\s*[★|☆|∞]?$', isRegExp = True)
self.session.addTrigger(self.tri_room)
super().__init__(session, "^walk(?:\s(\S+))?(?:\s(\S+))?(?:\s(\S+))?", *args, **kwargs)
async def execute(self, cmd, *args, **kwargs):
try:
m = re.match(self.patterns, cmd)
if m:
para = list()
for i in range(1, 4):
if m[i] != None:
para.append(m[i])
# 如果是步数设置,需要人工控制
# 例如walk yangzhou 8
if (len(para) > 0) and para[-1].isnumeric():
cnt = int(para[-1])
step = "walk " + " ".join(para[:-1])
self.info(f"即将通过walk行走,目的地{para[-2]},步数{para[-1]}步...", "walk")
self.session.writeline("set walk_speed 2") # 调节速度
await asyncio.sleep(1)
self.session.writeline(step)
# 使用循环控制步数
while cnt > 0:
cnt = cnt - 1
await self.tri_room.triggered() # 等待房间名被触发cnt次
self.session.writeline("walk -p")
self.session.writeline("unset walk_speed") # 恢复速度
self.info(f"通过walk行走,目的地{para[-2]},步数{para[-1]}步完毕", "walk")
# 否则直接命令发送
else:
self.session.writeline(cmd)
except Exception as e:
self.error(f"异步执行中遇到异常, {e}, 类型为 {type(e)}")
self.error(f"异常追踪为: {traceback.format_exc()}")
Command类也可以支持复杂逻辑带不同参数的指令,直至可以实现一个完整的任务辅助机器人。此处再举一个dazuo的例子。在北侠中,打坐是最长使用的命令之一。我们可能需要打坐到双倍内力即停止以进行后续任务,或者专门打坐修炼内力,或者使用dz命令进行打坐以打通任督二脉。在专门修炼内力时,还需要定期补充食物饮水。打坐的时候,也要统筹考虑气不足、精不足等各种可能遇到的情况。以下是一个完整实现的打坐命令,可以使用dzt xxx来执行上述所有操作。详细代码如下: 注:此Command调用了支持jifa/enable命令的CmdEnable,支持hpbrief的命令CmdHpbrief,以及支持食物饮水等生活的命令CmdLifMisc,具体代码未列出,但在阅读dazuoto命令代码时不影响可读性。
class CmdDazuoto(Command):
"""
各种打坐的统一命令, 使用方法:
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, cmdEnable, cmdHpbrief, cmdLifeMisc, *args, **kwargs):
super().__init__(session, "^(dzt)(?:\s+(\S+))?$", *args, **kwargs)
self._cmdEnable = cmdEnable
self._cmdHpbrief = cmdHpbrief
self._cmdLifeMisc = cmdLifeMisc
self._triggers = {}
self._initTriggers()
self._force_level = 0
self._dazuo_point = 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")
self.session.addTriggers(self._triggers)
def stop(self):
self.tri_dz_done.enabled = False
self._halted = True
self._always = False
async def dazuo_to(self, to):
# 开始打坐
dazuo_times = 0
self.tri_dz_done.enabled = True
if not self._force_level:
await self._cmdEnable.execute("enable")
force_info = self.session.getVariable("eff-force", ("none", 0))
self._force_level = force_info[1]
self._dazuo_point = (self._force_level - 5) // 10
if self._dazuo_point < 10: self._dazuo_point = 10
await self._cmdHpbrief.execute("hpbrief")
neili = int(self.session.getVariable("neili", 0))
maxneili = int(self.session.getVariable("maxneili", 0))
force_info = self.session.getVariable("eff-force", ("none", 0))
if to == "dz":
cmd_dazuo = "dz"
self.tri_dz_dz.enabled = True
self.info('即将开始进行dz,以实现小周天循环', '打坐')
elif to == "max":
cmd_dazuo = "dazuo max"
need = math.floor(1.90 * maxneili)
self.info('当前内力:{},需打坐到:{},还需{}, 打坐命令{}'.format(neili, need, need - neili, cmd_dazuo), '打坐')
elif to == "once":
cmd_dazuo = "dazuo 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()))
self.session.writeline(cmd_dazuo)
done, pending = await asyncio.wait(waited_tris, timeout =100, return_when = "FIRST_COMPLETED")
tasks_done = list(done)
tasks_pending = list(pending)
for t in tasks_pending:
t.cancel()
if len(tasks_done) == 1:
task = tasks_done[0]
_, name, _, _ = task.result()
if name in (self.tri_dz_done.id, self.tri_dz_dz.id):
if (to == "always"):
dazuo_times += 1
if dazuo_times > 100:
# 此处,每打坐200次,补满水食物
self.info('该吃东西了', '打坐')
await self._cmdLifeMisc.execute("feed")
dazuo_times = 0
elif (to == "dz"):
dazuo_times += 1
if dazuo_times > 50:
# 此处,每打坐50次,补满水食物
self.info('该吃东西了', '打坐')
await self._cmdLifeMisc.execute("feed")
dazuo_times = 0
elif (to == "max"):
await self._cmdHpbrief.execute("hpbrief")
neili = int(self.session.getVariable("neili", 0))
if self._force_level >= 161:
self.session.writeline("exert recover")
await asyncio.sleep(0.2)
elif (to == "once"):
self.info('打坐1次任务已成功完成.', '打坐')
break
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)
elif name == self.tri_dz_nojing.id:
await asyncio.sleep(1)
self.session.writeline("exert regenerate")
await asyncio.sleep(1)
elif name == self.tri_dz_wait.id:
await asyncio.sleep(5)
elif name == self.tri_dz_halt.id:
self.info("打坐已被手动halt中止。", '打坐')
break
elif name == self.tri_dz_finish.id:
self.info("内力已最大,将停止打坐。", '打坐')
break
else:
self.info("命令执行中发生错误,请人工检查", '打坐')
return self.FAILURE
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()}")
GMCP(Generic Mud Communication Protocol)是一种用于传递非显示字符的MUD通信协议,在标准telnet协议的基础上定义了特定的选项协商和子协商(命令为0xC9)。有关GMCP协议的详细信息参见 https://tintin.mudhalla.net/protocols/gmcp/
北侠是支持GMCP进行数据通信的,详细可以在游戏中使用tune gmcp进行查看可以支持的具体GMCP种类。
PyMUD使用GMCPTrigger类来进行GMCP消息的处理。其使用方法与标准Trigger基本相同,也同样支持同步与异步两种方式。与Trigger的最大差异在于,GMCPTrigger使用name参数作为触发条件,该name必须与MUD服务器发送的GMCP消息的名称完全一致(区分大小写,因此大小写也必须一致)。
要在会话中使用GMCP触发器,要做的两件事是:
- 构建一个GMCPTrigger类(或其子类)的实例。
- 将该实例通过session.addGMCPTrigger方法增加到会话的清单中。
GMCPTrigger的构造函数如下:
class GMCPTrigger(BaseObject):
"""
支持GMCP收到数据的处理,可以类似于Trigger的使用用法
GMCP必定以指定name为触发,触发时,其值直接传递给对象本身
"""
def __init__(self, session, name, *args, **kwargs):
pass
GMCPTrigger也使用了统一的函数语法,必须指定的位置参数包括session(指定会话)、name(为GMCPTrigger服务器发送的消息名称),其余的参数都通过命名参数在kwargs中指定,与Trigger基本相同。具体如下:
- id: 唯一标识符。不指定时,默认生成session中此类的唯一标识。
- group: GMCP触发器所属的组名,默认未空。支持使用session.enableGroup来进行整组对象的使能/禁用
- enabled: 使能状态,默认为True。标识是否使能该触发器。
- onSuccess: 函数的引用,默认为空。当触发器被触发时自动调用的函数,函数类型应为func(id, value)形式。
以下是针对北侠状态GMCP消息的一个触发器实例,由北侠玩家shendu提供。
class GMCP():
def __init__(self, session, *args, **kwargs):
self._gmcps = {}
self.session = session
self._gmcps['GMCP.Status'] = GMCPTrigger(self.session, r"GMCP.Status", onSuccess = self.GMCP_Status)
self.session.addGMCPs(self._gmcps)
def GMCP_Status(self, name, values):
Status_list = {}
if isinstance(values, str):
values = json.loads(values, strict=False)
for i in values:
if isinstance(values[i], str):
values[i] = re.sub('\[.*?m','',values[i])
Status_list[i] = values[i]
Status_name = self.session.getVariable('name')
# self.session.info(Status_name,'Status_name是')
if Status_name:
if 'name' in Status_list.keys():
if Status_list['name'] == Status_name:
result = True
else:
result = False
else:
result = True
else:
result = True
if result:
if 'combat_exp' in Status_list:
exp = int(self.session.getVariable('combat_exp',0))
Status_list['combat_exp'] = int(Status_list['combat_exp'])
if Status_list['combat_exp'] != exp:
exp_add = Status_list['combat_exp'] - exp
if exp_add > 0:
exp_add = f'+{exp_add}'
else:
exp_add = str(exp_add)
self.session.setVariable('exp_add',exp_add)
if 'potential' in Status_list:
potential = self.session.getVariable('potential',0)
if Status_list['potential'] != potential:
potential_add = Status_list['potential'] - potential
if potential_add > 0:
potential_add = f'+{potential_add}'
else:
potential_add = str(potential_add)
self.session.setVariable('potential_add',potential_add)
for i in Status_list:
self.session.setVariable(i, Status_list[i])
else:
# self.session.info(Status_list,'暂不添加')
pass
可以通过脚本定制状态窗口内容。要定制状态窗口的显示内容,将session.status_maker属性赋值为一个返回支持显示结果的函数即可。可以支持标准字符串或者prompt_toolkit所支持的格式化显示内容。 有关prompt_toolkit的格式化字符串显示,可以参见该库的官方帮助页面: https://python-prompt-toolkit.readthedocs.io/en/master/pages/printing_text.html
以下是一个实现状态窗口的示例:
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
from settings import Settings
class Configuration:
def __init__(self, session) -> None:
session.status_maker = self.status_window
def status_window(self):
formatted_list = list()
(jing, effjing, maxjing, jingli, maxjingli, qi, effqi, maxqi, neili, maxneili) = self.session.getVariables(("jing", "effjing", "maxjing", "jingli", "maxjingli", "qi", "effqi", "maxqi", "neili", "maxneili"))
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('charname'), self.session.getVariable('char'))))
formatted_list.append(("", " "))
formatted_list.append((Settings.styles["title"], "【门派】"))
formatted_list.append((Settings.styles["value"], "{}".format(self.session.getVariable('menpai'))))
formatted_list.append(("", " "))
formatted_list.append((Settings.styles["title"], "【存款】"))
formatted_list.append((Settings.styles["value"], "{}".format(self.session.getVariable('deposit'))))
formatted_list.append(("", " "))
formatted_list.append((Settings.styles["title"], "【食物】"))
food = int(self.session.getVariable('food', '0'))
if food < 100:
style = Settings.styles["red"]
elif food < 200:
style = Settings.styles["yellow"]
elif food < 350:
style = Settings.styles["green"]
else:
style = Settings.styles["skyblue"]
formatted_list.append((style, "{}".format(food)))
formatted_list.append(("", " "))
formatted_list.append((Settings.styles["title"], "【饮水】"))
water = int(self.session.getVariable('water', '0'))
if water < 100:
style = Settings.styles["red"]
elif water < 200:
style = Settings.styles["yellow"]
elif water < 350:
style = Settings.styles["green"]
else:
style = Settings.styles["skyblue"]
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('exp'))))
formatted_list.append(("", " "))
formatted_list.append((Settings.styles["title"], "【潜能】"))
formatted_list.append((Settings.styles["value"], "{}".format(self.session.getVariable('pot'))))
formatted_list.append(("", " "))
formatted_list.append((Settings.styles["title"], "【惯导】"))
if ins:
formatted_list.append((Settings.styles["skyblue"], "正常"))
formatted_list.append(("", " "))
formatted_list.append((Settings.styles["title"], "【位置】"))
formatted_list.append((Settings.styles["green"], f"{loc['city']} {loc['name']}({loc['id']})"))
else:
formatted_list.append((Settings.styles["red"], "丢失"))
formatted_list.append(("", " "))
formatted_list.append((Settings.styles["title"], "【位置】"))
formatted_list.append((Settings.styles["value"], f"{self.session.getVariable('%room')}"))
# a new-line
formatted_list.append(("", "\n"))
# line 2. hp
if jing != None:
formatted_list.append((Settings.styles["title"], "【精神】"))
if int(effjing) < int(maxjing):
style = Settings.styles["red"]
elif int(jing) < 0.8 * int(effjing):
style = Settings.styles["yellow"]
else:
style = Settings.styles["green"]
formatted_list.append((style, "{0}[{1:3.0f}%] / {2}[{3:3.0f}%]".format(jing, 100.0*float(jing)/float(maxjing), effjing, 100.0*float(effjing)/float(maxjing),)))
formatted_list.append(("", " "))
formatted_list.append((Settings.styles["title"], "【气血】"))
if int(effqi) < int(maxqi):
style = Settings.styles["red"]
elif int(qi) < 0.8 * int(effqi):
style = Settings.styles["yellow"]
else:
style = Settings.styles["green"]
formatted_list.append((style, "{0}[{1:3.0f}%] / {2}[{3:3.0f}%]".format(qi, 100.0*float(qi)/float(maxqi), effqi, 100.0*float(effqi)/float(maxqi),)))
formatted_list.append(("", " "))
formatted_list.append((Settings.styles["title"], "【精力】"))
if int(jingli) < 0.6 * int(maxjingli):
style = Settings.styles["red"]
elif int(jingli) < 0.8 * int(maxjingli):
style = Settings.styles["yellow"]
elif int(jingli) < 1.2 * int(maxjingli):
style = Settings.styles["green"]
else:
style = Settings.styles["skyblue"]
formatted_list.append((style, "{0} / {1}[{2:3.0f}%]".format(jingli, maxjingli, 100.0*float(jingli)/float(maxjingli))))
formatted_list.append(("", " "))
formatted_list.append((Settings.styles["title"], "【内力】"))
if int(neili) < 0.6 * int(maxneili):
style = Settings.styles["red"]
elif int(neili) < 0.8 * int(maxneili):
style = Settings.styles["yellow"]
elif int(neili) < 1.2 * int(maxneili):
style = Settings.styles["green"]
else:
style = Settings.styles["skyblue"]
formatted_list.append((style, "{0} / {1}[{2:3.0f}%]".format(neili, maxneili, 100.0*float(neili)/float(maxneili))))
formatted_list.append(("", " "))
# a new-line
formatted_list.append(("", "\n"))
# line 3. GPS info
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 Configuration.DIRS_ABBR.keys():
dir = Configuration.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