├── .DS_Store ├── .gitignore ├── .readthedocs.yaml ├── LICENSE.txt ├── README.md ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── _static │ ├── chars_menu.png │ ├── create_menu_win.png │ ├── icon.jpg │ ├── init.png │ ├── install_and_run.png │ ├── main_ui.png │ ├── status_window.png │ ├── ui_empty.png │ ├── ui_new_session_1.png │ ├── ui_new_session_2.png │ ├── ui_show_01.png │ └── ui_show_02.png │ ├── conf.py │ ├── css │ └── custom.css │ ├── examples.py │ ├── hotkeys.rst │ ├── index.rst │ ├── installation.rst │ ├── plugins.rst │ ├── references.rst │ ├── scripts.rst │ ├── settings.md │ ├── syscommand.rst │ ├── ui.rst │ └── updatehistory.md ├── pyproject.toml ├── src └── pymud │ ├── __init__.py │ ├── __main__.py │ ├── decorators.py │ ├── dialogs.py │ ├── extras.py │ ├── i18n.py │ ├── lang │ ├── i18n_chs.py │ └── i18n_eng.py │ ├── logger.py │ ├── main.py │ ├── modules.py │ ├── objects.py │ ├── pkuxkx.py │ ├── protocol.py │ ├── pymud.py │ ├── session.py │ └── settings.py ├── tox.ini └── uv.lock /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crapex/pymud/96dc03a5e2ab0c3384e141dbd9ff82a68a91b0fc/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # general things to ignore 2 | build/ 3 | 4 | dist/ 5 | *.egg-info/ 6 | *.egg 7 | *.py[cod] 8 | __pycache__/ 9 | *.so 10 | *~ 11 | 12 | # due to using tox and pytest 13 | .tox 14 | .cache 15 | 16 | # debug is my debug-venv-dir 17 | debug 18 | debug/ 19 | 20 | # docs build is build test directory 21 | docs/build 22 | docs/build/ 23 | 24 | # my update file for debugging 25 | update 26 | update.* 27 | .vscode/ 28 | src/.DS_Store 29 | docs/.DS_Store 30 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | build: 4 | os: "ubuntu-22.04" 5 | tools: 6 | python: "3.12" 7 | 8 | python: 9 | install: 10 | - requirements: docs/requirements.txt 11 | 12 | sphinx: 13 | configuration: docs/source/conf.py 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyMUD - Python原生MUD客户端 2 | ## 简介 3 | 4 | ### 北侠WIKI: https://www.pkuxkx.net/wiki/tools/pymud 5 | ### 源代码地址: https://github.com/crapex/pymud 6 | ### 帮助文档地址: https://pymud.readthedocs.org 7 | ### PyPi项目地址: https://pypi.org/project/pymud 8 | ### 由deepwiki自动生成的项目理解文档地址: https://deepwiki.com/crapex/pymud 9 | ### PyMUD用户shanghua写的入门教程文档:https://www.pkuxkx.net/forum/forum.php?mod=viewthread&tid=49999&forumuid=12067 10 | ### 交流QQ群:554672580 11 | 12 | 13 | ### 北大侠客行Mud (www.pkuxkx.net),最好的中文Mud游戏! 14 | ### PyMUD是我为了更好的玩北大侠客行,特意自行开发的MUD客户端。PyMUD具有以下特点: 15 | + 原生Python开发,除prompt-toolkit及其依赖库 wcwidth, pygment, pyperclip 外,不需要其他第三方库支持 16 | + 原生Python的asyncio实现的通信协议处理,支持async/await语法在脚本中直接应用,脚本实现的同步异步两种模式由你自己选择 17 | + 基于控制台的全屏UI界面设计,支持鼠标操作(Android上支持触摸屏操作),极低资源需求,在单核1GB内存的Linux VPS上也可流畅运行 18 | + 支持分屏显示,在数据快速滚动的时候,上半屏保持不动,以确保不错过信息 19 | + 解决了99%情况下,北大侠客行中文对不齐,也就是看不清字符画的问题 20 | + 真正的支持多session会话,支持命令或鼠标切换会话 21 | + 原生支持多种服务器端编码方式,不论是GBK、BIG5、还是UTF-8 22 | + 支持NWAS、MTTS协商,支持GMCP、MSDP、MSSP协议 23 | + 一次脚本开发,多平台运行。只要能在该平台上运行python,就可以运行PyMUD客户端 24 | + 脚本所有语法均采用Python原生语法,因此你只要会用Python,就可以自己写脚本,免去了再去学习lua、熟悉各类APP的使用的难处 25 | + Python拥有极为强大的文字处理能力,用于处理文本的MUD最为合适 26 | + Python拥有极为丰富的第三方库,能支持的第三方库,就能在PyMud中支持 27 | + 我自己还在玩,所以本客户端会持续进行更新:) 28 | 29 | ### 哪些人适合使用PyMUD 30 | + 比较熟悉Python语言,会使用Python写代码的 -> PyMUD是纯Python原生开发,不会有其他客户端对Python的支持能比得过PyMUD 31 | + 虽不太熟悉Python语言,但有想法想学习Python语言的 -> 正好使用PyMUD玩北侠写脚本的过程中学习Python语言 32 | + 觉得还有些功能现在所有客户端都没有的 -> 你有需求,我来增加,就是这么方便 33 | + 觉得也想自己整一个定制客户端玩玩的 -> PyMUD完全开源,且除ui框架外全部都是一行一行代码自己写的,可以直接参考PyMUD的设计 34 | 35 | ## 版本更新信息 36 | 37 | ### 0.21.2 (2025-06-01) 38 | 39 | + 问题修复: 修复了当自动重连启动时,即使会话关闭了,也会自动重连的问题。 40 | + 实现调整: 重写了专用的会话缓冲、记录缓冲与PyMud缓冲显示控制器,在prompt_toolkit的原Buffer和BufferControl的基础仅提供了PYMUD所需的基础功能,以降低内存占用。 41 | 经测试,当前内存基本稳定,视会话数量和脚本情况差异,维持在几百兆左右(500M以下),且不会有大幅波动。重写后,低配置的VPS也可以稳定运行PyMUD。 42 | 43 | ### 0.21.0 (2025-05-20) 44 | 45 | + 功能新增: 各类对象的group属性,新增组、子组概念,用于快速成组操作对象。组可以包括子组,子组可以再包括子组。 46 | 组名以点号.分隔,例如:group1.subgroup1.subsubgroup1。组的层级没有限制。 47 | 组的概念可以用于快速处理多个对象,例如启用/禁用一组对象、删除一组对象等。例如,以下几个组的关系: 48 | - mygroup1 49 | - mygroup1.subgroup1 # 属于 mygroup1的子组 50 | - mygroup1.subgroup2 # 属于 mygroup1的子组 51 | - mygroup1.subgroup2.subsubgroup1 # 属于 mygroup1.subgroup2的子组,也同样属于更高层级mygroup1的子组 52 | - mygroup2 53 | - mygroup2.subgroup1 # 属于 mygroup2的子组 54 | 55 | + 功能新增: 新增了deleteGroup函数,用于删除指定的对象组。可同时指定组名、是否包含子组名、有效类型范围。函数定义和示例代码如下: 56 | 57 | ``` Python 58 | def deleteGroup(self, group: str, subgroup = True, types: Union[Type, Union[Tuple, List]] = (Alias, Trigger, Command, Timer, GMCPTrigger)): 59 | pass 60 | # 各参数含义: 61 | # group: 要删除的组名,可以是完整的组名,也可以是部分组名。例如:"group1" 或 "group1.subgroup1" 等。 62 | # subgroup: 是否包含子组。如果为True,则删除指定组及其所有子组的对象。如果为False,则仅删除指定组的对象,不包括子组。 63 | # types: 要删除的对象类型范围。可以是单个类型,也可以是类型的元组或列表。例如:Trigger, Alias, Command, Timer, GMCPTrigger 等。 64 | # 示例代码: 65 | # 删除所有属于group1的Trigger和Alias对象,包括子组如 group1.subgroup1 和 group1.subgroup2 等 66 | self.session.deleteGroup("group1", True, [Trigger, Alias]) 67 | # 删除所有属于group1的Trigger对象,但不包括子组 68 | self.session.deleteGroup("group1", False, [Trigger]) 69 | ``` 70 | 71 | + 功能新增: 对 #trigger, #alias, #timer, #gmcp, #command, #t+, #t- 等命令新增了组处理选项,用于对整组对象进行处理。各命令的语法格式类似。 72 | 处理组时,组名应以大于号>或者等于号=开头,紧跟组名(无空格)。当使用>时,表示操作针对当前组及所有所属子组,当使用=时,表示操作仅针对当前组。 73 | 例如下面代码: 74 | 75 | ``` 76 | #t+ >group1 表示启用所有属于group1以及其子组的所有可管理对象,包括Trigger、Alias、Command、Timer、GMCPTrigger 77 | #t- =group1.subgroup1 表示禁用所有仅属于group1.subgroup1的Trigger、Alias、Command、Timer、GMCPTrigger等对象 78 | #tri >group1 off 表示禁用所有属于group1以及其子组的Trigger对象 79 | #ali =group1.subgroup1 on 表示启用所有仅属于group1.subgroup1的Alias对象 80 | ``` 81 | 82 | + 功能新增: 调整了enableGroup处理,可以通过组名支持子组操作,也可以指定有效类型范围。例如下面代码: 83 | ``` Python 84 | class MyTestConfig(IConfig): 85 | def __init__(self, session, *args, **kwargs): 86 | self._objs = [ 87 | Trigger(session, "tri1", group = "group1"), 88 | Trigger(session, "tri2", group = "group1.subgroup1"), 89 | Trigger(session, "tri3", group = "group1.subgroup2"), 90 | Alias(session, "alias1", group = "group1"), 91 | Alias(session, "alias2", group = "group1.subgroup1"), 92 | Timer(session, 5, group = "group1.subgroup1") 93 | ] 94 | 95 | #以下调用可以同时禁用上述6个对象,因为 group1.subgroup1 和 group1.subgroup2 都属于 group1 的子组 96 | session.enableGroup("group1", False) 97 | #以下调用可以同时仅启用触发器tri1和别名alias1,因为通过subgroup参数限定了不传递到子组 98 | session.enableGroup("group1", True, subgroup = False) 99 | # 以下调用可以同时禁用对应发器和别名,但不禁用定时器,因为通过types参数指定了有效范围: 100 | session.enableGroup("group1.subgroup1", False, types = [Trigger, Alias]) 101 | 102 | ``` 103 | 104 | + 功能新增: 增加了多处异常追踪提示。在模块或插件的脚本中发生错误时,均会打印错误追踪信息,方便定位错误。 105 | + 功能新增: 新增 #echo 命令,类似于 #test 命令,但该命令只会模拟收到服务器数据,直接激发各匹配触发器,但不显示触发测试结果。 106 | + 功能新增: #load / #unload 现在支持当前会话对插件的临时启用和禁用,实现方式为调用插件里的PLUGIN_SESSION_CREATE和PLUGIN_SESSION_DESTROYE函数。群文件的moving.py插件写法可以支持。 107 | + 功能调整: 各会话变量保存的.mud文件,统一移到save子目录下。原来当前目录下的.mud文件,在对应会话重新加载时会自动移动,无需人工处理。 108 | + 功能新增: 增加了国际化(i18n)支持,原生开发语言为中文简体,目前使用AI翻译生成了英文。应用语言通过Settings中新增的language配置来控制,默认为"chs",可以在pymud.cfg中覆盖该配置。其值目前可以为"chs"、"eng"。自行翻译的语言可以在pymud/lang目录下下新增语言文件,文件名为i18n_加语言代码,例如"i18n_chs.py"表示可以使用"chs"语言,其中使用Python字典方式定义了所有需动态显示的文本内容。 109 | + 功能新增: 新增了使用元类型及装饰器来管理Pymud对象,包括Alias, Trigger, Timer, GMCPTrigger四种可以使用对应的装饰器,@alias, @trigger, @timer, @gmcp来直接在标记函数上创建。可以参考本版本中的pkuxkx.py文件写法和注意事项。 110 | + 功能新增: 新增了两个装饰器,@exception和@async_exception,用于捕获异常并调用session.error进行显示。@exception用于捕获同步异常,@async_exception用于捕获异步异常。参考如下: 111 | ``` Python 112 | from pymud import Command, Trigger, IConfig, exception, async_exception 113 | 114 | class MyCustomCommand(Command, IConfig): 115 | @exception 116 | def a_sync_routine(self, args: list[str]): 117 | # 这里的代码抛出的异常会被self.session.error捕获并显示 118 | something_that_may_raise_an_exception() 119 | 120 | @async_exception 121 | async def execute(self, args: list[str]): 122 | # 这里的代码抛出的异常会被self.session.error捕获并显示 123 | await something_that_may_raise_another_exception() 124 | 125 | # 上述代码相当于以下代码 126 | class MyCustomCommand(Command, IConfig): 127 | def a_sync_routine(self, args: list[str]): 128 | try: 129 | something_that_may_raise_an_exception() 130 | except Exception as e: 131 | self.session.error(error_msg_of_e) 132 | 133 | async def execute(self, args: list[str]): 134 | try: 135 | await something_that_may_raise_another_exception() 136 | except Exception as e: 137 | self.session.error(error_msg_of_e) 138 | ``` 139 | + 问题修复: 修复了Alias和Command执行时的优先级判断。之前未进行优先级判断,因此遇到能同时匹配的多个时,不一定优先级高的被触发。现在对Alias和Command进行了优先级判断,优先级高的先触发。 140 | + 问题修复: 修复Alias中的keepEval参数和oneShot参数。keepEval参数支持多个匹配成功的别名同时生效,oneShot参数支持一个匹配成功的别名生效后,后续的匹配不再生效。 141 | + 问题修复: 修复Command中的keepEval参数。以往同时匹配生效的Command会覆盖后续Command和Alias,当前会持续匹配。 142 | + 功能增强: 对几乎所有函数的参数进行了类型标注,增加了类型检查,提高了代码的可读性和可维护性,也便于自行编写脚本时的提示。 143 | + 功能增强: 为Session类型增加了commandHistory属性,用于查询发送到服务器的命令历史。保存的命令历史的数量由pymud.cfg中的client["history_records"]控制,默认为500。当该值为0时,不会保存命令历史。为-1时,会保存所有命令历史。 144 | + 功能调整: #help命令时,增加了上下两行分隔符显示,以便明显区分帮助输出和游戏输出。 145 | + 功能增强: 当前pymud界面中显示的版本号会自动从pyproject.toml中读取,以确保版本号的准确性和唯一性。 146 | + 问题修复: 修复了代码中的部分编码错误。新版Python中能容忍一些错误,但老版本不行。经修复,当前代码支持的Python版本已测试3.8确保可用。建议使用3.10或更高版本的Python。 147 | + 问题修复: 删除了extras.py中多余的MenuItem类型定义,该定义与prompt_toolkit中的MenuItem定义冲突。 148 | + 问题修复: 调整了众多代码中未检查对象是否为None即调用、使用的局部变量可能未经过初始化和赋值路径等的情况,保证程序运行的健壮性。 149 | + 问题修复: 修复了#test命令的帮助内容错误。实际#show命令不触发脚本,仅测试;而#test会触发脚本。 150 | + 问题修复: 修复了协议处理中MSDP编码解码处理错误的问题;修复了协议处理中默认encoding不传递导致某些情况下报解码错误的问题。 151 | + 示例更新: 更新了包中自带的pkuxkx.py,增加了@alias, @trigger, @timer, @gmcp的示例以及状态窗口的示例。 152 | 153 | ### 0.20.4 (2025-03-30) 154 | + 功能调整: 为插件功能新增了 PLUGIN_PYMUD_DESTROY 方法,用于在插件被卸载时,进行一些清理工作。 155 | + 功能调整: 将插件的 PLUGIN_PYMUD_START 方法的调用,从插件加载时刻移动到事件循环启动之后,这样在加载时,可以使用 asyncio.create_task或 asyncio.ensure_future 来执行一些异步操作 156 | 157 | ### 0.20.3 (2025-03-05) 158 | + 功能调整: 为适应MacOS下的快捷键,增加Shift+左右箭头同样作为切换会话的快捷键。 159 | + 功能调整: 会话关闭和APP退出时,偶尔受网络影响导致服务器掉线但本地未检测到时会无法退出。现增加最长10s等待,超时后会中断,强制退出。 160 | 161 | ### 0.20.2 (2024-11-26) 162 | + 功能调整: MTTS协商中,将256 Color明确写入协商回复。原先仅包含ANSI 和 TrueColor。推测武庙特殊颜色偶尔不正常与此有关(已测试无关)。 163 | + 功能调整: 修复了纯文本正则处理,目前理论上支持所有ANSI控制代码的处置,以正确响应纯文本触发器。 164 | + 功能调整: 修改了#var和#global的显示实现,提高了变量打印排列的整齐度和辨识度,以适应长值变量和复杂变量。 165 | + 问题修复: 修复了单行颜色代码跨行无法显示问题。现在星宿毒草可以正常辨认颜色了。 166 | + 功能调整: 调整了info/warning/error的显示处理,默认样式进行了修改。 167 | + 功能新增: 新增菜单选项:打开/关闭美化,以便于更好的在触发器时复制出正确的内容(以前计算可能不准确)。 168 | + 功能新增: 状态栏的分隔符可以通过本地设置取消了。在pymud.cfg的client中新增设置,将 status_divider 设置为 false 即可。 169 | + 功能调整: 在pymud.cfg的client中可以支持将buffer_lines设置为0了,表示不清除缓存。 170 | + 功能新增: 为状态栏显示函数增加了异常保护,再有status_maker出错的时候,状态栏会显示出错信息。 171 | 172 | ### 0.20.1 (2024-11-16) 173 | + 功能调整: 会话中触发器匹配实现进行部分调整,减少循环次数以提高响应速度 174 | + 功能调整: #test / #show 触发器测试功能调整,现在会对使能的和未使能的触发器均进行匹配测试。其中,#show 命令仅测试,而 #test 命令会导致触发器真正响应。 175 | + 功能新增: pymud对象新增了一个持续运行的1s的周期定时任务。该任务中会刷新页面显示。可以使用 session.application.addTimerTickCallback 和 session.application.removeTimerTickCallback 来注册和解除定时器回调。 176 | 177 | ### 0.20.0 (2024-08-25) 178 | + 功能调整: 将模块主入口函数从__main__.py中移动到main.py中,以使可以在当前目录下,可直接使用pymud,也可使用python -m pymud启动 179 | + 功能调整: 使用argsparser标准模块来配置命令行,可以使用 pymud -h 查看命令行具体参数及说明 180 | + 功能新增: 命令行参数增加指定启动目录的功能,参数为 -s, --startup_dir。即可以从任意目录通过指定脚本目录方式启动PyMUD了。 181 | - 例如, PS C:\> pymud -s d:\prog\pkuxkx 相当于 PS D:\prog\pkuxk> pymud 182 | + 问题修复: MacOS下 python -m pymud init 创建目录报错的问题。同时,将所有系统上的默认目录均使用 ~/pkuxkx (影响windows) 183 | + 功能调整: 恢复在__init__.py中增加PyMudApp的导出,可以恢复使用from pymud import PyMudApp了 184 | + 功能新增: 增加log功能,详见 #log 命令介绍、类参考中的 Logger 类,以及 Session 类的 handle_log 方法 185 | + 功能新增: 增加 #disconnect, #dis 命令,可以使当前会话从服务器断开。相当于操作菜单 会话->断开连接 186 | + 功能调整: 在没有session的时候,也可以执行#exit命令 187 | + 功能新增: #session 命令增加快捷创建会话功能,假如已有快捷菜单 世界->pkuxkx->newstart , 则可以通过 #session pkuxkx.newstart 直接创建该会话,效果等同于点击该菜单 188 | + 功能调整: 点击菜单创建会话时,若会话已存在,则将该会话切换为当前会话 189 | + 重大更新: 完全重写了模块的加载、卸载、重新加载方法,修复模块使用中的问题 190 | + 功能调整: 现在只要将一个类型继承 IConfig 接口,即被识别为配置类型。这种类型在模块加载时会自动创建其实例。当然,名称为Configuration的类型也同样被认为是配置类型,保持向前兼容性。唯一要求是,该类型的构造函数允许仅传递一个session对象。 191 | + 功能新增: 各类配置类型的卸载现在既可以定义在__unload__方法中,也可以定义在unload方法中。可以根据自己喜好选择一个即可。 192 | + 功能调整: 各配置类型加载和重新加载前,会自动调用模块的__unload__方法或unload方法(若有) 193 | + 功能新增: Command基类增加__unload__方法和unload方法,二者在从会话中移除该 Command 时均会自动调用。自定义的Command子类应覆盖这两种方法中的一种方法,并在其中增加清除类型自行创建的 Trigger, Alias 等会话对象。这样,模块卸载时只要移除命令本身,在命令中新建的其他关联对象将被一同移除。 194 | + 功能新增: 所有PyMUD基础对象类型及其子类型,包括 Alias, Trigger, Timer, Command, GMCPTrigger 及它们的子类型,在创建的时候会自动添加到会话中,无需再进行 addObject 等操作了 195 | + 问题修复: 修复部分正则表达式书写错误问题 196 | + 功能新增: Session类新增waitfor函数,用于执行一段代码后立即等待某个触发器的情况,简化原三行代码写法 197 | 198 | ``` Python 199 | # 原来为确保await triggered的任务在输入前等待,有时候需要这么写: 200 | task = self.create_task(self.tri1.triggered()) 201 | await asyncio.sleep(0.05) 202 | self.session.writeline('dazuo') 203 | await task 204 | 205 | # 现在可以一句话简写: 206 | await self.session.waitfor('dazuo', self.create_task(self.tri1.triggered())) 207 | ``` 208 | 209 | + 功能调整: Session类的addTriggers等方法接受的dict中,会将对象本身id作为会话处理id。当该id与key不一致时,会同时显示警告。 210 | + 功能新增: Session类新增addObject, addObjects, delObject, delObjects用于操作别名、定时器、触发器、GMCP触发器、命令等对象。 211 | - 使用示例: 212 | 213 | ```Python 214 | # 所有对象均可以使用 delObject 直接从会话中移除,会自动根据对象类型推断,无需通过函数名区分 215 | session.delObject(self.tri1) 216 | session.delObject(self.ali1) 217 | session.delObject(self.timer1) 218 | 219 | objs = [ 220 | Trigger(session, xxx, xxx), 221 | Alias(session, xxx), 222 | SimpleCommand(session, xxx), 223 | Timer(session, xxx), 224 | GMCPTrigger(session, xxx) 225 | ] 226 | 227 | session.delObjects(objs) # 可以直接从会话中移除一个数组中的所有对象,会自动判断对象类别 228 | ``` 229 | 230 | + 功能新增: Session类型新增idletime属性,可以获取本会话发呆秒数(float类型)。当会话处于未连接状态时,返回 -1。可以利用定时器,在其中检测 idletime 值,以在机器人出错后处理恢复 231 | + 功能新增: Session的所有异步命令调用函数增加返回值,现在调用 session.exec_async, exec_command_async 等方法执行的内容若匹配为命令时,会返回最后最后一个 Command 对象的 execute 函数的返回值 232 | - 例如, result = await self.session.cmds.cmd_runto.execute('rt yz') 与 result = await self.session.exec_async('rt yz') 等价,返回值相同 233 | - 但 result = await self.session.exec_async('rt yz;dzt'),该返回的result 仅是 dzt 命令的 execute 的返回值。 rt yz 命令返回值被丢弃。 234 | + 功能新增: 增加临时变量概念,变量名以下划线开头的为临时变量,此类变量不会被保存到 .mud 文件中。 235 | + 功能新增: 为 BaseObject 基类的 self.session 增加了 Session 类型限定,现在自定义 Command 等时候,使用 self.session 时会有 IntelliSence 函数智能提示了,所有帮助说明已补全 236 | + 问题修复: 修复 #var 等命令中,若含有中文则等号位置不对齐的问题 237 | + 功能调整: 在 #tri 等命令中,当对象的 group 为空时,将不再显示 group 属性,减少无用信息 238 | 239 | ### 0.19.4 (2024-04-20) 240 | + 功能调整: info 现在 msg 恢复为可接受任何类型参数,不一定是 str 241 | + 功能调整: #var, #global 指令中,现在可以使用参数扩展了,例如 #var max_qi @qi 242 | + 功能调整: #var, #global 指令中,现在对字符串会先使用 eval 转换类型,转换失败时使用 str 类型。例如, #var myvar 1 时,myvar类型将为int 243 | + 功能调整: 变量替代时,会自动实现类型转化,当被替代变量值为非 str 类型时不会再报错 244 | + 问题修复: 修复之前从后向前选择时,无法复制的问题 245 | 246 | ### 0.19.3post2 (2024-04-05) 247 | + 问题修复: 一次发送多个命令时,发送顺序可能不正确的情况 248 | + 功能增加: 新增一个exec_async函数,是exec函数的异步形式。可以在其他会话中异步执行一段代码 249 | + 帮助完善: 帮助文档逻辑完善,已完成整个包的内置文档的编写和修改 250 | + 注: 由于我没弄太明白 readthedocs.io 网站对于读取github源代码的逻辑,目前只能通过新发布正式版本的形式来使 readthedocs.io 网站的文档中的类参考自动更新。 251 | + 问题修复: 修复退出程序时的小bug 252 | 253 | ### 0.19.2post2 (2024-03-24) 254 | + 错误修复:订正部分错别字、错误帮助、错别格式 255 | + 系统完善:完善帮助体系,按reST格式重写所有有关的docstring 256 | + 功能调整:session.exec_command / exec_command_async / exec 系列命令调整,现在可以在exec时带变量参数了。例如 session.exec("dazuo @dzpt"),直接调用 dzpt的变量值 257 | + 功能调整: settings.py中,client字典增加配置reconnect_wait,为自动重连的等待时间,默认15s,可本地覆盖 258 | + 功能调整: 变通解决了菜单栏右边单击 帮助 菜单会响应问题 259 | + 错误修复: 修复了会话关闭时插件卸载的代码错误 260 | + 功能调整: 在会话关闭、程序退出时增加等待,确保收到服务器断开命令之后才关闭和退出 261 | + 问题修复: 在退出程序时增加了插件卸载调用 262 | + 实现调整: 在清除task的列表推导过程中去掉了类型判断以减少任务时间占用 263 | + 其他调整: 从包中删除了拷贝过来作为参考的文件 264 | + 帮助完善: 帮助文档逻辑完善 265 | + 实现调整: 改用官方示例的task清除方式,每个任务结束后清除 266 | 267 | ### 0.19.1 (2024-03-06) 268 | + 功能新增: 新增鼠标启用禁用功能,以适用于ssh远程情况下的复制功能。F2快捷键可以切换状态。当鼠标禁用时,底部状态栏右侧会显示“鼠标已禁用状态” 269 | + 功能新增: 新增快捷键F1会直接通过浏览器打开帮助网址 https://pymud.readthedocs.io/ 270 | + 功能新增: 新增默认快捷键F3=#ig, F4=#cls, F11=#close, F12=#exit。此几个快捷键通过配置文件进行配置,可以自行定义或修改。F1、F2为写死的系统功能。 271 | + 功能调整: 将除#session之外的所有其他#命令实现统一到Session类中实现,这些命令均支持通过Session.exec_command运行 272 | + 功能调整: python -m pymud init时,创建的pymud.cfg文件增加了keys字典 273 | 274 | ### 0.19.0 (2024-03-01) 275 | + 实现调整: session.info/warning/error处理多行时,会给每一行加上同样颜色 276 | + 功能新增: 初次运行时,可以使用python -m pymud init来初始化环境,自动创建目录并在该目录中建立配置文件和样例脚本文件 277 | + 实现调整: 将缓冲清除行数的实现调整到SessionBuffer中,减少代码耦合并进一步降低内存占用 278 | + 功能新增: 新增命令行命令#T+, #T-, 可以使能/禁用指定组,相当于session.enableGroup操作 279 | + 功能新增: 新增命令行命令#task,可以列出所有系统管理的Task清单,主要用于开发测试 280 | + 实现调整: 调整系统管理Task的清空和退出机制,减少处理时间占用和内存占用 281 | + 实现调整: 调整COPY-RAW模式复制,即使仅选中行中的部分内容,也自动识别整行(多行模式也是整个多行) 282 | + 功能新增: Settings中新增keys字典,用于定义快捷键。可定义快捷键参见prompt_toolkit中Keys的定义。其值为可在session.exec_command运行支持的所有内容。该字典内容可以被pymud.cfg所覆盖。 283 | 284 | ### 0.18.4post4 (2024-02-23) 285 | + 功能新增:新增Settings.client["buffer_lines"],表示保留的缓冲行数(默认5000)。当Session内容缓冲行数达到该值2倍时(10000行),将截取一半(5000行),后一半内容进行保留,前一半丢弃。此功能是为了减少长时挂机的内存消耗和响应时间。 286 | + 功能修复:解决在显示美化(Settings.client["beautify"])打开之后,复制部分文字不能正确判断起始终止的问题。 287 | + 功能调整:修改缓冲行数判断逻辑,加快客户端判断响应速度。 288 | + 问题调整:修改缓冲截取处理中的小BUG。 289 | + 功能调整:将帮助窗口中的链接改到帮助网址: https://pymud.readthedocs.org 290 | + 问题修复:修复了随包提供的pkuxkx.py样例脚本中的几处错误 291 | 292 | ### 0.18.3 (2024-02-07) 293 | + 功能调整:原#unload时通过调用__del__来实现卸载的时间不可控,现将模块卸载改为调用unload函数。若需卸载时人工清除有关定时器、触发器等,请在Configuration类下新增unload函数(参数仅self),并在其中进行实现 294 | + 功能新增:新增会话Variable和全局Global的删除接口。可以通过session.delVariable(name)删除一个变量,可以通过session.delGlobal(name)来删除一个全局Global变量 295 | 296 | ### 0.18.2 (2024-02-06) 297 | + 问题修复:修改了定时器实现,以避免出现递归调用超限异常 298 | + 问题修复:修改了参数替代时的默认值,从None改为字符串"None",以避免替代时报None异常 299 | 300 | ### 0.18.1 (2024-02-05) 301 | + 问题修复:统一处置了task.cancel的参数和create_task的name属性,以适应更低版本的python环境(低至3.8) 302 | + 实现调整:为解决同步/异步执行问题,在CodeLine和CodeBlock的实现中,会通过调用命令来判断是否使用同步模式(默认为异步)。#gag、#replace为强制同步,#wa为强制异步。当同时存在时,同步失效,异步执行。 303 | + 实现调整:将%line、%raw的访问传递到触发器内部的执行中,避免同步异步问题。 304 | + 新增文档:将帮助文档添加到本项目,帮助文档自动同步到 pymud.readthedocs.org (文档内容暂未更新) 305 | 306 | ### 0.18.0 (2024-01-24) 307 | + 问题修复:修复了delTrigger/delAlias等等无法删除对象的问题 308 | + 功能调整:delTrigger等函数,修改为既可以接受Trigger对象本身,也可以接受其id。其他类似 309 | + 功能增加:增加了delTriggers(注意,带s)等函数,可以删除多个指定对象。可接受列表、元组等可迭代对象,并且其内容既可以为对象本身,也可以为id。 310 | + 功能增加:增加了session.reset()功能,可清除会话所有有关脚本信息。也可以在命令行使用#reset调用,另外,#unload不带参数调用时,有同样效果 311 | + 功能增加:增加了#ignore/#ig参数,类似于zmud的#ignore功能,可以切换全局触发器禁用状态。当全局被禁用时,底部状态栏右侧会显示此状态。(未全局禁用时不显示) 312 | + 功能调整:移除了会话切换时,状态栏显示的内容 313 | + 功能调整:会话命令的执行整体进行了实现调整,将参数替代延迟到特定命令执行时刻。(此实现影响面较大,请大家使用中发现BUG时都报告下,我及时修改) 314 | + 功能调整:代码块现在可以使用{}括起来了。这种情况下,命令和命令可以嵌套了。例如,#3 {#3 get g1b from bo yu;combine gem;pack gem;#wa 3000},该代码可以执行三次合并g1b宝石 315 | + 功能新增:增加了#ali,#tri,#ti的三参数使用,可以在命令行直接代码创建SimpleAlias, SimpleTrigger和SimpleTimer。 316 | + 使用示例:#ali {gp\s(\S+)} {get %1 from corpse}, #tri {^[> ]*【\S+】.+} {#mess %line}, #ti 10 {xixi;haha} 317 | + 功能新增:新增#session_name cmd命令,可以直接使名为session_name的会话执行cmd命令 318 | + 功能新增:session类型新增exec方法,使用方法为:session.exec(cmd, session_name)。可以使名为session_name的会话执行cmd命令。当不指定session_name时,在当前会话执行。 319 | + 功能调整:定时器创建时若不指定id,其自动生成的id前缀由tmr调整为ti 320 | + 实现调整:将#all、#session_name cmd等命令的实现从pymud.py中移动到了session.py中,这样可以在脚本中使用session.exec_command("#all xixi")。 321 | + 问题修复:修复了点击菜单"重新加载脚本配置"报错的问题 322 | + 功能调整:从菜单里点击创建会话时,会自动以登录名为本会话创建id变量 323 | + 当前已知问题:由于同步/异步执行问题,在SimpleTrigger中,#gag和#replace的执行结果会很奇怪,可能会隐藏和替换掉非触发行。可行的办法为在onSuccess里,调用session.replace进行处理。 324 | 325 | ### 0.17.4 (2024-01-08) 326 | + 问题修复:修复了DotDict在dump时出现错误的问题 327 | + 问题修复:修改了reconnect的实现方式,修复了断开重连时报错的问题 328 | + 功能增加:为Session增加两个事件属性,分别为event_connected和event_disconnected,接受一个带有session参数的函数,在连接和连接断开时触发。 329 | + 功能调整:调整了时间显示格式,只显示到秒,不显示毫秒数。 330 | 331 | ### 0.17.3 (2024-01-02) 332 | + 问题修复:修复了原有的#repeat功能。命令行#repeat/#rep可以重复输入上一次命令(这个基本没用,主要是我在远程连接时,手机上没有方向键...) 333 | + 问题修复:修改定时器的实现方式,真正修复了定时器每reload后会新增一个的bug。 334 | + 功能增加:命令行使用#tri, #ali, #cmd, #ti时,除了接受on/off参数外,增加了del参数,可以删除对应的触发器、别名、命令、定时器。例如:#ti tm_test del 可以删除id为“tm_test”的定时器。 335 | + 功能调整:调整了#help {cmd}的显示格式,最后一行也增加了换行符,确保后续数据在下一行出现。 336 | + 功能调整:调整了Timer和SimpleTimer在#timer时的显示格式。 337 | + 实现调整:调整了Session.clean实现中各对象清理的顺序,将任务清除移到了最后。 338 | 339 | ### 0.17.2post4 (2023-12-29) 340 | + 功能修改:会话菜单 "显示/隐藏命令" 和 "打开/关闭自动重连" 操作后,增加在当前会话中提示状态信息。 341 | + 功能修改:Timer实现进行修改,以确保一个定时器仅创建一个任务。 342 | + 功能调整:Timer对象在复位Session对象时,也同时复位。目的是确保reload时不重新创建定时器任务。 343 | + 功能调整:在会话连接时,不再复位Session有关对象信息。该复位活动仅在连接断开时和脚本重新加载时进行。 344 | + 功能调整:启动PYMUD时,会将控制台标题设置为PYMUD+版本号。 345 | + 问题修复:修复会话特定脚本模块会被其他会话加载的bug。 346 | + 问题修复:修复定时器Timer中的bug。 347 | 348 | ### 0.17.1post1 (2023-12-27) 349 | 本版对模块功能进行了整体调整,支持加载/卸载/重载/预加载多个模块,具体内容如下: 350 | + 当模块中存在名为Configuration类时,以主模块形式加载,即:自动创建该Configuration类的实例(与原脚本相同) 351 | + 当模块中不存在名为Configuration类时,以子模块形式加载,即:仅加载该模块,但不会创建Configuration的实例 352 | + #load命令支持同时加载多个模块,模块名以半角逗号(,)隔开即可。此时按给定的名称顺序逐一加载。如:#load mod1,mod2 353 | + 增加#unload命令,卸载卸载名称模块,同时卸载多个模块时,模块名以半角逗号(,)隔开即可。卸载时,如果该模块有Configuration类,会自动调用其__del__方法 354 | + 修改reload命令功能,当不带参数时,重新加载所有已加载模块,带参数时,首先尝试重新加载指定名称模块,若模块中不存在该名称模块,则重新加载指定名称的插件。若存在同名模块和插件,则仅重新加载插件(建议不要让插件和模块同名) 355 | + 增加#modules(简写为#mods)命令,可以列出所有已经加载的模块清单 356 | + Session类新增load_module方法,可以在脚本中调用以加载给定名称的模块。该方法接受1个参数,可以使用元组/列表形式指定多个模块,也可以使用字符串指定单个模块 357 | + Session类新增unload_module方法,可以在脚本中调用以卸载给定名称的模块。参数与load_module类似。 358 | + Session类新增reload_module方法,可以在脚本中调用以重新加载给定名称的模块。当不指定参数时,重新加载所有模块。当指定1个参数时,与load_module和unload_module方法类似 359 | + 修改Settings.py和本地pymud.cfg文件中sessions块脚本的定义的可接受值。默认加载脚本default_script现可接受字符串和列表以支持多个模块加载。多个模块加载有两种形式,既可以用列表形式指定多个,如["script1","script2"],也可以用字符串以逗号隔开指定多个,如"script1,script2" 360 | + 修改Settings.py和本地pymud.cfg文件中sessions块脚本中chars指定的会话菜单参数。当前,菜单后面的列表参数可以支持额外增加第3个对象,其中第3个为该会话特定需要加载的模块。该参数也可以使用逗号分隔或者列表形式。 361 | + 当创建会话时,自动加载的模块会首先加载default_script中指定的模块名称,然后再加载chars中指定的模块名称。 362 | + 上述所有修改均向下兼容,不影响原脚本使用。 363 | + 一个新的修改后的pymud.cfg示例如下 364 | ``` 365 | { 366 | "sessions": { 367 | "pkuxkx" : { 368 | "host" : "mud.pkuxkx.net", 369 | "port" : "8081", 370 | "encoding" : "utf8", 371 | "autologin" : "{0};{1}", 372 | "default_script": ["pkuxkx.common", "pkuxkx.commands", "pkuxkx.main"], 373 | "chars" : { 374 | "char1": ["yourid1", "yourpassword1"], 375 | "char2": ["yourid2", "yourpassword2", "pkuxkx.wudang"], 376 | "char3": ["yourid3", "yourpassword3", "pkuxkx.wudang,pkuxkx.lingwu"], 377 | "char4": ["yourid4", "yourpassword4", ["pkuxkx.shaolin","pkuxkx.lingwu"]] 378 | } 379 | } 380 | } 381 | } 382 | ``` 383 | 384 | + 问题修复:修复enableGroup中定时器处的bug 385 | + 功能修改:会话连接和重新连接时,取消原定时器停止的设定,目前保留为只清除所有task、复位Command 386 | + 功能修改:auto_reconnect设定目前对正常/异常断开均有效。若设置为True,当连接断开后15s后自动重连 387 | + 功能修改:会话菜单下增加“打开/关闭自动重连”子菜单,可以动态切换自动重连是否打开。 388 | 389 | ### 0.17.0 (2023-12-24) 390 | + 功能修改:调整修改GMCP数据的wildcards处理方式,恢复为eval,其余不变。(回滚0.16.2版更改) 391 | + 功能修改:将本地pymud.cfg文件的读取默认编码调整为utf8,以避免加载出现问题 392 | + 问题修复:sessions.py中,修复系统command与会话command重名的问题(这次才发现) 393 | + 功能修改:将自动脚本加载调整到session创建初始,而不论是否连接服务器 394 | + 功能修改:脚本load和reload时,不再清空任何对象,保留内容包括:中止并清空所有task,关闭所有定时器,将所有异步对象复位 395 | + 功能修改:去掉了左右边框 396 | + 问题修复:修复了当使用session.addCommand/addTrigger/addAlias等添加对象,而对象是Command/Trigger/Alias等的子类时,由于类型检查失败导致无法成功的问题 397 | + 功能修改:增加自动重连配置,Settings.client["auto_reconnect"]配置,当为True时,若连接过程中出现异常断开,则10秒后自动重连。该配置默认为False。 398 | + 功能修改:当连接过程中出现异常时,异常提示中增加异常时刻。 399 | + 功能修改:#reload指令增加可以重新加载插件功能。例如,#reload chathook会重新加载名为chathook的插件。 400 | + 功能增加:增加#py指令,可以直接在命令行中写代码并执行。执行的上下文环境为当前环境,即self代表当前session。例如,#py self.writeline("xixi")相当于直接在脚本会话中调用发送xixi指令 401 | + 功能新增:新增插件(Plugins)功能。将自动读取pymud模块目录的plugins子目录以及当前脚本目录的plugins子目录下的.py文件,若发现遵照插件规范脚本,将自动加载该模块到pymud。可以使用#plugins查看所有被加载的插件,可以直接带参数插件名(如#plugins myplugin)查看插件的详细信息(自动打印插件的__doc__属性,即写在文件最前面的字符串常量)插件文件中必须有以下定义: 402 | 403 | |名称|类型|状态|含义| 404 | |-|-|-|-| 405 | |PLUGIN_NAME|str|必须有|插件唯一名称| 406 | |PLUGIN_DESC|dict|必须有|插件描述信息的详情,必要关键字包含VERSION(版本)、AUTHOR(作者)、RELEASE_DATE(发布日期)、DESCRIPTION(简要描述)| 407 | |PLUGIN_PYMUD_START|func(app)|函数定义必须有,函数体可以为空|PYMUD自动读取并加载插件时自动调用的函数, app为PyMudApp(pymud管理类)。该函数仅会在程序运行时,自动加载一次| 408 | |PLUGIN_SESSION_CREATE|func(session)|函数定义必须有,函数体可以为空|在会话中加载插件时自动调用的函数, session为加载插件的会话。该函数在每一个会话创建时均被自动加载一次| 409 | |PLUGIN_SESSION_DESTROY|func(session)|函数定义必须有,函数体可以为空|在会话中卸载插件时自动调用的函数, session为卸载插件的会话。卸载在每一个会话关闭时均被自动运行一次。| 410 | 411 | + 功能修改:对session自动加载mud文件中变量失败时的异常进行管理,此时将不加载变量,自动继续进行 412 | + 功能修改:所有匹配类对象的匹配模式patterns支持动态修改,涉及Alias,Trigger,Command。修改方式为直接对其patterns属性赋值。如tri.patterns = aNewPattern 413 | + 功能修改:连接/断开连接时刻都会在提示中增加时刻信息,而不论是否异常。 414 | 415 | ### 0.16.2 (2023-12-19) 416 | + 功能修改:归一化#命令和非#命令处理,使session.exec_command、exec_command_async、exec_command_after均可以处理#命令,例如session.exec_command("#save")。同时,也可以在命令行使用#all发送#命令,如"#all #save"此类 417 | + 功能修改:调整脚本加载与变量自动加载的顺序。当前为连接自动加载时,首先加载变量,然后再加载脚本。目的是使脚本的变化可以覆盖加载的变量内容,而不是反向覆盖。 418 | + 功能修改:会话变量保存和加载可以配置是否打开,默认为打开。见Settings.client["var_autosave] 和 Settings.client["var_autoload"]。同理,该配置可以被本地pymud.cfg所覆盖 419 | + 功能修改:将MatchObject的同步onSuccess和异步await的执行顺序进行调整,以确保一定是同步onSuccess先执行。涉及Trigger、Command等。 420 | + 功能修改:修改了GMCPTrigger的onSuccess处置和await triggered处置参数,以保持与Trigger同步。当前,onSuccess函数传递3个参数,name,line(GMCP收到的原始str数据),wildcards(经eval处理的GMCP数据,大概率是dict,偶尔也可能eval失败,返回与line相同值)。await triggered返回与Triggerd的await triggered相同,均为BaseObject.State,包含4个参数的元组,result(永为True),name(GMCP的id),line(GMCP原始数据),wildcards(GMCP处理后数据)。其中,后3个参数与onSuccess函数调用时传递参数相同。 421 | + 功能修改:增加GMCP默认处理。当未使用GMCPTrigger对对应的GMCP消息进行处理时,默认使用[GMCP] name: value的形式输出GMCP收到的消息,以便于个人脚本调试。 422 | + 功能修改:修改GMCP数据的处理方式从eval修改为json.load,其余不变。 423 | 424 | ### 0.16.1.post2 (2023-12-12) 425 | + 问题修复:修复__init__.py中的__all__变量为字符串 426 | + 功能增加:可以加载自定义Settings。在执行python -m pymud时,会自动从当前目录读取pymud.cfg文件。使用json格式将配置信息写在该文件中即可。支持模块中settings.py里的sessions, client, server, styles, text字段内容。 427 | + 功能增加:增加全局变量集,可以使用session.setGlobal和session.getGlobal进行访问,以便于跨session通信。也可以使用#global在命令行访问 428 | + 功能增加:增加变量的持久化,持久化文件保存于当前目录,文件名为session名称.mud,该文件在会话初始化时自动读取,会话断开时自动保存,其他时候使用#save保存。 429 | + 功能增加:在extras.py中增加DotDict,用于支持字典的.访问方式 430 | + 功能增加:使用DotDict增加了session有关对象的点访问方式(.)的快捷访问,包括变量vars,全局变量globals,触发器tris,别名alis,命令cmds,定时器timers,gmcp。例如:session.vars.charname,相当于session.getVariable('charname') 431 | + 功能增加:增加#all命令,可以向当前所有活动会话发送同一消息,例如#all xixi,可以使所有连接的会话都发送emote 432 | + 功能增加:增加%copy系统变量,当复制后,会将复制内容赋值给%copy变量 433 | + 功能增加:增加Trigger测试功能,使用#test {msg}在命令行输入后,会如同接收到服务端数据一样引发触发反应,并且会使用[PYMUD TRIGGER TEST]进行信息显示。 434 | + 功能增加:匹配#test命令和%copy变量使用如下:窗体中复制有关行,然后命令行中输入#test %copy可使用复制的行来测试触发器 435 | + 功能修改:将原CodeBlock修改为CodeBlock和CodeLine组成,以适应新的#test命令 436 | + 功能修改:session对命令的输入异步处理函数handle_input_async进行微小调整,以适应#test命令使用 437 | + 功能修改:退出时未断开session时的提示窗口文字改为红色(原黄色对比度问题,看不清楚) 438 | + 功能修改:恢复了#help功能,可以在任意会话中使用#help列出所有帮助主题,#help {topic}可以查看主题详情 439 | + 功能修改:在#reload重新加载脚本时,保留变量数据 440 | + 问题修复:修复版本显示,更正为0.16.1(原0.16.0) 441 | + 问题修复:发布日期标志修改为当前时间 442 | + 功能修改:CodeLine的执行运行处理修改为不删除中间的多余空白 443 | + 问题修复:修改github项目地址为原pymud地址 444 | 445 | ### 0.15.8 (2023-12-05) 446 | 首次发布到pip。 -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | recommonmark 3 | sphinx-markdown-tables 4 | sphinx-rtd-theme 5 | piccolo_theme 6 | pymud -------------------------------------------------------------------------------- /docs/source/_static/chars_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crapex/pymud/96dc03a5e2ab0c3384e141dbd9ff82a68a91b0fc/docs/source/_static/chars_menu.png -------------------------------------------------------------------------------- /docs/source/_static/create_menu_win.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crapex/pymud/96dc03a5e2ab0c3384e141dbd9ff82a68a91b0fc/docs/source/_static/create_menu_win.png -------------------------------------------------------------------------------- /docs/source/_static/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crapex/pymud/96dc03a5e2ab0c3384e141dbd9ff82a68a91b0fc/docs/source/_static/icon.jpg -------------------------------------------------------------------------------- /docs/source/_static/init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crapex/pymud/96dc03a5e2ab0c3384e141dbd9ff82a68a91b0fc/docs/source/_static/init.png -------------------------------------------------------------------------------- /docs/source/_static/install_and_run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crapex/pymud/96dc03a5e2ab0c3384e141dbd9ff82a68a91b0fc/docs/source/_static/install_and_run.png -------------------------------------------------------------------------------- /docs/source/_static/main_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crapex/pymud/96dc03a5e2ab0c3384e141dbd9ff82a68a91b0fc/docs/source/_static/main_ui.png -------------------------------------------------------------------------------- /docs/source/_static/status_window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crapex/pymud/96dc03a5e2ab0c3384e141dbd9ff82a68a91b0fc/docs/source/_static/status_window.png -------------------------------------------------------------------------------- /docs/source/_static/ui_empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crapex/pymud/96dc03a5e2ab0c3384e141dbd9ff82a68a91b0fc/docs/source/_static/ui_empty.png -------------------------------------------------------------------------------- /docs/source/_static/ui_new_session_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crapex/pymud/96dc03a5e2ab0c3384e141dbd9ff82a68a91b0fc/docs/source/_static/ui_new_session_1.png -------------------------------------------------------------------------------- /docs/source/_static/ui_new_session_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crapex/pymud/96dc03a5e2ab0c3384e141dbd9ff82a68a91b0fc/docs/source/_static/ui_new_session_2.png -------------------------------------------------------------------------------- /docs/source/_static/ui_show_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crapex/pymud/96dc03a5e2ab0c3384e141dbd9ff82a68a91b0fc/docs/source/_static/ui_show_01.png -------------------------------------------------------------------------------- /docs/source/_static/ui_show_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crapex/pymud/96dc03a5e2ab0c3384e141dbd9ff82a68a91b0fc/docs/source/_static/ui_show_02.png -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | #import sphinx_rtd_theme, sphinx_book_theme, sphinxawesome_theme, sphinx_nefertiti, piccolo_theme 6 | import recommonmark, os, sys 7 | from recommonmark.parser import CommonMarkParser 8 | from recommonmark.transform import AutoStructify 9 | from importlib.metadata import version as get_version 10 | 11 | HERE = os.path.dirname(__file__) 12 | sys.path.insert(0, os.path.abspath(r'../../src')) 13 | 14 | source_parsers = { 15 | '.md': CommonMarkParser, 16 | } 17 | source_suffix = ['.rst', '.md'] 18 | 19 | # -- Project information ----------------------------------------------------- 20 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 21 | 22 | project = 'PyMUD 帮助文档' 23 | copyright = '2023-2025, crapex@crapex.cc' 24 | author = 'crapex' 25 | release = get_version('pymud') 26 | 27 | # -- General configuration --------------------------------------------------- 28 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 29 | 30 | extensions = ['recommonmark', 'sphinx_markdown_tables', 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] 31 | 32 | templates_path = ['_templates'] 33 | exclude_patterns = [] 34 | 35 | language = 'zh_CN' 36 | 37 | # -- Options for HTML output ------------------------------------------------- 38 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 39 | # sphinx_bootstrap_theme sphinx_rtd_theme sphinx_nefertiti 40 | 41 | html_theme = 'piccolo_theme' 42 | html_static_path = ['css'] 43 | html_css_files = ['custom.css'] 44 | -------------------------------------------------------------------------------- /docs/source/examples.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pymud import Session, Command, Trigger, GMCPTrigger, IConfig 3 | 4 | # 房间名匹配正则表达式 5 | REGX_ROOMNAME = r'^[>]*(?:\s)?(\S.+)\s-\s*(?:杀戮场)?(?:\[(\S+)\]\s*)*(?:㊣\s*)?[★|☆|∞|\s]*$' 6 | 7 | # 移动命令中的各种方位清单 8 | DIRECTIONS = ( 9 | "n","s","w","e","ne","nw","se","sw", 10 | "u","d","nu","su","wu","eu","nd","sd","wd","ed", 11 | "north", "south", "west", "east", "northeast", "northwest", "southeast", "southwest", 12 | "up", "down","northup","southup","westup","eastup","northdown","southdown","westdown","eastdown", 13 | "enter(\s\S+)?", "out", "zuan(\s\S+)?", "\d", "leave(\s\S+)?", "jump\s(jiang|out)", "climb(\s(ya|yafeng|up|west|wall|mount))?", 14 | "sheshui", "tang", "act zuan to mao wu", "wander", "xiaolu", "cai\s(qinyun|tingxiang|yanziwu)", "row mantuo", "leave\s(\S+)" 15 | ) 16 | 17 | # 移动失败(无法移动)的描述正则匹配清单 18 | MOVE_FAIL = ( 19 | r'^[> ]*哎哟,你一头撞在墙上,才发现这个方向没有出路。$', 20 | r'^[> ]*这个方向没有出路。$', 21 | r'^[> ]*守军拦住了你的去路,大声喝到:干什么的?要想通过先问问我们守将大人!$', 22 | ) 23 | 24 | # 本次移动失败(但可以重新再走的)的描述正则匹配清单 25 | MOVE_RETRY = ( 26 | r'^[> ]*你正忙着呢。$', 27 | r'^[> ]*你的动作还没有完成,不能移动。$', 28 | r'^[> ]*你还在山中跋涉,一时半会恐怕走不出这(六盘山|藏边群山|滇北群山|西南地绵绵群山)!$', 29 | r'^[> ]*你一脚深一脚浅地沿着(\S+)向着(\S+)方走去,虽然不快,但离目标越来越近了。', 30 | r'^[> ]*你一脚深一脚浅地沿着(\S+)向着(\S+)方走去,跌跌撞撞,几乎在原地打转。', 31 | r'^[> ]*你小心翼翼往前挪动,遇到艰险难行处,只好放慢脚步。$', 32 | r'^[> ]*山路难行,你不小心给拌了一跤。$', 33 | r'^[> ]*你忽然不辨方向,不知道该往哪里走了。', 34 | r'^[> ]*走路太快,你没在意脚下,被.+绊了一下。$', 35 | r'^[> ]*你不小心被什么东西绊了一下,差点摔个大跟头。$', 36 | r'^[> ]*青海湖畔美不胜收,你不由停下脚步,欣赏起了风景。$', 37 | r'^[> ]*(荒路|沙石地|沙漠中)几乎没有路了,你走不了那么快。$', 38 | r'^[> ]*你小心翼翼往前挪动,生怕一不在意就跌落山下。$', 39 | ) 40 | 41 | class CmdMove(Command, IConfig): 42 | MAX_RETRY = 3 43 | 44 | def __init__(self, session: Session, *args, **kwargs): 45 | # 将所有可能的行走命令组合成匹配模式 46 | pattern = "^({0})$".format("|".join(DIRECTIONS)) 47 | super().__init__(session, pattern, *args, **kwargs) 48 | self.session = Session 49 | self.timeout = 10 50 | self._executed_cmd = "" 51 | 52 | self._objs = list() 53 | 54 | # 此处使用的GMCPTrigger和Trigger全部使用异步模式,因此均无需指定onSuccess 55 | self._objs.append(GMCPTrigger(self.session, "GMCP.Move")) 56 | self._objs.append(Trigger(self.session, REGX_ROOMNAME, id = "tri_move_succ", group = "cmdmove", keepEval = True, enabled = False)) 57 | 58 | idx = 1 59 | for s in MOVE_FAIL: 60 | self._objs.append(Trigger(self.session, patterns = s, id = f"tri_move_fail{idx}", group = "cmdmove", enabled = False)) 61 | idx += 1 62 | 63 | idx = 1 64 | for s in MOVE_RETRY: 65 | self._objs.append(Trigger(self.session, patterns = s, id = f"tri_move_retry{idx}", group = "cmdmove", enabled = False)) 66 | idx += 1 67 | 68 | self.session.addObjects(self._objs) 69 | 70 | def unload(self): 71 | self.session.delObjects(self._objs) 72 | 73 | async def execute(self, cmd, *args, **kwargs): 74 | self.reset() 75 | 76 | retry_times = 0 77 | self.session.enableGroup("cmdMove") 78 | 79 | while True: 80 | 81 | tasklist = list() 82 | for tr in self._objs: 83 | tasklist.append(self.create_task(tr.triggered())) 84 | 85 | done, pending = await self.session.waitfor(cmd, asyncio.wait(tasklist, timeout = self.timeout, return_when = "FIRST_COMPLETED")) 86 | 87 | for t in list(pending): 88 | self.remove_task(t) 89 | 90 | result = self.NOTSET 91 | tasks_done = list(done) 92 | if len(tasks_done) > 0: 93 | task = tasks_done[0] 94 | 95 | # 所有触发器在 onSuccess 时需要的参数,在此处都可以通过 task.result() 获取 96 | # result返回值与 await tri.triggered() 返回值完全相同 97 | # 这种返回值比onSuccess中多一个state参数,该参数在触发器中必定为 self.SUCCESS 值 98 | state, id, line, wildcards = task.result() 99 | 100 | # success 101 | if id == 'GMCP.Move': 102 | # GMCP.Move: [{"result":"true","dir":["west"],"short":"林间小屋"}] 103 | move_info = wildcards[0] 104 | if move_info["result"] == "true": 105 | roomname = move_info["short"] 106 | self.session.setVariable("roomname", roomname) 107 | result = self.SUCCESS 108 | elif move_info["result"] == "false": 109 | result = self.FAILURE 110 | 111 | break 112 | 113 | elif id == 'tri_move_succ': 114 | roomname = wildcards[0] 115 | self.session.setVariable("roomname", roomname) 116 | result = self.SUCCESS 117 | break 118 | 119 | elif id.startswith('tri_move_fail'): 120 | self.error(f'执行{cmd},移动失败,错误信息为{line}', '移动') 121 | result = self.FAILURE 122 | break 123 | 124 | elif id.startswith('tri_move_retry'): 125 | retry_times += 1 126 | if retry_times > self.MAX_RETRY: 127 | result = self.FAILURE 128 | break 129 | 130 | await asyncio.sleep(2) 131 | 132 | else: 133 | self.warning(f'执行{cmd}超时{self.timeout}秒', '移动') 134 | result = self.TIMEOUT 135 | break 136 | 137 | self.session.enableGroup("cmdMove", False) 138 | return result 139 | 140 | import re, traceback, math 141 | 142 | import traceback, re, asyncio, math 143 | from pymud import Command, Trigger, IConfig 144 | 145 | 146 | class CmdDazuoto(Command, IConfig): 147 | "持续打坐或打坐到max" 148 | def __init__(self, session, *args, **kwargs): 149 | super().__init__(session, r"^(dzt)(?:\s+(\S+))?$", *args, **kwargs) 150 | 151 | self._triggers = {} 152 | 153 | self._initTriggers() 154 | 155 | self._force_level = 0 156 | self._dazuo_point = 10 157 | 158 | self._halted = False 159 | 160 | def _initTriggers(self): 161 | self._triggers["done"] = self.tri_dz_done = Trigger(self.session, r'^[> ]*你运功完毕,深深吸了口气,站了起来。', id = "tri_dz_done", keepEval = True, group = "dazuoto") 162 | self._triggers["noqi"] = self.tri_dz_noqi = Trigger(self.session, r'^[> ]*你现在的气太少了,无法产生内息运行全身经脉。|^[> ]*你现在气血严重不足,无法满足打坐最小要求。|^[> ]*你现在的气太少了,无法产生内息运行小周天。', id = "tri_dz_noqi", group = "dazuoto") 163 | self._triggers["nojing"] = self.tri_dz_nojing = Trigger(self.session, r'^[> ]*你现在精不够,无法控制内息的流动!', id = "tri_dz_nojing", group = "dazuoto") 164 | self._triggers["wait"] = self.tri_dz_wait = Trigger(self.session, r'^[> ]*你正在运行内功加速全身气血恢复,无法静下心来搬运真气。', id = "tri_dz_wait", group = "dazuoto") 165 | self._triggers["halt"] = self.tri_dz_halt = Trigger(self.session, r'^[> ]*你把正在运行的真气强行压回丹田,站了起来。', id = "tri_dz_halt", group = "dazuoto") 166 | self._triggers["finish"] = self.tri_dz_finish = Trigger(self.session, r'^[> ]*你现在内力接近圆满状态。', id = "tri_dz_finish", group = "dazuoto") 167 | self._triggers["dz"] = self.tri_dz_dz = Trigger(self.session, r'^[> ]*你将运转于全身经脉间的内息收回丹田,深深吸了口气,站了起来。|^[> ]*你的内力增加了!!', id = "tri_dz_dz", group = "dazuoto") 168 | 169 | def __unload__(self): 170 | self.session.delObjects(self._triggers) 171 | 172 | def stop(self): 173 | self.tri_dz_done.enabled = False 174 | self._halted = True 175 | self._always = False 176 | 177 | async def dazuo_to(self, to): 178 | # 开始打坐 179 | dazuo_times = 0 180 | self.tri_dz_done.enabled = True 181 | if not self._force_level: 182 | await self.session.exec_async("enable") 183 | force_info = self.session.getVariable("eff-force", ("none", 0)) 184 | self._force_level = force_info[1] 185 | 186 | self._dazuo_point = (self._force_level - 5) // 10 187 | if self._dazuo_point < 10: self._dazuo_point = 10 188 | 189 | if self.session.getVariable("status_type", "hpbrief") == "hpbrief": 190 | self.session.writeline("tune gmcp status on") 191 | 192 | neili = int(self.session.getVariable("neili", 0)) 193 | maxneili = int(self.session.getVariable("max_neili", 0)) 194 | force_info = self.session.getVariable("eff-force", ("none", 0)) 195 | self._force_level = force_info[1] 196 | 197 | TIMEOUT_DEFAULT = 10 198 | TIMEOUT_MAX = 360 199 | 200 | timeout = TIMEOUT_DEFAULT 201 | 202 | if to == "dz": 203 | cmd_dazuo = "dz" 204 | timeout = TIMEOUT_MAX 205 | self.tri_dz_dz.enabled = True 206 | self.info('即将开始进行dz,以实现小周天循环', '打坐') 207 | 208 | elif to == "max": 209 | cmd_dazuo = "dazuo max" 210 | timeout = TIMEOUT_MAX 211 | need = math.floor(1.90 * maxneili) 212 | self.info('当前内力:{},需打坐到:{},还需{}, 打坐命令{}'.format(neili, need, need - neili, cmd_dazuo), '打坐') 213 | 214 | elif to == "once": 215 | cmd_dazuo = "dazuo max" 216 | timeout = TIMEOUT_MAX 217 | self.info('将打坐1次 {dazuo max}.', '打坐') 218 | 219 | else: 220 | cmd_dazuo = f"dazuo {self._dazuo_point}" 221 | self.info('开始持续打坐, 打坐命令 {}'.format(cmd_dazuo), '打坐') 222 | 223 | while (to == "dz") or (to == "always") or (neili / maxneili < 1.90): 224 | if self._halted: 225 | self.info("打坐任务已被手动中止。", '打坐') 226 | break 227 | 228 | awts = [self.create_task(self._triggers[key].triggered()) for key in ["done", "noqi", "nojing", "wait", "halt"]] 229 | 230 | if to != "dz": 231 | awts.append(self.create_task(self._triggers["finish"].triggered())) 232 | else: 233 | awts.append(self.create_task(self._triggers["dz"].triggered())) 234 | 235 | done, pending = await self.session.waitfor(cmd_dazuo, asyncio.wait(awts, timeout = timeout, return_when = "FIRST_COMPLETED")) 236 | 237 | tasks_pending = list(pending) 238 | for t in tasks_pending: 239 | self.remove_task(t) 240 | 241 | tasks_done = list(done) 242 | if len(tasks_done) == 0: 243 | self.info('打坐中发生了超时问题,将会继续重新来过', '打坐') 244 | 245 | elif len(tasks_done) == 1: 246 | task = tasks_done[0] 247 | _, name, _, _ = task.result() 248 | 249 | if name in (self.tri_dz_done.id, self.tri_dz_dz.id): 250 | if (to == "always"): 251 | dazuo_times += 1 252 | if dazuo_times > 100: 253 | # 此处,每打坐200次,补满水食物 254 | self.info('该吃东西了', '打坐') 255 | await self.session.exec_async("feed") 256 | dazuo_times = 0 257 | 258 | 259 | elif (to == "dz"): 260 | dazuo_times += 1 261 | if dazuo_times > 10: 262 | # 此处,每打坐10次,补满水食物 (剑心居 feed 同样句子无反馈信息) 263 | self.info('该吃东西了', '打坐') 264 | await self.session.exec_async("feed") 265 | dazuo_times = 0 266 | 267 | elif (to == "max"): 268 | if self.session.getVariable("status_type", "hpbrief") == "hpbrief": 269 | self.session.writeline("tune gmcp status on") 270 | 271 | neili = int(self.session.getVariable("neili", 0)) 272 | 273 | if self._force_level >= 161: 274 | self.session.writeline("exert recover") 275 | await asyncio.sleep(0.2) 276 | 277 | elif (to == "once"): 278 | self.info('打坐1次任务已成功完成.', '打坐') 279 | break 280 | 281 | elif name == self.tri_dz_noqi.id: 282 | if self._force_level >= 161: 283 | await asyncio.sleep(0.1) 284 | self.session.writeline("exert recover") 285 | await asyncio.sleep(0.1) 286 | else: 287 | await asyncio.sleep(15) 288 | 289 | elif name == self.tri_dz_nojing.id: 290 | await asyncio.sleep(1) 291 | self.session.writeline("exert regenerate") 292 | await asyncio.sleep(1) 293 | 294 | elif name == self.tri_dz_wait.id: 295 | await asyncio.sleep(5) 296 | 297 | elif name == self.tri_dz_halt.id: 298 | self.info("打坐已被手动halt中止。", '打坐') 299 | break 300 | 301 | elif name == self.tri_dz_finish.id: 302 | self.info("内力已最大,将停止打坐。", '打坐') 303 | break 304 | 305 | else: 306 | ids = [] 307 | for task in tasks_done: 308 | _, name, _, _ = task.result() 309 | ids.append(name) 310 | 311 | self.info(f"命令执行中发生错误,应完成1个任务,实际完成{len(tasks_done)}个,任务ID分别为{ids}, 将等待5秒后继续", '打坐') 312 | 313 | await asyncio.sleep(5) 314 | 315 | self.info('已成功完成', '打坐') 316 | self.tri_dz_done.enabled = False 317 | self.tri_dz_dz.enabled = False 318 | self._onSuccess() 319 | return self.SUCCESS 320 | 321 | async def execute(self, cmd, *args, **kwargs): 322 | try: 323 | self.reset() 324 | if cmd: 325 | m = re.match(self.patterns, cmd) 326 | if m: 327 | cmd_type = m[1] 328 | param = m[2] 329 | self._halted = False 330 | 331 | if param == "stop": 332 | self._halted = True 333 | self.info('已被人工终止,即将在本次打坐完成后结束。', '打坐') 334 | #self._onSuccess() 335 | return self.SUCCESS 336 | 337 | elif param in ("dz",): 338 | return await self.dazuo_to("dz") 339 | 340 | elif param in ("0", "always"): 341 | return await self.dazuo_to("always") 342 | 343 | elif param in ("1", "once"): 344 | return await self.dazuo_to("once") 345 | 346 | elif not param or param == "max": 347 | return await self.dazuo_to("max") 348 | 349 | 350 | except Exception as e: 351 | self.error(f"异步执行中遇到异常, {e}, 类型为 {type(e)}") 352 | self.error(f"异常追踪为: {traceback.format_exc()}") 353 | -------------------------------------------------------------------------------- /docs/source/hotkeys.rst: -------------------------------------------------------------------------------- 1 | 5 快捷键 2 | =============== 3 | 4 | 5.1 系统快捷键 5 | --------------- 6 | 7 | 系统快捷键是PyMUD固话在程序中的快捷键设置,这些快捷键不能通过配置文件自定义覆盖。 8 | 9 | ``PageUp`` 10 | ^^^^^^^^^^^^^^^ 11 | 12 | 上翻页功能。将当前光标向上滚动一定行数。该行数由窗口尺寸所确定。当光标位于最末尾的半屏行数之前时,屏幕会自动分屏。 13 | 14 | ``PageDown`` 15 | ^^^^^^^^^^^^^^^ 16 | 17 | 下翻页功能。将当前光标向下滚动一定行数。该行数由窗口尺寸所确定。当光标位于最末尾的半屏行数之后时,屏幕自动取消分屏。 18 | 19 | ``Control + Z`` 20 | ^^^^^^^^^^^^^^^ 21 | 22 | 取消分屏功能。不论当前光标位于何处, ``Ctrl+Z`` 均可以直接去掉分屏,使光标回到最末尾处。 23 | 24 | ``Control + C`` 25 | ^^^^^^^^^^^^^^^ 26 | 27 | 纯文本复制功能。以纯文本形式复制选定区域。当选中区域多行时,复制选中行全部内容。 28 | 29 | *注意 : MacOS系统中,复制也是 Control + C ,而不是系统复制快捷键 Command + C。* 30 | 31 | ``Control + R`` 32 | ^^^^^^^^^^^^^^^ 33 | 34 | 原始文本复制功能。以包含ANSI字符代码的形式进行复制。当选中区域仅在一行时,复制该行整行。当选中区域多行时,复制选中行全部内容。 35 | 36 | ``Control + V / Command + V`` 37 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 38 | 39 | 粘贴功能。 40 | 41 | *注意: 粘贴快捷键是系统快捷键,列在此处是说明Windows和MacOS不同系统下要使用不同的快捷键。* 42 | 43 | 右箭头 ``→`` 44 | ^^^^^^^^^^^^^^^ 45 | 46 | 命令行快速命令补完。所有历史命令会作为快速补完的数据源。输入部分内容后,可使用右箭头快速补完。 47 | 48 | 上箭头 ``↑`` 与 下箭头 ``↓`` 49 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 50 | 51 | 命令行历史命令切换检索。 52 | 53 | ``Control + ←`` 和 ``Control + →``, ``Shift + ←`` 和 ``Shift + →`` 54 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 55 | 56 | 多会话下的当前会话快速切换。 ``Ctrl + ←`` 或 ``Shift + ←`` 向前切换, ``Ctrl + →`` 或 ``Shift + →`` 向后切换。 57 | 增加Shift处置的原因是,在macOS下,全屏终端下, ``Ctrl + ←`` 和 ``Ctrl + →`` 为系统快捷键切换全屏应用,无法用于切换会话。 58 | 59 | ``F1`` 60 | ^^^^^^^^^^^^^^^ 61 | 62 | 帮助命令。将浏览器导航到 `PyMUD的帮助文档站点`_ 63 | 64 | ``F2`` 65 | ^^^^^^^^^^^^^^^ 66 | 67 | 鼠标使能/禁用切换命令。在ssh远程连接下,复制命令无法复制到本地剪贴板,此时可以禁用鼠标后使用鼠标调用复制功能,可以复制到本地剪贴板中。 68 | 当鼠标被禁用时,状态栏右下角会显示“鼠标已禁用”状态。 69 | 70 | 71 | 5.2 自定义快捷键 (0.19.1新增) 72 | ----------------------------------- 73 | 74 | 自定义快捷键是通过 ``Settings.py`` 和本地 ``pymud.cfg`` 所设置的快捷键,该快捷键的 ``Settings.py`` 默认选项可以被本地设置所覆盖。 75 | 76 | 设置快捷键时,key为 `prompt toolkit`_ 所支持的快捷键字符串,value为通过session.exec_command运行所支持的任意命令。 77 | 78 | 以下为未覆盖配置时的默认自定义快捷键功能 79 | 80 | ``F3`` 81 | ^^^^^^^^^^^^^^^ 82 | 83 | 相当于命令 `#ignore`_ 84 | 85 | ``F4`` 86 | ^^^^^^^^^^^^^^^ 87 | 88 | 相当于命令 `#clear`_ 89 | 90 | ``F11`` 91 | ^^^^^^^^^^^^^^^ 92 | 93 | 相当于命令 `#close`_ 94 | 95 | *注意: Windows Terminal下,F11键是切换全屏/窗口状态,因此PyMUD快捷键不生效。* 96 | 97 | ``F12`` 98 | ^^^^^^^^^^^^^^^ 99 | 100 | 相当于命令 `#exit`_ 101 | 102 | 103 | .. _#ignore: syscommand.html#ignore 104 | .. _#clear: syscommand.html#clear 105 | .. _#close: syscommand.html#close 106 | .. _#exit: syscommand.html#exit 107 | .. _prompt toolkit: https://python-prompt-toolkit.readthedocs.io/en/master/pages/advanced_topics/key_bindings.html 108 | .. _PyMUD的帮助文档站点: https://pymud.readthedocs.io 109 | 110 | 111 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. pymud-cookbook documentation master file, created by 2 | sphinx-quickstart on Sun Feb 4 09:39:35 2024. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | PyMUD 帮助文档 7 | ========================================== 8 | 9 | 10 | 有关链接 11 | ^^^^^^^^^ 12 | 13 | - QQ交流群: `554672580 `_ 14 | - GitHub地址: https://github.com/crapex/pymud 15 | - PyPi地址: https://pypi.org/project/pymud 16 | - 北侠wiki地址: https://www.pkuxkx.net/wiki/tools/pymud 17 | - 北侠地址: https://www.pkuxkx.net/ 18 | - deepwiki自动生成的项目理解文档地址: https://deepwiki.com/crapex/pymud 19 | - PyMUD用户shanghua写的入门教程文档: https://www.pkuxkx.net/forum/forum.php?mod=viewthread&tid=49999&forumuid=12067 20 | 21 | 写在最前面的话 22 | ^^^^^^^^^^^^^^^ 23 | 24 | 最早想要自己写MUD客户端的念头,还是在几年前。但前几年事情太多,人太忙,我记得自20年疫情之后,到今年年初就没有再登陆过北侠了。 25 | 23年春节之后空闲一些,于2023年2月19日重启MUD客户端的计划,2023年5月29日形成第一个发布版(0.05b),2023年12月5日发布首个支持 26 | pip安装的package版本(0.15),目前发布pip安装的最新版为0.20.4。 27 | 28 | 在自己写客户端之前,我主要用过zmud和mushclient两个客户端,北大侠客行一直是用mushclient(玩的那会儿还没有mudlet)。 29 | 我认为mushclient是一个功能非常强大的客户端,唯一缺点是不支持跨平台。由于工作原因,上班的地方不能上网,手机玩的话, 30 | 确实没有特别适合的跨平台客户端(tintint--倒是支持,但一直不想重学然后重写我在mushclient里的所有python脚本), 31 | 加上我是一个程序爱好者,所以决定自己干起,正好在游戏之中学习了。 32 | 33 | 因为我要综合平衡工作、生活、写代码、当然还有自己玩,所以整个更新节奏不会很快,但我认为我会一直更新下去的。 34 | 感谢北大侠客行巫师团队的努力,北侠吸引我玩的动力,也是我不断更新完善客户端的动力! 35 | 36 | 37 | 特点 38 | ^^^^^^^^^ 39 | 40 | - 原生纯Python开发,除 `prompt-toolkit `_ 及其依赖库(wcwidth, pygments, pyperclip)外,不需要其他第三方库支持 41 | - 使用原生asyncio库进行网络和事务处理,支持async/await语法的原生异步操作(PyMUD最大特色)。原生异步意味着可以支持很多其他异步库,例如可以使用aiohttp来进行网络页面访问而不产生阻塞等等:) 42 | - 基于控制台的全屏UI界面设计(无需图形环境,如linux下的X环境)。同时支持鼠标操作(可触摸设备上支持触摸屏操作) 43 | - 支持分屏显示,在数据快速滚动的时候,上半屏保持不动,以确保不错过信息 44 | - 解决了99%情况下,北大侠客行中文对不齐,也就是看不清字符画的问题(因为我没有走遍所有地方,不敢保证100%) 45 | - 真正的支持多session会话,支持命令和鼠标切换会话 46 | - 原生支持多种服务器端编码方式,不论是GBK、BIG5、还是UTF-8 47 | - 支持NWAS、MTTS协商,支持GMCP、MSDP、MSSP协议。暂不支持MXP 48 | - 一次脚本开发,多平台运行。Windows, Linux, MacOS, Android(基于termux), iOS(基于iSH)。可部署在docker和云端,只要能在该平台上运行Python,就可以运行PyMUD客户端 49 | - 脚本所有语法均采用Python原生语法,因此你只要会用Python,就可以自己写脚本,免去了再去学习lua、熟悉各类APP的使用的难处 50 | - 全开源代码,因此脚本也可以很方便的使用visual studio code等工具进行调试,可以设置断点、查看变量等 51 | - Python拥有极为强大的文字处理能力,用于处理文本的MUD最为合适 52 | - Python拥有极为丰富的第三方库,能支持的第三方库,就能在PyMUD中支持 53 | - 我自己还在玩,所以本客户端会持续进行更新:) 54 | 55 | **美化对齐的字符画** 56 | 57 | .. image:: _static/ui_show_01.png 58 | :alt: 美化对齐的字符画 59 | 60 | **滚动时自动分屏** 61 | 62 | .. image:: _static/ui_show_02.png 63 | :alt: 滚动时自动分屏 64 | 65 | .. toctree:: 66 | :maxdepth: 3 67 | :caption: 目录 68 | 69 | installation 70 | ui 71 | settings 72 | syscommand 73 | hotkeys 74 | scripts 75 | plugins 76 | references 77 | updatehistory 78 | 79 | 80 | 索引与表 81 | ================== 82 | 83 | * :ref:`genindex` 84 | * :ref:`modindex` 85 | * :ref:`search` -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | 1 需求、安装与运行 2 | ====================== 3 | 4 | 1.1 环境需求 5 | ---------------------- 6 | 7 | PyMUD是一个原生基于Python语言的MUD客户端,因此最基本的环境是Python环境而非操作系统环境。 8 | 理论上,只要你的操作系统下可以运行Python,就可以运行PyMUD。 9 | 另外,本客户端的UI设计是基于控制台的,因此也不需要有图形环境的支持,可以方便的部署在云端和docker中。 10 | 11 | - 操作系统需求:不限,能运行Python是必要条件。可以windows(推荐使用 `Windows Terminal`_ 作为终端)、linux(不需要X支持)、macOS(推荐使用 iTerm2 终端)、Android(使用termux)、iOS(使用iSH)。 12 | - 版本需求:要求 >=3.7(0.21.0已测试3.8可正常运行,3.7版本机无法安装因此不确定能否使用,请自行尝试),32位/64位随意,建议用64位版,可以支持4G以上的内存访问。 13 | - 支持库需求:prompt-toolkit 3.0( `prompt toolkit 3 source`_ ), 以及由 ``prompt-toolkit`` 所依赖的 ``wcwidth、pygment、pyperclip`` 。 14 | - prompt-toolkit 帮助页面: `prompt toolkit 3 help`_ 15 | 16 | 1.2 安装 17 | ---------------------- 18 | 19 | - 3.11开始,Python官方推荐使用venv来管理Python环境,建议使用uv工具 `https://docs.astral.sh/uv/`_ 作为包及虚拟环境管理工具。 20 | - 安装Python、pip。uv工具可以一并搞定(linux下pip是一个单独的包,debian/ubuntu可以使用 ``apt-get`` 分别安装)。 21 | - 使用pip安装(或更新)PyMUD程序本体:可以直接使用pip安装或更新。所需的支持库会自动安装。 22 | - 在Python 3.12 版本下, 23 | 24 | .. code:: bash 25 | 26 | pip install pymud # 安装 27 | pip install --upgrade pymud # 更新 28 | pip install --upgrade pymud==0.21.0 # 指定版本 29 | pip install --upgrade pymud==0.21.0a1 -i https://pypi.org/simple # 指定pypi官方源。由于镜像同步需要时间,所以有时候刚发布更新时,需指定到pypi官方源 30 | 31 | # 或者使用uv工具 32 | uv init # 初始化项目 33 | uv add pymud # 添加pymud依赖 34 | uv add pymud==0.21.0a4 # 添加指定版本pymud依赖 35 | 36 | 37 | 1.3 初始化环境 38 | ---------------------- 39 | 40 | PyMUD 支持通过命令行参数进行启动配置。可以通过 ``pymud -h`` (直接安装时) 或 ``uv run pymud -h`` (使用uv作为包管理工具时) 查看有关帮助。 41 | 42 | 安装后,可以在命令行任意目录下使用 ``pymud init`` (直接安装时) 或 ``uv run pymud init`` 初始化默认环境。 43 | 44 | 根据该初始化指引,会创建一个脚本目录,在该目录下生成包含主要配置的 ``pymud.cfg`` 配置文件,以及一个示例的 ``examples.py`` 脚本文件。 45 | 46 | 初始化示例见下图: 47 | 48 | .. image:: _static/init.png 49 | 50 | 51 | 1.4 运行 52 | ---------------------- 53 | 54 | PyMUD 通过在当前目录下直接键入命令 ``pymud`` (直接安装时) 或使用uv工具的命令 ``uv run pymud`` 执行。 55 | 56 | PyMUD 支持命令行参数配置启动行为。具体参数及含义可以通过增加 -h 或者 --help 查看。列出如下: 57 | 58 | .. code:: 59 | 60 | PS C:\> pymud -h 61 | usage: pymud [-h] [-d] [-l logfile] [-a] [-s startup_dir] {init} ... 62 | 63 | PyMUD命令行参数帮助 64 | 65 | positional arguments: 66 | {init} init用于初始化运行环境 67 | 68 | options: 69 | -h, --help show this help message and exit 70 | -d, --debug 指定以调试模式进入PyMUD。此时,系统log等级将设置为logging.NOTSET, 所有log数据均会被记录。默认不启用。 71 | -l logfile, --logfile logfile 72 | 指定调试模式下记录文件名,不指定时,默认为当前目录下的pymud.log 73 | -a, --appendmode 指定log文件的访问模式是否为append尾部添加模式,默认为True。当为False时,使用w模式,即每次运行清空之前记录 74 | -s startup_dir, --startup_dir startup_dir 75 | 指定启动目录,默认为当前目录。使用该参数可以在任何目录下,通过指定脚本目录来启动 76 | 77 | PS C:\> pymud init -h 78 | usage: usage: pymud init [-h] [-d dir] 79 | 80 | 初始化pymud运行环境, 包括建立脚本目录, 创建默认配置文件, 创建样例脚本等. 81 | 82 | options: 83 | -h, --help show this help message and exit 84 | -d dir, --dir dir 指定构建脚本目录的名称, 不指定时会根据操作系统选择不同默认值 85 | 86 | 87 | .. code:: 88 | 89 | # 示例 从脚本目录的当前目录启动 PyMUD 90 | PS C:\> cd ~\pkuxkx # 进入自己的脚本目录(可由 pymud init 创建) 91 | PS C:\Users\home\pkuxkx> pymud # 直接使用pymud命令运行PyMUD. 也可以使用 python -m pymud 命令,效果相同 92 | PS C:\Users\home\pkuxkx> uv run pymud # 使用uv命令在管理的虚拟环境下运行PyMUD 93 | 94 | # 示例: 从任意位置通过指定脚本目录启动 PyMUD 95 | PS C:\> pymud -s ~\pkuxkx 96 | 97 | # 示例: 从任意位置通过指定脚本目录启动 PyMUD, 并打开调试模式 98 | PS C:\> pymud -d -s ~\pkuxkx 99 | 100 | 1.5 Windows下安装与启动示例 101 | -------------------------------------------- 102 | 103 | - 建议使用 `Windows Terminal`_ 作为shell,并使用 `PowerShell 7`_ 作为启动终端 104 | - 使用uv初始化项目: ``uv init`` 105 | - 添加pymud依赖: ``uv add pymud`` 106 | - 通过init创建自己的脚本目录: ``uv run pymud init`` 107 | - 在脚本目录下启动运行pymud: ``uv run pymud`` 108 | 109 | 启动后的界面 110 | """"""""""""""""""""""""""""""""""""" 111 | 112 | .. image:: _static/ui_empty.png 113 | 114 | 在 `Windows Terminal`_ 中增加快捷菜单 115 | """"""""""""""""""""""""""""""""""""" 116 | 117 | - 创建一个配置文件(比如从 `PowerShell 7`_ 复制一个) 118 | - 将名称改为你喜欢的名称,如 ``PyMUD`` 119 | - 将命令行改为: ``pymud`` 或 ``python -m pymud`` 120 | - 将启动目录改为你的脚本目录,比如 d:\\pkuxkx 121 | - 可以自己设置一个喜欢的图标:) 122 | 123 | .. image:: _static/create_menu_win.png 124 | 125 | 126 | .. _Windows Terminal: https://aka.ms/terminal 127 | .. _PowerShell 7: https://aka.ms/powershell-release?tag=stable 128 | .. _prompt toolkit 3 source : https://github.com/prompt-toolkit/python-prompt-toolkit 129 | .. _prompt toolkit 3 help : https://python-prompt-toolkit.readthedocs.io -------------------------------------------------------------------------------- /docs/source/plugins.rst: -------------------------------------------------------------------------------- 1 | 7 插件 2 | =============== 3 | 4 | 插件是为了方便大家共享,将一个或多个通用功能到模块按照一定的规范所编写的特殊脚本。 5 | 插件需要放在当前目录下的 plugins 目录中,PyMUD在启动时会自动从该文件夹下搜索符合插件规范的文件并加载为插件。 6 | 7 | 按照插件规范,插件文件中必须要存在以下几个部分: 8 | 9 | - 插件名称, 在文件中以 str 类型赋值给 PLUGIN_NAME 变量,如: PLUGIN_NAME = "your-plugin-name" 10 | - 插件有关描述,在文件中以 dict 类型赋值给 PLUGIN_DESC 变量,字典内应包含 "VERSION" (版本), "AUTHOR" (作者), "RELEASE_DATE" (发布日期), "DESCRIPTION" (插件描述)四个字段。如: 11 | - 一个在应用启动时读取到本插件是调用的函数,应为 `def PLUGIN_PYMUD_START(app: PyMudApp):` 这种形式,其中 app 为传递的应用程序实例 12 | - 一个在每一个会话创建时调用的函数,应为 `def PLUGIN_SESSION_CREATE(session: Session):` 这种形式,其中 session 为传递的创建的会话实例 13 | - 一个在某一个会话被销毁(关闭)时调用的函数,应为 `def PLUGIN_SESSION_DESTROY(session: Session):` 这种形式,其中 session 为传递的销毁的会话实例 14 | - 一个在应用程序关闭时调用的函数,应为 `def PLUGIN_PYMUD_DESTROY(app: PyMudApp):` 这种形式,其中 app 为传递的应用程序实例 15 | 16 | 下面给出一个我使用的用于与群晖Synology Chat进行双向通信的插件,供参考插件制作: 17 | 18 | .. code:: Python 19 | 20 | from pymud import Session, Alias, Command 21 | from functools import partial 22 | import re, json, asyncio, urllib.parse, traceback, time, platform 23 | from datetime import datetime 24 | from aiohttp import web, ClientSession 25 | from aiohttp.web_request import Request 26 | 27 | # 插件唯一名称 28 | PLUGIN_NAME = "chathook" 29 | 30 | # 插件有关描述信息 31 | PLUGIN_DESC = { 32 | "VERSION" : "1.0.1", 33 | "AUTHOR" : "newstart", 34 | "RELEASE_DATE" : "2024-02-14", 35 | "DESCRIPTION" : "使用群晖Synology Chat的webhook插件,可以用于与游戏进行交互" 36 | } 37 | 38 | WEBHOOK_URL = "https://Please.Change.The.URL.To.Your.Own.Address" 39 | 40 | class ChatHook: 41 | HOOK_COMMANDS = { 42 | "get": "hookget", 43 | } 44 | 45 | def __init__(self, app) -> None: 46 | self.app = app 47 | self.app.set_globals("hooked", False) 48 | app.globals.hook = self 49 | self.site = None 50 | 51 | def start_webhook(self): 52 | try: 53 | hooked = self.app.get_globals("hooked") 54 | if not hooked: 55 | asyncio.ensure_future(self.start_webserver()) 56 | else: 57 | if self.app.current_session: 58 | self.app.current_session.info("WEBHOOK已监听!", "CHATHOOK") 59 | 60 | except Exception as e: 61 | self.app.set_status(f"插件CHATHOOK在启动WEBHOOK时发生错误,错误信息:{e}") 62 | 63 | def stop_webhook(self): 64 | try: 65 | hooked = self.app.get_globals("hooked") 66 | if hooked: 67 | asyncio.ensure_future(self.stop_webserver()) 68 | 69 | except Exception as e: 70 | self.app.set_status(f"插件CHATHOOK的WEBHOOK监听服务关闭时出现错误: {e}") 71 | 72 | async def start_webserver(self): 73 | try: 74 | self.webapp = web.Application() 75 | self.webapp.add_routes([web.post('/', self.handle_post), web.get('/', self.handle_get)]) 76 | self.runner = web.AppRunner(self.webapp) 77 | await self.runner.setup() 78 | self.site = web.TCPSite(self.runner, '0.0.0.0', 8000) 79 | await self.site.start() 80 | 81 | self.app.set_globals("hooked", True) 82 | if self.app.current_session: 83 | self.app.current_session.info("WEBHOOK已在端口8000进行监听.", "CHATHOOK") 84 | self.app.set_status("插件CHATHOOK的WEBHOOK已在端口8000进行监听.") 85 | except OSError as e: 86 | # 备注:WinError错误代码为10048,98应该为LINUX系统 87 | if (e.errno == 98) or (e.errno == 10048): 88 | self.app.set_status("端口8000使用中,插件CHATHOOK的WEBHOOK监听服务启动失败.") 89 | else: 90 | self.app.set_status(f"插件CHATHOOK的WEBHOOK监听服务启动出现OSError错误,错误代码: {e.errno}") 91 | 92 | except Exception as e2: 93 | self.app.set_status(f"插件CHATHOOK的WEBHOOK监听服务启动出现错误: {e2}") 94 | 95 | async def stop_webserver(self): 96 | try: 97 | if isinstance(self.site, web.TCPSite): 98 | await self.site.stop() 99 | self.app.set_globals("hooked", False) 100 | self.app.set_status("插件CHATHOOK的WEBHOOK已关闭8000端口的监听.") 101 | if self.app.current_session: 102 | self.app.current_session.info("插件CHATHOOK的WEBHOOK已关闭8000端口的监听.", "CHATHOOK") 103 | except Exception as e: 104 | self.app.set_status(f"插件CHATHOOK的WEBHOOK监听服务关闭时出现错误: {e}") 105 | 106 | async def execute_session_command(self, name, command, from_user): 107 | if name in self.app.sessions.keys(): 108 | await self.app.sessions[name].exec_command_async(command) 109 | else: 110 | self.app.set_status(f"不存在名称为 {name} 的会话,请重试!") 111 | await self.asyncSendMessage(f"【错误】发送命令执行错误:不存在名称为 {name} 的会话,请重试!", user = from_user) 112 | 113 | async def execute_hook_command(self, name, command, param, from_user): 114 | if name in self.app.sessions.keys(): 115 | if command == "lock": # 锁定指定会话,后续发送消息时,可以不声明会话 116 | self.app.set_globals(f"session_lock_{from_user}", name) 117 | self.app.sessions[name].info(f"已将用户 {from_user} 的WEBHOOK命令锁定到本会话", "WEBHOOK") 118 | await self.asyncSendMessage(f"【状态】成功将本用户的WEBHOOK命令消息锁定到会话 {name} .", user = from_user) 119 | elif command == "unlock": 120 | self.app.set_globals(f"session_lock_{from_user}", None) 121 | self.app.sessions[name].info(f"已将用户 {from_user} 的WEBHOOK命令从本会话解锁", "WEBHOOK") 122 | await self.asyncSendMessage(f"【状态】成功将本用户的WEBHOOK命令消息从本会话解锁 {name} .", user = from_user) 123 | else: 124 | cmd = self.HOOK_COMMANDS.get(command, command) 125 | command = f"{cmd} {param}" 126 | cmd_hook = self.app.sessions[name].cmds["cmd_hook"] 127 | #await self.app.sessions[name].exec_command_async(command) 128 | await self.app.sessions[name].create_task(cmd_hook.execute(command, from_user = from_user)) 129 | 130 | elif not name: 131 | if command == "get": 132 | alive_sessions, dead_sessions = list(), list() 133 | for key, session in self.app.sessions.items(): 134 | if isinstance(session, Session): 135 | if session.connected: 136 | alive_sessions.append(key) 137 | else: 138 | dead_sessions.append(key) 139 | 140 | alive_session_msg = f'已连接会话包括:{",".join(alive_sessions)}' if len(alive_sessions) > 0 else "没有已连接会话" 141 | dead_session_msg = f'未连接会话包括:{",".join(dead_sessions)}' if len(dead_sessions) > 0 else "没有未连接会话" 142 | lock = self.app.get_globals(f"session_lock_{from_user}", None) 143 | lock_msg = f'已锁定会话{lock}' if lock else '未锁定会话' 144 | send_msg = ", ".join((alive_session_msg, dead_session_msg, lock_msg)) + "。" 145 | 146 | await self.asyncSendMessage(send_msg, user = from_user) 147 | 148 | else: 149 | self.app.set_status(f"不存在名称为 {name} 的会话,请重试!") 150 | await self.asyncSendMessage(f"【错误】发送命令执行错误:不存在名称为 {name} 的会话,请重试!", user = from_user) 151 | 152 | async def handle_post(self, request: Request): 153 | try: 154 | text = await request.text() 155 | data = urllib.parse.parse_qs(text) 156 | from_username = data['username'][0] 157 | from_userid = data['user_id'][0] 158 | message = data['text'][0] 159 | 160 | # 命令特性处置 161 | if ":" in message: 162 | msg = message.split(":") 163 | if len(msg) == 2: 164 | session_lock = self.app.get_globals(f"session_lock_{from_userid}", None) 165 | if session_lock and session_lock in self.app.sessions.keys(): 166 | self.app.sessions[session_lock].info(f"收到来自 {from_username}({from_userid}) 发送的消息: {message}", "CHATHOOK") 167 | await self.execute_hook_command(session_lock, msg[0], msg[1], from_userid) 168 | elif msg[0] in self.app.sessions.keys(): 169 | self.app.sessions[msg[0]].info(f"收到来自 {from_username}({from_userid}) 发送的消息: {message}", "CHATHOOK") 170 | await self.execute_session_command(msg[0], msg[1], from_userid) 171 | 172 | elif len(msg) == 3: 173 | name, op, param = msg[0], msg[1], msg[2] 174 | if name in self.app.sessions.keys(): 175 | self.app.sessions[name].info(f"收到来自 {from_username}({from_userid}) 发送的消息: {message}", "CHATHOOK") 176 | await self.execute_hook_command(name, op, param, from_userid) 177 | 178 | else: 179 | session_lock = self.app.get_globals(f"session_lock_{from_userid}", None) 180 | if session_lock and session_lock in self.app.sessions.keys(): 181 | self.app.sessions[session_lock].info(f"收到来自 {from_username}({from_userid}) 发送的消息: {message}", "CHATHOOK") 182 | await self.execute_session_command(session_lock, message, from_userid) 183 | else: 184 | await self.asyncSendMessage(f"【错误】既没有锁定会话,也没有指定会话,当前消息「{message}」无法执行。", user = from_userid) 185 | 186 | return web.json_response({'success': True}) 187 | 188 | except json.JSONDecodeError as e: 189 | return web.Response(text=str(e), status=400) 190 | 191 | except Exception as e2: 192 | self.app.set_status(f"post发生错误: {e2}") 193 | 194 | async def handle_get(self, request): 195 | return web.Response(text="GET method not supported.", status=501) 196 | 197 | def sendMessage(self, text, user = 5): 198 | asyncio.ensure_future(self.asyncSendMessage(text, user = user)) 199 | 200 | def sendImage(self, imagelink, text = "图像测试", user = 5): 201 | asyncio.ensure_future(self.asyncSendMessage(text, imagelink, user)) 202 | 203 | def sendFullme(self, session, link, extra_text = "FULLME", user = 5): 204 | asyncio.ensure_future(self.loadAndSendFullme(session, link, extra_text, user)) 205 | 206 | async def loadAndSendFullme(self, session, link, extra_text, user = 5): 207 | try: 208 | fmadress = link.split("robot.php?filename=")[-1] 209 | url = f"http://fullme.pkuxkx.net/robot.php?filename={fmadress}" 210 | imgs = list() 211 | 212 | client = ClientSession() 213 | for i in range(0, 3): 214 | async with client.get(url) as response: 215 | if response.status != 200: 216 | continue 217 | 218 | text = await response.text() 219 | matches = re.search(r'src="\.([^"]+\.jpg)"', text) 220 | if not matches: 221 | continue 222 | 223 | img_url = "http://fullme.pkuxkx.net" + matches.group(1) 224 | # imgs.append(img_url) 225 | 226 | msg = f"来自会话[{session.name}] 的 {extra_text} 消息:" 227 | await self.asyncSendMessage(msg, img_url, user) 228 | await asyncio.sleep(0.5) 229 | 230 | await client.close() 231 | 232 | except Exception as e: 233 | session.error(f"执行fullme的HOOK挂接时出现错误,信息为: {e}") 234 | session.error(f"异常追踪为: {traceback.format_exc()}") 235 | 236 | async def asyncSendMessage(self, text, file_url = None, user = 5): 237 | try: 238 | text = f'{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}: {text}' 239 | if file_url: 240 | data = {"payload": json.dumps({"text": text, "file_url": file_url, "user_ids": [user]})} 241 | else: 242 | data = {"payload": json.dumps({"text": text, "user_ids": [user]})} 243 | 244 | async with ClientSession() as client: 245 | async with client.post(WEBHOOK_URL, data = data) as response: 246 | info = await response.json() 247 | if info.get("success"): 248 | self.app.set_status(f"消息成功发送到用户 {user}.") 249 | else: 250 | self.app.set_status(f"消息没有成功发送到用户 {user}. 错误为 {info.get('error')}") 251 | 252 | except Exception as e: 253 | self.app.set_status(f"执行fullme的HOOK挂接时出现错误,信息为: {e}") 254 | #session.error(f"异常追踪为: {traceback.format_exc()}") 255 | if self.app.current_session: 256 | self.app.current_session.error(f"执行fullme的HOOK挂接时出现错误,信息为: {e}") 257 | self.app.current_session.error(f"异常追踪为: {traceback.format_exc()}") 258 | 259 | class CmdHookMessageHandler(Command): 260 | def __init__(self, session, *args, **kwargs): 261 | super().__init__(session, r"^(hookget)(?:\s+(\S.+))$", *args, **kwargs) 262 | 263 | def get_status(self) -> str: 264 | msg_lines = list() 265 | msg_lines.append("") 266 | fullme = int(self.session.getVariable('%fullme', 0)) 267 | delta = time.time() - fullme 268 | msg_lines.append(f"FULLME时间: {int(delta // 60)}分钟") 269 | exp, pot, food, water = self.session.getVariables(["combat_exp", "potential", "food", "water"]) 270 | busy, fight = self.session.getVariables(["is_busy", "is_fighting"]) 271 | msg_lines.append(f"实战经验: {exp}, 潜能: {pot}") 272 | msg_lines.append(f"食物: {food}, 饮水: {water} {'【忙】' if busy else '【不忙】'} {'【战斗中】' if fight else '【空闲中】'}") 273 | jing, eff_jing, max_jing = self.session.getVariables(["jing", "eff_jing", "max_jing"]) 274 | msg_lines.append(f"精神: {jing} / {eff_jing} / {max_jing}") 275 | qi, eff_qi, max_qi = self.session.getVariables(["qi", "eff_qi", "max_qi"]) 276 | msg_lines.append(f"气血: {qi} / {eff_qi} / {max_qi}") 277 | jingli, max_jingli, neili, max_neili = self.session.getVariables(["jingli", "max_jingli", "neili", "max_neili"]) 278 | msg_lines.append(f"精力: {jingli} / {max_jingli}, 内力: {neili} / {max_neili}") 279 | loc, ins_loc = self.session.getVariables(["room", "ins_loc"]) 280 | if ins_loc: 281 | msg_lines.append(f"当前位置(惯导): {ins_loc['city']} {ins_loc['name']} {ins_loc['id']}") 282 | else: 283 | msg_lines.append(f"当前位置(无惯导): {loc}") 284 | jobManager = self.session.cmds["jobmanager"] 285 | msg_lines.append(f"当前任务: {jobManager.currentJob}, 当前状态: {jobManager.currentStatus}") 286 | return "\n".join(msg_lines) 287 | 288 | async def get_skills(self) -> str: 289 | await asyncio.wait([self.create_task(self.session.exec_command_async("skills")),], timeout = 3) 290 | msg_lines = list() 291 | msg_lines.append("") 292 | skills = self.session.getVariable("skills", dict()) 293 | for key, value in skills.items(): 294 | skill_line = f"{value[2]}({key}): {value[0]} / {value[1]}" 295 | msg_lines.append(skill_line) 296 | 297 | return "\n".join(msg_lines) 298 | 299 | async def execute(self, cmd, *args, **kwargs): 300 | try: 301 | from_user = kwargs.get("from_user", 5) 302 | m = re.match(self.patterns, cmd) 303 | if m: 304 | command, param = m[1], m[2] 305 | if command == "hookget": 306 | get_func = getattr(self, f"get_{param}") 307 | if asyncio.iscoroutine(get_func) or asyncio.iscoroutinefunction(get_func): 308 | id, name = self.session.getVariables(["id", "name"]) 309 | result = await get_func() 310 | msg = f"来自{name}({id})的信息:{result}" 311 | self.session.globals.hook.sendMessage(msg, from_user) 312 | elif callable(get_func): 313 | id, name = self.session.getVariables(["id", "name"]) 314 | msg = f"来自{name}({id})的信息:{get_func()}" 315 | self.session.globals.hook.sendMessage(msg, from_user) 316 | else: 317 | msg = f"CHATHOOK不支持获取{param}参数" 318 | self.session.globals.hook.sendMessage(msg, from_user) 319 | else: 320 | msg = f"CHATHOOK不支持{command}命令" 321 | self.session.globals.hook.sendMessage(msg, from_user) 322 | 323 | except Exception as e: 324 | self.error(f"异步执行中遇到异常, {e}, 类型为 {type(e)}") 325 | self.error(f"异常追踪为: {traceback.format_exc()}") 326 | 327 | def sendMessageToHook(session, name, line, wildcards): 328 | msg = f"来自会话[{session.name}]的消息: {wildcards[0]}" 329 | session.globals.hook.sendMessage(msg) 330 | 331 | def PLUGIN_PYMUD_START(app): 332 | "PYMUD自动读取并加载插件时自动调用的函数, app为APP本体。该函数仅会在程序运行时,自动加载一次" 333 | chathook = ChatHook(app) 334 | app.set_status(f"插件{PLUGIN_NAME}已加载!") 335 | # 可以设置为全局变量,以供销毁时使用 336 | app.set_globals("chathook", chathook) 337 | 338 | def PLUGIN_SESSION_CREATE(session: Session): 339 | "在会话中加载插件时自动调用的函数, session为加载插件的会话。该函数在每一个会话创建时均被自动加载一次" 340 | # 对象在创建时会自动加入会话,因此不再需要 session.addXXX 方法调用了 341 | Alias(session, "^starthook$", id = "ali_starthook", onSuccess = lambda name, line, wildcards: session.globals.hook.start_webhook()) 342 | Alias(session, "^stophook$", id = "ali_stophook", onSuccess = lambda name, line, wildcards: session.globals.hook.stop_webhook()) 343 | Alias(session, r"^send\s(.+)$", id = "ali_sendmsg", onSuccess = partial(sendMessageToHook, session)) 344 | CmdHookMessageHandler(session, id = "cmd_hook") 345 | 346 | def PLUGIN_SESSION_DESTROY(session: Session): 347 | "在会话中卸载插件时自动调用的函数, session为卸载插件的会话。卸载在每一个会话关闭时均被自动运行一次。" 348 | # 此处清除本会话添加的Alias和Command 349 | session.delAlias("ali_starthook") 350 | session.delAlias("ali_stophook") 351 | session.delAlias("ali_sendmsg") 352 | session.delCommand("cmd_hook") 353 | 354 | def PLUGIN_PYMUD_DESTROY(app: PyMudApp): 355 | "在应用程序关闭时自动调用的函数, app为应用程序本体。" 356 | # 如果有需要销毁的对象,比如相关线程资源等,可以在此处销毁 357 | chathook = app.get_globals("chathook", None) 358 | if isinstance(chathook, ChatHook): 359 | chathook.stop_webhook() -------------------------------------------------------------------------------- /docs/source/references.rst: -------------------------------------------------------------------------------- 1 | 8 类参考 class references 2 | ===================================== 3 | 4 | .. autoclass:: pymud.PyMudApp 5 | :members: 6 | 7 | .. autoclass:: pymud.Session 8 | :members: 9 | 10 | .. autoclass:: pymud.CodeBlock 11 | :members: 12 | 13 | .. autoclass:: pymud.objects.BaseObject 14 | :members: 15 | 16 | .. autoclass:: pymud.objects.MatchObject 17 | :members: 18 | 19 | .. autoclass:: pymud.Alias 20 | :members: 21 | 22 | .. autoclass:: pymud.SimpleAlias 23 | :members: 24 | 25 | .. autoclass:: pymud.Trigger 26 | :members: 27 | 28 | .. autoclass:: pymud.SimpleTrigger 29 | :members: 30 | 31 | .. autoclass:: pymud.GMCPTrigger 32 | :members: 33 | 34 | .. autoclass:: pymud.Timer 35 | :members: 36 | 37 | .. autoclass:: pymud.SimpleTimer 38 | :members: 39 | 40 | .. autoclass:: pymud.Command 41 | :members: 42 | 43 | .. autoclass:: pymud.SimpleCommand 44 | :members: 45 | 46 | .. autoclass:: pymud.DotDict 47 | :members: 48 | 49 | .. autoclass:: pymud.modules.Plugin 50 | :members: 51 | 52 | .. autoclass:: pymud.modules.IConfig 53 | :members: 54 | 55 | .. autoclass:: pymud.modules.ModuleInfo 56 | :members: 57 | 58 | .. autoclass:: pymud.Logger 59 | :members: -------------------------------------------------------------------------------- /docs/source/settings.md: -------------------------------------------------------------------------------- 1 | # 3 应用配置及本地化 2 | 3 | ## 3.1 概述 4 | 5 | 与App有关的各类配置、常量等的配置均保存在settings.py文件,其实现了一个Settings类,并直接使用类属性设置各变量的值。 6 | 其中部分值可以被启动app的当前目录(即执行python -m pymud的目录)下的pymud.cfg文件中的配置所覆盖。当未被覆盖时,使用该文件中定义的默认值。 7 | 8 | ## 3.2 应用常量定义 9 | 10 | settings.py开头的部分为APP常量定义,如无必要,请勿修改其值。此类定义不会被其他设置所覆盖。 11 | 12 | 结尾的几个变量为APP使用的格式定义,主要指定了session.info、warning、error时的默认样式。 13 | 14 | |变量名|含义|备注| 15 | |-|-|-| 16 | |language|程序语言|默认"chs",即简体中文。另外当前还支持"eng",为AI翻译的英文版。| 17 | |\__appname__|程序名称,也是MTTS定义中的名称类型,在北侠登录时会显示|默认"PYMUD",请勿修改| 18 | |\__appdesc__|程序描述,该描述将在菜单 帮助->关于 中显示|默认"a MUD client written in Python",请勿修改| 19 | |\__version__|程序当前版本,该版本将在菜单 帮助->关于 对话框,以及状态栏右下角显示,也会在shell的窗口上显示的title中显示|有时pip上发布的版本带有beta、post标识,但不一定在此处显示。此处主要显示主版本号。请勿修改| 20 | |\__release__|当前版本程序的发布日期|请勿修改| 21 | |\__author__|程序作者,该值将在菜单 帮助->关于 中显示|请勿修改| 22 | |\__email__|程序作者的联系邮箱,该值将在菜单 帮助->关于 中显示|请勿修改| 23 | |\__website__|程序的帮助文档链接,该值将在菜单 帮助->关于 中显示,单击后会自动打开该网站|请勿修改| 24 | |INFO_STYLE|session.info默认样式|ANSI绿色标识, \\x1b[32m| 25 | |WARN_STYLE|session.warning默认样式|ANSI黄色标识, \\x1b[33m| 26 | |ERR_STYLE|session.error默认样式|ANSI绿色标识,\\x1b[31m| 27 | |CLR_STYLE|清除前面样式|ANSI清除格式标识, \\x1b[0m| 28 | 29 | ## 3.3 server字典 30 | 31 | server字典,包含了对服务器的有关配置。**该配置可以被pymud.cfg文件中的配置所覆盖**,覆盖配置时,可以只给出需要覆盖的具体关键字配置,其余未给定的关键字,将使用该文件中定义的默认值。 32 | 若是使用本客户端玩北大侠客行,所有此处的配置均无需覆盖修改,维持默认值即可。 33 | 34 | |变量名|默认值|含义|备注| 35 | |-|-|-|-| 36 | |default_encoding|utf-8|服务器默认编码|当创建会话未指定编码时,会默认使用该编码。连接pkuxkx.net时,8081端口默认utf-8| 37 | |encoding_errors|ignore|编解码错误时的默认操作,|ignore即编解码错误时不会抛出异常| 38 | |newline|\n|服务器换行符特性|与服务器有关,在不同的系统中,换行符可能为\r、\n、\r\n,北侠是\n| 39 | |SGA|True|Telnet协商选项SGA,在全双工环境中,不需要GA信号,因此默认同意抑制|参见 rfc858: | 40 | |ECHO|False|Telnet协商选项ECHO|参见 rfc857: | 41 | |GMCP|True|MUD协议,通用MUD通信协议|北侠支持GMCP,具体参见: | 42 | |MSDP|True|MUD协议,服务器数据协议|北侠数据通过GMCP而非MSDP发送。具体参见: | 43 | |MSSP|True|MUD协议,服务器状态协议|具体参见: | 44 | |MCCP2|False|MUD协议,压缩通信协议V2版|本客户端暂不支持MCCP,请不要修改此设定。协议参见: | 45 | |MCCP3|False|MUD协议,压缩通信协议V3版|本客户端暂不支持MCCP,请不要修改此设定。协议参见: | 46 | |MSP|False|MUD协议,音频协议|本客户端暂不支持MSP,请不要修改此设定。协议参见: | 47 | |MXP|False|MUD协议,MXP扩展协议|本客户端暂不支持MXP,请不要修改此设定。协议参见: | 48 | 49 | ## 3.4 mnes字典 50 | 51 | mnes字典,包含了MUD协议所需的的默认MNES(Mud New-Environment Standard)配置信息,该值均为发送到服务器所需的数据定义。该值不能被覆盖 52 | 53 | |变量名|默认值|含义| 54 | |-|-|-| 55 | |CHARSET|server["default_encoding"]|字符集| 56 | |CLIENT_NAME|\__appname__|客户端名称| 57 | |CLIENT_VERSION|\__version__|客户端版本| 58 | |AUTHOR|\__author__|客户端作者| 59 | 60 | ## 3.5 client字典 61 | 62 | client字典,包含了对PyMUD客户端有关的配置信息,对客户端定制主要修改该字典的内容。该值可以被pymud.cfg文件的定义所覆盖。 63 | 64 | |变量名|默认值|含义|备注| 65 | |-|-|-|-| 66 | |buffer_lines|5000|保留的缓冲行数|0.18.4版新增配置。该值表示了会话在清除历史数据时保留的最大行数。| 67 | |naws_width|150|客户端向服务器发送NAWS信息时的列数默认值|在实际使用过程中,程序会先通过库函数获取窗口显示的宽度和高度,无法获取时才使用该配置参数,因此无需修改。| 68 | |naws_height|40|客户端向服务器发送NAWS信息时的行数默认值|在实际使用过程中,程序会先通过库函数获取窗口显示的宽度和高度,无法获取时才使用该配置参数,因此无需修改。| 69 | |newline|\n|客户端换识别的换行符|由于系统不同,有的换行符是\r,有的是\n,有的是\r\n,用于本地写入窗体信息时换行使用。当前\n可以较好工作,因此无需修改| 70 | |tabstop|4|制表符使用空格替换的数量|该参数仅在对远程\t符号进行本地显示时使用,因此无需修改| 71 | |seperator|;|多个命令之间的分隔符|如无特殊必要,建议不要修改| 72 | |appcmdflag|#|区分PyMUD应用命令的标记,#开头的识别为PyMUD命令,非#开头的识别为发送到服务器的命令|如无特殊必要,建议不要修改| 73 | |interval|10|单位ms,指异步多个命令执行时,两条命令之间自动插入的间隔时间|例如一段路径:e;s;s;e;n,在执行时每个命令之间会自动插入10ms间隔| 74 | |auto_connect|True|创建会话后是否自动连接,当为False时,创建会话后不会自动连接到服务器,需要手动或输入命令#connect连接。|特别备注,若在pymud.cfg中覆盖该配置,由于cfg文件是json格式原因,不能使用True来表示,建议改成1,或true(小写)| 75 | |auto_reconnect|False|在已连接的会话由于种种原因导致断开后,是否会自动重新连接的配置|pymud.cfg覆盖时,注意json格式| 76 | |reconnect_wait|15|当启动自动重连时,从断开到下次连接之间等待的时间,默认15秒|注意15是个int值,不是字符串"15"| 77 | |var_autosave|True|是否自动保存会话变量的配置。当为True时,在会话断开时刻会自动将所有本会话的variables变量保存到会话名.mud文件中|注意,断开时刻才会保存。若直接#exit或菜单退出,会导致来不及读到服务器断开的消息,可能变量不会正确保存| 78 | |var_autoload|True|是否自动加载会话变量的配置。当为True时,在会话创建时刻,会自动检查当前目录是否存在会话名.mud文件,若存在,会自动将其中的变量加载到session的variables中| 79 | |remain_last_input|False|在命令行回车后,是否保留上一次输入的内容|此处有bug,当为True时,是可以保持上一次的内容,但回车、重新键入值等操作均会失效,因此暂时不要将该值改为True| 80 | |history_records|500|记录发送到服务器的数据历史的数量|默认500。为0时表示不记录,为-1时表示记录所有历史| 81 | |echo_input|False|是否在session窗口中回显输入的命令|该设置可以临时通过会话菜单进行切换| 82 | |beautify|True|解决中文字符环境下的对齐问题,打开后会自动修改收到的数据中不被正确识别宽度的字符,以解决对齐问题|玩中文MUD游戏时,特别是北大侠客行时,建议此设置打开。| 83 | |status_divider|True|是否在状态窗口中显示分隔线|当status_display为1时在状窗口上方,当为2时在状态窗口左侧| 84 | |status_display|1|状态窗口的显示设置。0不显示状态窗口,1显示在下方,2显示在右方|状态窗口通过session.status_maker属性接口进行显示设置| 85 | |status_width|30|状态窗口显示宽度(字符数)|当status_display为2时生效,此时为右侧显示的状态窗口列数| 86 | |status_height|6|状态窗口显示高度(行数)|当status_display为1时生效,此时为下侧显示状态窗口行数| 87 | 88 | 89 | ## 3.6 text字典 90 | 91 | text字典,包含了可配置的显示内容定义。菜单读取、显示的一些基本内容都可以在此修改。可被pymud.cfg所覆盖。 92 | 目前,text字典被国际化语言包所替代,替代文件位于pymud/lang/目录下,此处不再建议修改相关内容。 93 | 各菜单对应的操作含义,见 [2.2 菜单操作](../ui.html#id3) 94 | 部分未在本文件列出的其他text字典内容暂未被使用。 95 | 96 | |变量名|默认值|含义| 97 | |-|-|-| 98 | |welcome|欢迎使用PYMUD客户端 - 北大侠客行,最好的中文MUD游戏|打开PyMUD时,显示在底部状态栏的内容| 99 | |world|世界|世界菜单显示字符| 100 | |new_session|世界菜单下的第一个子菜单显示字符,操作时弹出对话框创建新会话| 101 | |exit|退出|世界菜单下退出菜单显示字符,操作时退出PyMUD应用| 102 | |session|会话|会话菜单显示字符| 103 | |connect|连接/重新连接|会话菜单下子菜单,操作时相当于键入#connect命令| 104 | |disconnect|断开连接|会话菜单下子菜单| 105 | |echoinput|显示/隐藏输入指令|会话菜单下子菜单,临时改变client["echo_input"]的配置状态| 106 | |nosplit|取消分屏|会话菜单下子菜单,在分屏模式下取消分屏,等同于快捷键Ctrl+Z| 107 | |copy|复制(纯文本)|会话菜单下子菜单,以纯文本模式复制选中内容到剪贴板,等同于快捷键Ctrl+C。特别说明,Mac系统下,复制快捷键也是Ctrl+C,系统快捷键Command+C是不生效的| 108 | |copyraw|复制(ANSI)|会话菜单下子菜单,以带ANSI码格式复制行(仅能用于行复制)| 109 | |clearsession|清空会话内容|会话菜单下子菜单| 110 | |closesession|关闭当前会话|会话菜单下子菜单| 111 | |autoreconnect|打开/关闭自动重连|会话菜单下子菜单,临时改变client["auto_reconnect"]的配置状态| 112 | |reloadconfig|重新加载脚本配置|会话菜单下子菜单| 113 | |help|帮助|帮助一级菜单| 114 | |about|关于|帮助菜单下子菜单| 115 | |input_prompt|命令:|命令行的提示符内容,必须是可被prompt_toolkit所识别的HTML对象| 116 | 117 | ## 3.7 styles字典 118 | 119 | styles字典定义了PyMUD显示时的各种格式,该格式定义类似于HTML的css层叠样式表,具体格式要按prompt_toolkit中的定义。 120 | 此处具体内容不在详细展开叙述,若在status_maker中需要使用自定义格式,可以在styles增加自己的定义,并在status_maker的接口函数中自行使用这些样式。 121 | 122 | ## 3.8 sessions字典 123 | 124 | sessions字典是启动PyMUD应用时,自动创建会话菜单的关键,所有相关信息都填在此处。settings.py文件中给定的sessions字典可以作为写pymud.cfg文件的参考,但为解决应用本地化问题,每个人应该在自己运行pymud的目录下创建pymud.cfg文件,并覆盖sessions字典有关内容。 125 | 126 | sessions字典支持多个key,其中每一个key对应的value都应该是一个字典,每一个key会在会话菜单下产生一个菜单,value的值则会在key产生的菜单下生成更下一级的子菜单。 127 | 128 | 每个key对应的value字典下,可以包含的关键字和含义如下: 129 | 130 | |关键字|可接受对象类型|含义|备注| 131 | |-|-|-|-| 132 | |host|字符串|此字典角色对应的服务器地址,可接受IP或者域名|如北侠则设置为 mud.pkuxkx.net| 133 | |port|字符串|此字典角色对应的服务器端口号|端口号和编码格式有关,如北侠默认采用UTF-8编码的端口为8081| 134 | |encoding|字符串|服务器编码方式,如GBK、UTF8、BIG5等等|要python识别的编码方式才可以| 135 | |autologin|字符串|当自动登录时,自动输入用户名密码的操作。可接受格式化参数,如{0},{1}|参数由下面char关键字内容的列表所定义。北侠登录是先输入用户名,然后输入密码,因此可以{0};{1}表示。evennia类MUD的登录是使用connect user pass,因此使用connect {0} {1}表示| 136 | |default_script|字符串|该组角色默认加载的脚本清单|写在此处的脚本会被下面chars所有角色连接时自动加载,可支持多个脚本,以列表['modulea','moduleb']形式隔开即可。所有脚本不要带.py扩展名,其名称应和python代码中import xxx所使用的名称相同| 137 | |chars|字典|在该host下的所有角色,每一个角色会创建一个菜单项|chars字典中的key是菜单项上显示的名称,该key对应的value应该是一个列表,列表可包含三个对象,分别为登录的id、密码、以及仅该角色加载的脚本清单。脚本清单也可以不指定(此时使用2个对象即可,也可以指定多个,使用与default_script相同的列表样式表示| 138 | 139 | -------------------------------------------------------------------------------- /docs/source/syscommand.rst: -------------------------------------------------------------------------------- 1 | 4 系统命令 2 | ================= 3 | 4 | | 系统命令是指在命令行键入的用于操作系统功能的命令,一般以#号开头。 5 | | 除 `#session`_ 命令外,其他命令均可以在代码中通过 ``session.exec`` 系列命令进行调用。 6 | | 在命令中可以使用大括号{}将一段代码括起来,形成代码块。代码块会被作为一条命令处理。 7 | 8 | ``#action`` 9 | ---------------- 10 | 11 | 等同于 `#trigger`_ 12 | 13 | ``#ali`` 14 | ---------------- 15 | 16 | 为 `#alias`_ 命令的简写 17 | 18 | ``#alias`` 19 | ---------------- 20 | 21 | #alias命令用于操作别名。#ali是该命令的简写方式。该命令可以不带参数、带一个参数或者两个参数。用法与示例如下。 22 | 23 | - ``#ali`` : 无参数, 打印列出当前会话中所有的别名清单 24 | - ``#ali my_ali`` : 一个参数, 列出id为my_ali的 :class:`pymud.Alias` 对象的详细信息 25 | - ``#ali my_ali on`` : 两个参数,启用id为my_ali的 :class:`pymud.Alias` 对象(enabled = True) 26 | - ``#ali my_ali off`` : 两个参数, 禁用id为my_ali的 :class:`pymud.Alias` 对象(enabled = False) 27 | - ``#ali my_ali del`` : 两个参数,删除id为my_ali的 :class:`pymud.Alias` 对象 28 | - ``#ali {^gp\s(.+)$} {get %1 from corpse}`` : 两个参数,新增创建一个 :class:`pymud.Alias` 对象。使用时, ``gp gold = get gold from corpse`` 29 | 30 | ``#all`` 31 | ---------------- 32 | 33 | #all命令可以同时向所有会话发送统一命令。用法与示例如下。 34 | 35 | - ``#all #cls`` : 所有会话统一执行#cls命令 36 | - ``#all quit`` : 所有会话的角色统一执行quit退出 37 | 38 | ``#clear`` 39 | ---------------- 40 | 41 | 清屏命令,清除当前会话所有缓存显示内容。 42 | 43 | ``#cls`` 44 | ---------------- 45 | 46 | `#clear`_ 命令的简写 47 | 48 | ``#close`` 49 | ---------------- 50 | 51 | 关闭当前会话,并将当前会话从pymud的会话列表中移除。 52 | 53 | *注:当前会话处于连接状态时,#close关闭会话会弹出对话框确认是否关闭* 54 | 55 | ``#command`` 56 | ---------------- 57 | 58 | #command命令用于操作命令。#cmd是该命令的简写方式。该命令可以不带参数、带一个参数或者两个参数。用法与示例如下。 59 | 60 | - ``#cmd`` : 无参数, 打印列出当前会话中所有的命令清单 61 | - ``#cmd my_cmd`` : 一个参数, 列出id为my_cmd的 :class:`pymud.Command` 对象的详细信息 62 | - ``#cmd my_cmd on`` : 两个参数,启用id为my_cmd的 :class:`pymud.Command` 对象(enabled = True) 63 | - ``#cmd my_cmd off`` : 两个参数, 禁用id为my_cmd的 :class:`pymud.Command` 对象(enabled = False) 64 | - ``#cmd my_cmd del`` : 两个参数,删除id为my_cmd的 :class:`pymud.Command` 对象 65 | 66 | ``#con`` 67 | ---------------- 68 | 69 | `#connect`_ 命令的简写 70 | 71 | ``#connect`` 72 | ---------------- 73 | 74 | 连接到远程服务器(仅当远程服务器未连接时有效)。命令是通过调用 ``Session.open()`` 来实现连接。 75 | 76 | ``#cmd`` 77 | ---------------- 78 | 79 | `#command`_ 命令的简写 80 | 81 | ``#dis`` 82 | ---------------- 83 | 84 | `#disconnect`_ 命令的简写 85 | 86 | ``#disconnect`` 87 | ---------------- 88 | 89 | 断开到远程服务器的连接。命令是通过调用 ``Session.disconnect()`` 来实现连接。 90 | 91 | ``#echo`` 92 | ---------------- 93 | 94 | 触发器测试命令。使用 #echo 测试时,相当于收到了来自服务器传递的对应数据,是否能导致触发器执行按当前程序实际情况。并且,#echo 除了键入的内容外,不会显示触发结果的额外信息。 95 | 96 | - ``#echo 你深深吸了口气,站了起来。`` : 相当于从服务器收到“你深深吸了口气,站了起来。” 97 | - ``#echo %copy``: 复制一句话,相当于从服务器再次收到复制的这句内容。 98 | 99 | ``#error`` 100 | ---------------- 101 | 102 | 使用 ``Session.error`` 输出信息, 该信息默认带有红色的标记。 103 | 104 | ``#exit`` 105 | ---------------- 106 | 107 | 退出PyMUD程序。 108 | 109 | *注:当应用中存在还处于连接状态的会话时,#exit退出应用会逐个弹出对话框确认这些会话是否关闭* 110 | 111 | ``#gag`` 112 | ---------------- 113 | 114 | 在主窗口中不显示当前行内容,一般用于触发器中。 115 | 116 | *注意:一旦当前行被gag之后,无论如何都不会再显示此行内容,但对应的触发器仍会生效* 117 | 118 | ``#global`` 119 | ---------------- 120 | 121 | #global命令用于操作全局变量。该命令可以不带参数、带一个参数或者两个参数。用法与示例如下。 122 | 123 | - ``#global`` : 不带参数,列出程序当前所有全局变量清单 124 | - ``#global hooked`` : 带1个参数,列出程序当前名称为hooked的全局变量值 125 | - ``#global hooked 1`` : 带2个参数,设置名称为hooked的变量值为1(字符串格式) 126 | 127 | ``#gmcp`` 128 | ---------------- 129 | 130 | #gmcp命令用于操作GMCPTrigger。该命令可以不带参数、带一个参数或者两个参数。用法与示例如下。 131 | 132 | - ``#gmcp`` : 无参数, 打印列出当前会话中所有的 `GMCPTrigger` 清单 133 | - ``#cmd GMCP.Move`` : 一个参数, 列出id为GMCP.Move的 `GMCPTrigger` 对象的详细信息 134 | - ``#cmd GMCP.Move on`` : 两个参数,启用id为GMCP.Move的 `GMCPTrigger` 对象(enabled = True) 135 | - ``#cmd GMCP.Move off`` : 两个参数,禁用id为GMCP.Move的 `GMCPTrigger` 对象(enabled = False) 136 | - ``#cmd GMCP.Move del`` : 两个参数,删除id为GMCP.Move的 `GMCPTrigger` 对象 137 | 138 | ``#help`` 139 | ---------------- 140 | 141 | 显示帮助。当不带参数时, #help会列出所有可用的帮助主题。带参数显示该系统命令的帮助。参数中不需要#号。用法与示例如下。 142 | 143 | - ``#help`` : 打印所有支持的系统命令清单。其中,绿色字体的为简称/别名,白色字体的为原始命令 144 | - ``#help trigger`` : 显示#trigger命令的使用帮助 145 | 146 | ``#ig`` 147 | ---------------- 148 | 149 | 命令 `#ignore`_ 的简写 150 | 151 | ``#ignore`` 152 | ---------------- 153 | 切换所有触发器是否被响应的状态。当触发器被全局禁用时,状态栏右下角处会显示“全局禁用”字符提示。 154 | 155 | *注意:在触发器中使用#ig可能导致无法预料的影响* 156 | 157 | *使用快捷键F3(可由pymud.cfg配置)相当于输入命令#ignore(0.19.1版新增)* 158 | 159 | ``#load`` 160 | ---------------- 161 | 162 | 为当前session加载指定的模块。当要加载多个模块时,使用空格或英文逗号隔开。也可以用于激活本会话中的插件。 163 | 164 | 多个模块加载时,按指定名称的先后顺序逐个加载(当有依赖关系时,需指定顺序按依赖影响依次加载) 。 165 | 166 | - ``#load myscript`` : 加载myscript模块,首先会从执行PyMUD应用的当前目录下查找myscript.py文件并进行加载 167 | - ``#load pymud.pkuxkx`` : 加载pymud.pkuxkx模块。相当于脚本中的 import pymud.pkuxkx 命令 168 | - ``#load myscript1 myscript2`` : 依次加载myscript1和myscript2模块 169 | - ``#load myscript1,myscript2`` : 多个脚本之间也可以用逗号分隔 170 | - ``#load myplugin`` : 在本会话中激活名称为myplugin的插件(注意,是插件中由 PLUGIN_NAME 变量指定的名称,而不是文件名),实际是调用插件中定义的 PLUGIN_SESSION_CREATE 方法。 171 | 172 | ``#mess`` 173 | ---------------- 174 | 175 | `#message`_ 的简写 176 | 177 | ``#message`` 178 | ---------------- 179 | 180 | 使用弹出窗体显示消息。 181 | 182 | - ``#mess 这是一行测试`` : 使用弹出窗口显示“这是一行测试” 183 | - ``#mess %line`` : 使用弹出窗口显示系统变量%line的值 184 | 185 | ``#mods`` 186 | ---------------- 187 | 188 | `#modules`_ 命令的简写 189 | 190 | ``#modules`` 191 | ---------------- 192 | 193 | 模块命令,该命令不带参数。可列出本程序当前已加载的所有模块信息. 194 | 195 | ``#num`` 196 | ---------------- 197 | 198 | 重复执行num次后面的命令。命令也可以代码块进行嵌套使用。如: 199 | 200 | - ``#3 get m1b from nang`` : 从锦囊中取出3次地*木灵 201 | - ``#3 {#3 get m1b from nang;#wa 500;combine gem;#wa 4000};xixi`` : 执行三次合并地*木灵宝石的操作,中间留够延时等待时间,全部结束后发出xixi。 202 | 203 | ``#plugins`` 204 | ---------------- 205 | 206 | 插件命令。当不带参数时,列出本程序当前已加载的所有插件信息 207 | 208 | 当带参数时,列出指定名称插件的具体信息 。使用示例如下。 209 | 210 | - ``#plugins`` : 显示当前所有已加载插件 211 | - ``#plugins chathook`` : 显示插件chathook的具体信息 212 | 213 | ``#py`` 214 | ---------------- 215 | 216 | 直接执行后面跟着的python语句。执行语句时,环境为当前上下文环境,此时self代表当前会话。 217 | 218 | - ``#py self.info("hello")`` : 相当于在当前会话中调用 ``session.info("hello")`` 219 | - ``#py self.enableGroup("group1", False)`` : 相当于调用 ``session.enableGroup("group1", False)`` 220 | 221 | ``#reload`` 222 | ---------------- 223 | 224 | 对已加载脚本进行重新加载。 225 | 226 | 不带参数时,为当前session重新加载所有配置模块(不是重新加载插件)。 227 | 228 | 带参数时, 若指定名称为模块,则重新加载模块;若指定名称为插件,则重新加载插件。若指定名称既有模块也有插件,则仅重新加载模块(建议不要重名)。 229 | 230 | 若要重新加载多个模块,可以在参数中使用空格或英文逗号隔开多个模块名称 。 231 | 232 | - ``#reload`` : 重新加载所有已加载模块 233 | - ``#reload mymodule`` : 重新加载名为mymodule的模块 234 | - ``#reload myplugins`` : 重新加载名为myplugins的插件 235 | - ``#reload mymodule myplugins`` : 重新加载名为mymodule的模块和名为myplugins的插件。 236 | 237 | **注意事项** 238 | 239 | 1. #reload只能重新加载#load方式加载的模块(包括在pymud.cfg中指定的),但不能重新加载import xxx导入的模块。 240 | 2. 若加载的模块脚本中有语法错误,#reload貌似无法生效。此时需要退出PyMUD重新打开 241 | 3. 若加载时依次加载了不同模块,且模块之间存在依赖关系,那么重新加载时,应按原依赖关系顺序逐个重新加载,否则容易找不到依赖或依赖出错 242 | 243 | ``#replace`` 244 | ---------------- 245 | 246 | 修改显示内容,将当前行原本显示内容替换为msg显示。不需要增加换行符。 247 | 248 | *注意:应在触发器的同步处理中使用。多行触发器时,替代只替代最后一行。* 249 | 250 | - ``#replace %raw - 捕获到此行`` : 将捕获的当前行信息后面增加标注 251 | 252 | ``#reset`` 253 | ---------------- 254 | 复位全部脚本。将复位所有的触发器、命令、未完成的任务,并清空所有触发器、命令、别名、变量。 255 | 256 | ``#save`` 257 | ---------------- 258 | 259 | 将当前会话中的变量保存到文件,系统变量(%line, %raw, %copy)除外 260 | 261 | 文件保存在当前目录下,文件名为 {会话名}.mud 。 262 | 263 | *注意:变量保存使用了python的pickle模块,因此所有变量都应是自省的。 264 | 虽然PyMUD的变量支持所有的Python类型,但是仍然建议仅在变量中使用可以序列化的类型。 265 | 另外,namedtuple不建议使用,因为加载后在类型匹配比较时会失败,不认为两个相同定义的namedtuple是同一种类型。* 266 | 267 | ``#session`` 268 | ---------------- 269 | 270 | 会话操作命令。#session命令可以创建会话,直接#sessionname可以切换会话和操作会话命令。使用示例如下。 271 | 272 | - ``#session {名称} {宿主机} {端口} {编码}`` : 创建一个远程连接会话,使用指定编码格式连接到远程宿主机的指定端口并保存为 {名称} 。其中,编码可以省略,此时使用Settings.server["default_encoding"]的值,默认为utf8 273 | - ``#session newstart mud.pkuxkx.net 8080 GBK`` : 使用GBK编码连接到mud.pkuxkx.net的8080端口,并将该会话命名为newstart 274 | - ``#session newstart mud.pkuxkx.net 8081`` : 使用UTF8编码连接到mud.pkuxkx.net的8081端口,并将该会话命名为newstart 275 | - ``#session pkuxkx.newstart`` : 通过指定快捷配置创建会话,相当于点击 世界->pkuxkx->newstart 菜单创建会话。若该会话存在,则切换到该会话 276 | - ``#newstart`` : 将名称为newstart的会话切换为当前会话 277 | - ``#newstart give miui gold`` : 使名称为newstart的会话执行give miui gold指令,但不切换到该会话 278 | 279 | *注意: 一个PyMUD应用中,不能存在重名的会话。* 280 | 281 | ``#show`` 282 | ---------------- 283 | 284 | 触发器测试命令。类似于zmud的#show命令。 285 | 286 | - ``#show 你深深吸了口气,站了起来。`` : 模拟服务器收到“你深深吸了口气,站了起来。”时的情况进行触发测试 287 | - ``#show %copy``: 复制一句话,模拟服务器再次收到复制的这句内容时的情况进行触发器测试 288 | 289 | *注意: #show命令测试触发器时,触发器不会真的响应。* 290 | 291 | ``#t+`` 292 | ---------------- 293 | 294 | 组使能命令。使能给定组名及所有子组名内的的所有对象,包括别名、触发器、命令、定时器、GMCPTrigger等。 295 | 296 | - ``#t+ mygroup`` : 将mygroup组,以及所有其自组(组名类似 mygroup.xxx mygroup.xxx.yyy 为 mygroup 的子组)的所有对象使能状态打开。 297 | 298 | ``#t-`` 299 | ---------------- 300 | 301 | 组禁用命令。禁用给定组名的所有对象,包括别名、触发器、命令、定时器、GMCPTrigger等。 302 | 303 | - ``#t- mygroup`` : 将mygroup组,以及所有其自组(组名类似 mygroup.xxx mygroup.xxx.yyy 为 mygroup 的子组)的所有对象设置为禁用。 304 | 305 | ``#task`` 306 | ---------------- 307 | 308 | 列出当前由本session管理的所有task清单。主要用于调试。 309 | 310 | 使用 ``session.create_task`` 创建的任务默认会加入此清单。使用 ``session.remove_task`` 可以将任务从清单中移除。 311 | 312 | 系统会定期/不定期从清单中清除已完成或已取消的任务。 313 | 314 | ``#test`` 315 | ---------------- 316 | 317 | 触发器测试命令。与#show命令的唯一差异在于,使用#test进行测试会导致触发器响应。可以用#test来进行强制触发响应。 318 | 319 | - ``#test 你深深吸了口气,站了起来。`` : 模拟服务器收到“你深深吸了口气,站了起来。”时的情况进行触发测试 320 | - ``#test %copy``: 复制一句话,模拟服务器再次收到复制的这句内容时的情况进行触发器测试 321 | 322 | *注意: #test命令测试触发器时,触发器无论是否使能,均会真的响应。* 323 | 324 | ``#ti`` 325 | ---------------- 326 | 327 | 定时器命令 `#timer`_ 的简写形式 328 | 329 | ``#timer`` 330 | ---------------- 331 | 332 | #timer命令用于操作定时器。#ti是该命令的简写方式。该命令可以不带参数、带一个参数或者两个参数。用法与示例如下。 333 | 334 | - ``#ti``: 无参数, 打印列出当前会话中所有的定时器清单 335 | - ``#ti my_timer``: 一个参数, 列出id为my_timer的Timer对象的详细信息 336 | - ``#ti my_timer on``: 两个参数,启用id为my_timer的Timer对象(enabled = True) 337 | - ``#ti my_timer off``: 两个参数, 禁用id为my_timer的Timer对象(enabled = False) 338 | - ``#ti my_timer del``: 两个参数,删除id为my_timer的Timer对象 339 | - ``#ti 100 {drink jiudai;#wa 200;eat liang}``: 两个参数,新增创建一个Timer对象。每隔100s,自动执行一次喝酒袋吃干粮。 340 | 341 | *注意: PyMUD支持同时任意多个定时器。* 342 | 343 | ``#tri`` 344 | ---------------- 345 | 346 | 触发器命令 `#trigger`_ 的简写形式 347 | 348 | ``#trigger`` 349 | ---------------- 350 | 351 | #trigger命令用于操作触发器。#tri是该命令的简写方式。该命令可以不带参数、带一个参数或者两个参数。用法与示例如下。 352 | 353 | - ``#tri``: 无参数, 打印列出当前会话中所有的触发器清单 354 | - ``#tri my_tri``: 一个参数, 列出id为my_tri的Trigger对象的详细信息 355 | - ``#tri my_tri on``: 两个参数,启用id为my_tri的Trigger对象(enabled = True) 356 | - ``#tri my_tri off``: 两个参数, 禁用id为my_tri的Trigger对象(enabled = False) 357 | - ``#tri my_tri del``: 两个参数,删除id为my_tri的Trigger对象 358 | - ``#tri {^[> ]*段誉脚下一个不稳.+} {get duan}``: 两个参数,新增创建一个Trigger对象。当段誉被打倒的时刻把他背起来。 359 | 360 | ``#unload`` 361 | ---------------- 362 | 363 | 为当前session卸载指定的模块。当要卸载多个模块时,使用空格或英文逗号隔开。 364 | 也可以用于在当前会话中临时禁用指定的插件。 365 | 366 | 卸载模块时,将调用模块IConfig子类的__unload__方法,若存在未使用装饰器实现的自定义对象,请将这些对象的清理工作代码显式放在此方法中 。 367 | 368 | - ``#unload mymodule``: 卸载名为mymodule的模块(并调用其中Configuration类的unload方法【若有】) 369 | - ``#unload myplugin`` : 在本会话中禁用名称为myplugin的插件(注意,是插件中由 PLUGIN_NAME 变量指定的名称,而不是文件名),实际是调用插件中定义的 PLUGIN_SESSION_DESTROY 方法。 370 | 371 | ``#var`` 372 | ---------------- 373 | 374 | 变量操作命令 `#variable`_ 的简写 375 | 376 | ``#variable`` 377 | ---------------- 378 | 379 | 变量操作命令。#var时该命令的简写形式。该命令可以不带参数、带一个参数、两个参数。 380 | 381 | - ``#var``: 不带参数,列出当前会话中所有的变量清单 382 | - ``#var myvar``: 带1个参数,列出当前会话中名称为myvar的变量值 383 | - ``#var myvar 2``: 带2个参数,设置名称为myvar的变量值为2(字符串格式) 384 | 385 | *注意: #var设置的变量,其格式都是字符串形式,即#var myvar 2后,myvar = '2',而不是myvar = 2* 386 | 387 | ``#wa`` 388 | ---------------- 389 | 390 | 延时等待命令 `#wait`_ 的缩写形式 391 | 392 | ``#wait`` 393 | ---------------- 394 | 395 | 异步延时等待指定时间,用于多个命令间的延时等待。 396 | 397 | - ``drink jiudai;#wa 200;eat liang``: 喝酒袋之后,等待200ms再执行吃干粮命令 398 | 399 | ``#warning`` 400 | ---------------- 401 | 402 | 使用 ``Session.warning`` 输出信息, 该信息默认带有黄色的标记。 403 | 404 | ``#info`` 405 | ---------------- 406 | 使用 ``Session.info`` 输出信息, 该信息默认带有绿色的标记。 407 | 408 | 409 | ``#log`` 410 | ---------------- 411 | #log命令用于操作自带记录器Logger对象的处理。该命令可以不带参数、带一个参数或者多个参数。用法与示例如下。 412 | 413 | - ``#log``: 无参数, 显示所有记录器的状态情况 414 | - ``#log start [logger-name] [-a|-w|-n] [-r]``: 启动一个记录器 415 | 416 | 参数: 417 | - :logger-name: 记录器名称。当不指定时,选择名称为会话名称的记录器(会话默认记录器) 418 | - :-a|-w|-n: 记录器模式选择。 -a 为添加模式(未指定时默认值),在原记录文件后端添加; -w 为覆写模式,清空原记录文件并重新记录; -n 为新建模式,以名称和当前时间为参数,使用 name.now.log 形式创建新的记录文件 419 | - :-r: 指定记录器是否使用 raw 模式 420 | 421 | - ``#log stop [logger-name]``: 停止一个记录器 422 | 423 | 参数: 424 | - :logger-name: 记录器名称。当不指定时,选择名称为会话名称的记录器(会话默认记录器) 425 | 426 | - ``#log show [loggerFile]``: 显示全部日志记录或指定记录文件 427 | 428 | 参数: 429 | - :loggerFile: 要显示的记录文件名称。当不指定时,弹出对话框列出当前目录下所有记录文件 430 | -------------------------------------------------------------------------------- /docs/source/ui.rst: -------------------------------------------------------------------------------- 1 | 2 界面和操作 2 | ===================== 3 | 4 | 2.1 界面 5 | --------------------- 6 | 7 | 下图是主要界面,用1-7标注出7个主要内容,含义如下: 8 | 9 | 1. 菜单栏,可以鼠标操作(手机上是触控),目前包括3个1级菜单。 10 | 2. 多会话的显示和切换标签。多个会话时,每一个会话的名称会在此处,灰底亮字的是当前会话。颜色为绿色的表示当前会话处于连接状态,未处于连接状态的为白色(灰色)。可以直接单击会话名称切换会话。键盘的话,Ctrl+左右箭头可以切换。 11 | 3. 中间分隔线。当向上滚动或翻页的时候,会产生中间分隔线,此时上半部分不再滚动,下半部分持续滚动显示最新的消息。取消分隔的话,可以向下滚动到底,或者Ctrl-Z快捷键取消(仿zmud操作)。上下翻页只能使用鼠标滚轮,或者PageUp / PageDown按键。鼠标滚轮是一次一行,键盘是一次一页。 12 | 4. 状态窗口。可以使用函数自定义状态窗口显示内容,图例是我自己定义的状态窗口显示内容。 13 | 5. 命令输入行。命令输入行支持历史记录、以及基于历史记录的自动建议和自动完成。键盘上下方向键可以在历史记录中选择,键盘右键实现历史记录建议的自动完成。比如前面输入过ask pu about job,后面输入ask的时候,后面的 pu about job会以灰色显示(表示自动建议),只需键盘右箭头,即可完成输入。 14 | 6. 状态栏,显示状态信息,用于显示部分运行时的状态信息(比如复制内容),也可以在代码中通过 ``session.application.set_status`` 来设置显示的文字。 15 | 7. 状态栏(右),显示鼠标状态、全局触发器状态、连接状态和连接时间。其中,鼠标状态(显示为:鼠标已禁用)和全局触发器状态(显示为:全局禁用)仅在禁用情况下显示。另外,因为基于控制台的ui不是实时刷新,因此链接的时间有时会滞后显示,看上去就是秒不在跳动,不影响时间记录。 16 | 17 | .. image:: _static/main_ui.png 18 | :alt: 主界面 19 | 20 | 2.2 菜单 21 | --------------------- 22 | 23 | 当前UI中共有3个一级菜单,分别为:世界、会话、帮助 24 | 25 | 2.2.1 世界 26 | ^^^^^^^^^^^^^^^^^^^^^^ 27 | 28 | - **创建新会话:** 29 | 30 | 使用ui界面创建一个新会话 31 | 32 | .. image:: _static/ui_new_session_1.png 33 | :alt: 创建会话菜单 34 | 35 | .. image:: _static/ui_new_session_2.png 36 | :alt: 创建会话窗口 37 | 38 | - **显示记录信息** 39 | 40 | 当在运行时使用调试模式或记录器进行记录之后,此菜单能列出当前目录下运行APP的所有记录文件以供查看。 41 | 42 | - **退出:** 43 | 44 | 退出PYMUD应用 45 | 46 | - **创建新会话与退出中间的菜单** 47 | 48 | 在没有指定自定义配置时,该菜单默认是读取包中的settings.py创建。该菜单可以自行配置,配置方法如下: 49 | 50 | 在当前目录下(当前目录即指运行 python -m pymud 的某个目录,可自行设置,如 d:\pkuxkx\)新建pymud.cfg文件,将下列内容拷贝复制进去(内容为json格式,该json文件不支持注释): 51 | 52 | .. code:: json 53 | 54 | { 55 | "sessions": { 56 | "pkuxkx" : { 57 | "host" : "mud.pkuxkx.net", 58 | "port" : "8081", 59 | "encoding" : "utf8", 60 | "autologin" : "{0};{1}", 61 | "default_script": ["pkuxkx.common", "pkuxkx.commands", "pkuxkx.main"], 62 | "chars" : { 63 | "char1": ["yourid1", "yourpassword1"], 64 | "char2": ["yourid2", "yourpassword2", "pkuxkx.wudang"], 65 | "char3": ["yourid3", "yourpassword3", "pkuxkx.wudang,pkuxkx.lingwu"], 66 | "char4": ["yourid4", "yourpassword4", ["pkuxkx.shaolin","pkuxkx.lingwu"]] 67 | } 68 | } 69 | } 70 | } 71 | 72 | 73 | 其中,要修改的部分包括: 74 | 75 | - default_script: 所有角色均会加载的脚本 76 | - char1, char2, char3, char4: 需要添加的角色在菜单上显示的内容(每一个角色会生成一个菜单项) 77 | - 每一个角色后面的3个对象含义为:yourid即角色id,yourpassword即角色密码,该角色单独加载的脚本(可以使用逗号或者列表指定多个,也可以没有) 78 | 79 | 此时,再次在d:\\pkuxkx\\下执行 ``python -m pymud`` 时,会生成如下菜单 80 | 81 | .. image:: _static/chars_menu.png 82 | :alt: 会话菜单 83 | 84 | 85 | 2.2.2 会话 86 | ^^^^^^^^^^^^^^^^^^^^^^ 87 | 88 | - **连接/重新连接** 89 | 90 | 将当前会话重新连接到服务器。若没有当前会话、或当前会话处于连接状态时,该菜单操作无效。可以在命令行使用 ``#connect`` 或 ``#con`` 实现同样功能 91 | 92 | - **断开连接** 93 | 94 | 将当前会话从服务器断开。若没有当前会话、或当前会话已处于断开状态时,该菜单操作无效。 95 | 96 | - **关闭当前会话** 97 | 98 | 关闭当前会话窗口。若没有当前会话时,该菜单操作无效。若当前会话处于连接状态时,此操作会弹出确认提示框。可以在命令行使用 ``#close`` 实现同样功能。 99 | 100 | - **打开/关闭自动重连** 101 | 102 | 切换应用的自动重连功能。该功能默认由 `settings.py `_ 中, `client` 下的 `auto_reconnect` 配置所确定。该配置可以被 `pymud.cfg` 覆盖。该设置会对所有会话生效。 103 | 104 | - **显示/隐藏输入指令** 105 | 106 | 切换会话命令输入时是否在主窗口中是否回显。该功能默认由 `settings.py `_ 中, `client` 下的 `echo_input` 配置所确定。该配置可以被 `pymud.cfg` 覆盖。该设置会对所有会话生效。 107 | 108 | - **取消分屏** 109 | 110 | 当窗口信息较多,向上滚动时(支持鼠标滚动和PageUp翻页键),会自动分屏。该菜单操作会取消分屏,将显示回到最底部。可以通过快捷键 Ctrl + Z 实现同样功能。 111 | 112 | - **复制(纯文本)** 113 | 114 | 将选中内容以纯文本形式复制到剪贴板。选中操作使用鼠标完成。可以支持字符选择、行选择(鼠标双击该行)、多行选择模式。 115 | 其中,多行模式下,复制会复制所有行内容,而不论起始和终止选择位置是否位于行首和行尾。 116 | 117 | 可以通过快捷键 Ctrl + C 实现同样功能。 118 | 119 | *注: 在远程ssh使用tmux作为终端时,复制到剪贴板后,只有pymud可以识别复制内容,本地剪贴板不能识别复制。* 120 | 121 | - **复制(ANSI)** 122 | 123 | 将选中内容的原始ANSI代码复制到剪贴板。在进行颜色代码判断是,需要复制原始颜色代码,该命令适用。 124 | 由于显示区间定位问题,ANSI复制建议使用整行复制或者多行复制,否则有可能复制内容不是实际需要的内容。 125 | 126 | 可以通过快捷键 Ctrl + R 实现同样功能。 127 | 128 | *注: 在远程ssh使用tmux作为终端时,复制到剪贴板后,只有pymud可以识别复制内容,本地剪贴板不能识别复制。* 129 | 130 | - **清空会话内容** 131 | 132 | 清空当前会话缓冲的 **所有** 显示内容。 133 | 134 | 当前会话缓冲的行数由settings.py中,client下的buffer_lines配置指定。该配置可以被pymud.cfg覆盖。 135 | 136 | 缓冲行数逻辑为,当已缓冲行数达到buffer_lines的两倍时,且屏幕未处于分屏状态下,会保留后buffer_lines行数内容,前面内容自动清除。 137 | 138 | - **重新加载脚本配置** 139 | 140 | 当修改过脚本文件之后,为使修改生效,可以使用该菜单操作。可以通过命令行输入#reload实现同样功能。 141 | 142 | *注:重新加载脚本文件仅在脚本文件没有语法错误的情况下会生效,若某次加载时存在语法错误,后续重新加载无法加载改正后的脚本, 143 | 需要退出pymud重新进入,或者将原错误脚本生成的中间文件.pyc文件删除后,再重新使用#load加载。* 144 | 145 | 2.2.3 帮助 146 | ^^^^^^^^^^^^^^^^^^^^^^ 147 | 148 | - **关于** 149 | 150 | 关于菜单会显示一个窗口,包含PYMUD的版本号、系统和系统版本、Python环境的版本等内容。 151 | 152 | 窗口中包含了帮助文档的地址,鼠标单击可以链接到本页面。 153 | 154 | 2.3 会话与连接管理 155 | --------------------- 156 | 157 | 可以使用以下三种方式创建会话 158 | 159 | - 使用创建新会话菜单创建,见菜单说明 160 | - 创建快捷菜单,见菜单说明 161 | - 使用 `#session `_ 命令可以创建新会话。命令使用如下: 162 | 163 | .. code:: 164 | 165 | #session {session_name} {host} {port} {encoding} 166 | 167 | 大括号内容分别代表会话名称、服务器地址、端口、编码方式(编码方式可不显式指定,此时默认为utf-8编码)。例如,使用下列命令可以创建一个名为 ``newstart`` 的会话并连接到北侠。 168 | 169 | .. code:: 170 | 171 | #session newstart mud.pkuxkx.net 8081 172 | 173 | 还可以使用 #session 命令快速调用菜单栏内容创建会话, 比如,已通过 pymud.cfg 配置好,在 pkuxkx 菜单下有一个 newstart 的子菜单项,则可以用以下命令快速创建会话 174 | 175 | .. code:: 176 | 177 | #session pkuxkx.newstart 178 | -------------------------------------------------------------------------------- /docs/source/updatehistory.md: -------------------------------------------------------------------------------- 1 | # 附录: 更新历史 2 | 3 | ## 0.21.2 (2025-06-01) 4 | 5 | + 问题修复: 修复了当自动重连启动时,即使会话关闭了,也会自动重连的问题。 6 | + 实现调整: 重写了专用的会话缓冲、记录缓冲与PyMud缓冲显示控制器,在prompt_toolkit的原Buffer和BufferControl的基础仅提供了PYMUD所需的基础功能,以降低内存占用。 7 | 经测试,当前内存基本稳定,视会话数量和脚本情况差异,维持在几百兆左右(500M以下),且不会有大幅波动。重写后,低配置的VPS也可以稳定运行PyMUD。 8 | 9 | 10 | ## 0.21.0 (2025-05-20) 11 | + 功能新增: #load / #unload 现在支持当前会话对插件的临时启用和禁用,实现方式为调用插件里的PLUGIN_SESSION_CREATE和PLUGIN_SESSION_DESTROYE函数。群文件的moving.py插件写法可以支持。 12 | + 功能调整: 各会话变量保存的.mud文件,统一移到save子目录下。原来当前目录下的.mud文件,在对应会话重新加载时会自动移动,无需人工处理。 13 | + 功能新增: 调整了enableGroup处理,可以通过组名支持子组操作,也可以指定有效类型范围。例如下面代码: 14 | 15 | ``` Python 16 | class MyTestConfig(IConfig): 17 | def __init__(self, session, *args, **kwargs): 18 | self._objs = [ 19 | Trigger(session, "tri1", group = "group1"), 20 | Trigger(session, "tri2", group = "group1.subgroup1"), 21 | Trigger(session, "tri3", group = "group1.subgroup2"), 22 | Alias(session, "alias1", group = "group1"), 23 | Alias(session, "alias2", group = "group1.subgroup1"), 24 | Timer(session, 5, group = "group1.subgroup1") 25 | ] 26 | 27 | #以下调用可以同时禁用上述6个对象,因为 group1.subgroup1 和 group1.subgroup2 都属于 group1 的子组 28 | session.enableGroup("group1", False) 29 | #以下调用可以同时仅启用触发器tri1和别名alias1,因为通过subgroup参数限定了不传递到子组 30 | session.enableGroup("group1", True, subgroup = False) 31 | # 以下调用可以同时禁用对应发器和别名,但不禁用定时器,因为通过types参数指定了有效范围: 32 | session.enableGroup("group1.subgroup1", False, types = [Trigger, Alias]) 33 | 34 | ``` 35 | 36 | + 功能新增: 增加了多处异常追踪提示。在模块或插件的脚本中发生错误时,均会打印错误追踪信息,方便定位错误。 37 | + 功能新增: 新增 #echo 命令,类似于 #test 命令,但该命令只会模拟收到服务器数据,直接激发各匹配触发器,但不显示触发测试结果。 38 | + 功能新增: 增加了国际化(i18n)支持,原生开发语言为中文简体,目前使用AI翻译生成了英文。应用语言通过Settings中新增的language配置来控制,默认为"chs",可以在pymud.cfg中覆盖该配置。其值目前可以为"chs"、"eng"。自行翻译的语言可以在pymud/lang目录下下新增语言文件,文件名为i18n_加语言代码,例如"i18n_chs.py"表示可以使用"chs"语言,其中使用Python字典方式定义了所有需动态显示的文本内容。 39 | + 功能新增: 新增了使用元类型及装饰器来管理Pymud对象,包括Alias, Trigger, Timer, GMCPTrigger四种可以使用对应的装饰器,@alias, @trigger, @timer, @gmcp来直接在标记函数上创建。可以参考本版本中的pkuxkx.py文件写法和注意事项。 40 | + 功能新增: 新增了两个装饰器,@exception和@async_exception,用于捕获异常并调用session.error进行显示。@exception用于捕获同步异常,@async_exception用于捕获异步异常。参考如下: 41 | 42 | ``` Python 43 | from pymud import Command, Trigger, IConfig, exception, async_exception 44 | 45 | class MyCustomCommand(Command, IConfig): 46 | @exception 47 | def a_sync_routine(self, args: list[str]): 48 | # 这里的代码抛出的异常会被self.session.error捕获并显示 49 | something_that_may_raise_an_exception() 50 | 51 | @async_exception 52 | async def execute(self, args: list[str]): 53 | # 这里的代码抛出的异常会被self.session.error捕获并显示 54 | await something_that_may_raise_another_exception() 55 | 56 | # 上述代码相当于以下代码 57 | class MyCustomCommand(Command, IConfig): 58 | def a_sync_routine(self, args: list[str]): 59 | try: 60 | something_that_may_raise_an_exception() 61 | except Exception as e: 62 | self.session.error(error_msg_of_e) 63 | 64 | async def execute(self, args: list[str]): 65 | try: 66 | await something_that_may_raise_another_exception() 67 | except Exception as e: 68 | self.session.error(error_msg_of_e) 69 | ``` 70 | 71 | + 问题修复: 修复了Alias和Command执行时的优先级判断。之前未进行优先级判断,因此遇到能同时匹配的多个时,不一定优先级高的被触发。现在对Alias和Command进行了优先级判断,优先级高的先触发。 72 | + 问题修复: 修复Alias中的keepEval参数和oneShot参数。keepEval参数支持多个匹配成功的别名同时生效,oneShot参数支持一个匹配成功的别名生效后,后续的匹配不再生效。 73 | + 问题修复: 修复Command中的keepEval参数。以往同时匹配生效的Command会覆盖后续Command和Alias,当前会持续匹配。 74 | + 功能增强: 对几乎所有函数的参数进行了类型标注,增加了类型检查,提高了代码的可读性和可维护性,也便于自行编写脚本时的提示。 75 | + 功能增强: 为Session类型增加了commandHistory属性,用于查询发送到服务器的命令历史。保存的命令历史的数量由pymud.cfg中的client["history_records"]控制,默认为500。当该值为0时,不会保存命令历史。为-1时,会保存所有命令历史。 76 | + 功能调整: #help命令时,增加了上下两行分隔符显示,以便明显区分帮助输出和游戏输出。 77 | + 功能增强: 当前pymud界面中显示的版本号会自动从pyproject.toml中读取,以确保版本号的准确性和唯一性。 78 | + 问题修复: 修复了代码中的部分编码错误。新版Python中能容忍一些错误,但老版本不行。经修复,当前代码支持的Python版本已测试3.8确保可用。建议使用3.10或更高版本的Python。 79 | + 问题修复: 删除了extras.py中多余的MenuItem类型定义,该定义与prompt_toolkit中的MenuItem定义冲突。 80 | + 问题修复: 调整了众多代码中未检查对象是否为None即调用、使用的局部变量可能未经过初始化和赋值路径等的情况,保证程序运行的健壮性。 81 | + 问题修复: 修复了#test命令的帮助内容错误。实际#show命令不触发脚本,仅测试;而#test会触发脚本。 82 | + 问题修复: 修复了协议处理中MSDP编码解码处理错误的问题;修复了协议处理中默认encoding不传递导致某些情况下报解码错误的问题。 83 | + 示例更新: 更新了包中自带的pkuxkx.py,增加了@alias, @trigger, @timer, @gmcp的示例以及状态窗口的示例。 84 | 85 | 86 | ## 0.20.4 (2025-03-30) 87 | + 功能调整: 为插件功能新增了 PLUGIN_PYMUD_DESTROY 方法,用于在插件被卸载时,进行一些清理工作。 88 | + 功能调整: 将插件的 PLUGIN_PYMUD_START 方法的调用,从插件加载时刻移动到事件循环启动之后,这样在加载时,可以使用 asyncio.create_task或 asyncio.ensure_future 来执行一些异步操作 89 | 90 | ## 0.20.3 (2025-03-05) 91 | + 功能调整: 为适应MacOS下的快捷键,增加Shift+左右箭头同样作为切换会话的快捷键。 92 | + 功能调整: 会话关闭和APP退出时,偶尔受网络影响导致服务器掉线但本地未检测到时会无法退出。现增加最长10s等待,超时后会中断,强制退出。 93 | 94 | ## 0.20.2 (2024-11-26) 95 | + 功能调整: MTTS协商中,将256 Color明确写入协商回复。原先仅包含ANSI 和 TrueColor。推测武庙特殊颜色偶尔不正常与此有关(已测试无关)。 96 | + 功能调整: 修复了纯文本正则处理,目前理论上支持所有ANSI控制代码的处置,以正确响应纯文本触发器。 97 | + 功能调整: 修改了#var和#global的显示实现,提高了变量打印排列的整齐度和辨识度,以适应长值变量和复杂变量。 98 | + 问题修复: 修复了单行颜色代码跨行无法显示问题。现在星宿毒草可以正常辨认颜色了。 99 | + 功能调整: 调整了info/warning/error的显示处理,默认样式进行了修改。 100 | + 功能新增: 新增菜单选项:打开/关闭美化,以便于更好的在触发器时复制出正确的内容(以前计算可能不准确)。 101 | + 功能新增: 状态栏的分隔符可以通过本地设置取消了。在pymud.cfg的client中新增设置,将 status_divider 设置为 false 即可。 102 | + 功能调整: 在pymud.cfg的client中可以支持将buffer_lines设置为0了,表示不清除缓存。 103 | + 功能新增: 为状态栏显示函数增加了异常保护,再有status_maker出错的时候,状态栏会显示出错信息。 104 | 105 | ## 0.20.1 (2024-11-16) 106 | + 功能调整: 会话中触发器匹配实现进行部分调整,减少循环次数以提高响应速度 107 | + 功能调整: #test / #show 触发器测试功能调整,现在会对使能的和未使能的触发器均进行匹配测试。其中,#show 命令仅测试,而 #test 命令会导致触发器真正响应。 108 | + 功能新增: pymud对象新增了一个持续运行的1s的周期定时任务。该任务中会刷新页面显示。可以使用 session.application.addTimerTickCallback 和 session.application.removeTimerTickCallback 来注册和解除定时器回调。 109 | 110 | ## 0.20.0 (2024-08-25) 111 | + 功能调整: 将模块主入口函数从__main__.py中移动到main.py中,以使可以在当前目录下,可直接使用pymud,也可使用python -m pymud启动 112 | + 功能调整: 使用argsparser标准模块来配置命令行,可以使用 pymud -h 查看命令行具体参数及说明 113 | + 功能新增: 命令行参数增加指定启动目录的功能,参数为 -s, --startup_dir。即可以从任意目录通过指定脚本目录方式启动PyMUD了。 114 | - 例如, PS C:\> pymud -s d:\prog\pkuxkx 相当于 PS D:\prog\pkuxk> pymud 115 | + 问题修复: MacOS下 python -m pymud init 创建目录报错的问题。同时,将所有系统上的默认目录均使用 ~/pkuxkx (影响windows) 116 | + 功能调整: 恢复在__init__.py中增加PyMudApp的导出,可以恢复使用from pymud import PyMudApp了 117 | + 功能新增: 增加log功能,详见 #log 命令介绍、类参考中的 Logger 类,以及 Session 类的 handle_log 方法 118 | + 功能新增: 增加 #disconnect, #dis 命令,可以使当前会话从服务器断开。相当于操作菜单 会话->断开连接 119 | + 功能调整: 在没有session的时候,也可以执行#exit命令 120 | + 功能新增: #session 命令增加快捷创建会话功能,假如已有快捷菜单 世界->pkuxkx->newstart , 则可以通过 #session pkuxkx.newstart 直接创建该会话,效果等同于点击该菜单 121 | + 功能调整: 点击菜单创建会话时,若会话已存在,则将该会话切换为当前会话 122 | + 重大更新: 完全重写了模块的加载、卸载、重新加载方法,修复模块使用中的问题 123 | + 功能调整: 现在只要将一个类型继承 IConfig 接口,即被识别为配置类型。这种类型在模块加载时会自动创建其实例。当然,名称为Configuration的类型也同样被认为是配置类型,保持向前兼容性。唯一要求是,该类型的构造函数允许仅传递一个session对象。 124 | + 功能新增: 各类配置类型的卸载现在既可以定义在__unload__方法中,也可以定义在unload方法中。可以根据自己喜好选择一个即可。 125 | + 功能调整: 各配置类型加载和重新加载前,会自动调用模块的__unload__方法或unload方法(若有) 126 | + 功能新增: Command基类增加__unload__方法和unload方法,二者在从会话中移除该 Command 时均会自动调用。自定义的Command子类应覆盖这两种方法中的一种方法,并在其中增加清除类型自行创建的 Trigger, Alias 等会话对象。这样,模块卸载时只要移除命令本身,在命令中新建的其他关联对象将被一同移除。 127 | + 功能新增: 所有PyMUD基础对象类型及其子类型,包括 Alias, Trigger, Timer, Command, GMCPTrigger 及它们的子类型,在创建的时候会自动添加到会话中,无需再进行 addObject 等操作了 128 | + 问题修复: 修复部分正则表达式书写错误问题 129 | + 功能新增: Session类新增waitfor函数,用于执行一段代码后立即等待某个触发器的情况,简化原三行代码写法 130 | 131 | ``` Python 132 | # 原来为确保await triggered的任务在输入前等待,有时候需要这么写: 133 | task = self.create_task(self.tri1.triggered()) 134 | await asyncio.sleep(0.05) 135 | self.session.writeline('dazuo') 136 | await task 137 | 138 | # 现在可以一句话简写: 139 | await self.session.waitfor('dazuo', self.create_task(self.tri1.triggered())) 140 | ``` 141 | 142 | + 功能调整: Session类的addTriggers等方法接受的dict中,会将对象本身id作为会话处理id。当该id与key不一致时,会同时显示警告。 143 | + 功能新增: Session类新增addObject, addObjects, delObject, delObjects用于操作别名、定时器、触发器、GMCP触发器、命令等对象。 144 | - 使用示例: 145 | 146 | ```Python 147 | # 所有对象均可以使用 delObject 直接从会话中移除,会自动根据对象类型推断,无需通过函数名区分 148 | session.delObject(self.tri1) 149 | session.delObject(self.ali1) 150 | session.delObject(self.timer1) 151 | 152 | objs = [ 153 | Trigger(session, xxx, xxx), 154 | Alias(session, xxx), 155 | SimpleCommand(session, xxx), 156 | Timer(session, xxx), 157 | GMCPTrigger(session, xxx) 158 | ] 159 | 160 | session.delObjects(objs) # 可以直接从会话中移除一个数组中的所有对象,会自动判断对象类别 161 | ``` 162 | 163 | + 功能新增: Session类型新增idletime属性,可以获取本会话发呆秒数(float类型)。当会话处于未连接状态时,返回 -1。可以利用定时器,在其中检测 idletime 值,以在机器人出错后处理恢复 164 | + 功能新增: Session的所有异步命令调用函数增加返回值,现在调用 session.exec_async, exec_command_async 等方法执行的内容若匹配为命令时,会返回最后最后一个 Command 对象的 execute 函数的返回值 165 | - 例如, result = await self.session.cmds.cmd_runto.execute('rt yz') 与 result = await self.session.exec_async('rt yz') 等价,返回值相同 166 | - 但 result = await self.session.exec_async('rt yz;dzt'),该返回的result 仅是 dzt 命令的 execute 的返回值。 rt yz 命令返回值被丢弃。 167 | + 功能新增: 增加临时变量概念,变量名以下划线开头的为临时变量,此类变量不会被保存到 .mud 文件中。 168 | + 功能新增: 为 BaseObject 基类的 self.session 增加了 Session 类型限定,现在自定义 Command 等时候,使用 self.session 时会有 IntelliSence 函数智能提示了,所有帮助说明已补全 169 | + 问题修复: 修复 #var 等命令中,若含有中文则等号位置不对齐的问题 170 | + 功能调整: 在 #tri 等命令中,当对象的 group 为空时,将不再显示 group 属性,减少无用信息 171 | 172 | ## 0.19.4 (2024-04-20) 173 | + 功能调整: info 现在 msg 恢复为可接受任何类型参数,不一定是 str 174 | + 功能调整: #var, #global 指令中,现在可以使用参数扩展了,例如 #var max_qi @qi 175 | + 功能调整: #var, #global 指令中,现在对字符串会先使用 eval 转换类型,转换失败时使用 str 类型。例如, #var myvar 1 时,myvar类型将为int 176 | + 功能调整: 变量替代时,会自动实现类型转化,当被替代变量值为非 str 类型时不会再报错 177 | + 问题修复: 修复之前从后向前选择时,无法复制的问题 178 | 179 | ## 0.19.3post2 (2024-04-05) 180 | + 问题修复: 一次发送多个命令时,发送顺序可能不正确的情况 181 | + 功能增加: 新增一个exec_async函数,是exec函数的异步形式。可以在其他会话中异步执行一段代码 182 | + 帮助完善: 帮助文档逻辑完善,已完成整个包的内置文档的编写和修改 183 | + 注: 由于我没弄太明白 readthedocs.io 网站对于读取github源代码的逻辑,目前只能通过新发布正式版本的形式来使 readthedocs.io 网站的文档中的类参考自动更新。 184 | + 问题修复: 修复退出程序时的小bug 185 | 186 | ## 0.19.2post2 (2024-03-24) 187 | + 错误修复:订正部分错别字、错误帮助、错别格式 188 | + 系统完善:完善帮助体系,按reST格式重写所有有关的docstring 189 | + 功能调整:session.exec_command / exec_command_async / exec 系列命令调整,现在可以在exec时带变量参数了。例如 session.exec("dazuo @dzpt"),直接调用 dzpt的变量值 190 | + 功能调整: settings.py中,client字典增加配置reconnect_wait,为自动重连的等待时间,默认15s,可本地覆盖 191 | + 功能调整: 变通解决了菜单栏右边单击 帮助 菜单会响应问题 192 | + 错误修复: 修复了会话关闭时插件卸载的代码错误 193 | + 功能调整: 在会话关闭、程序退出时增加等待,确保收到服务器断开命令之后才关闭和退出 194 | + 问题修复: 在退出程序时增加了插件卸载调用 195 | + 实现调整: 在清除task的列表推导过程中去掉了类型判断以减少任务时间占用 196 | + 其他调整: 从包中删除了拷贝过来作为参考的文件 197 | + 帮助完善: 帮助文档逻辑完善 198 | + 实现调整: 改用官方示例的task清除方式,每个任务结束后清除 199 | 200 | ## 0.19.1 (2024-03-06) 201 | + 功能新增: 新增鼠标启用禁用功能,以适用于ssh远程情况下的复制功能。F2快捷键可以切换状态。当鼠标禁用时,底部状态栏右侧会显示“鼠标已禁用状态” 202 | + 功能新增: 新增快捷键F1会直接通过浏览器打开帮助网址 https://pymud.readthedocs.io/ 203 | + 功能新增: 新增默认快捷键F3=#ig, F4=#cls, F11=#close, F12=#exit。此几个快捷键通过配置文件进行配置,可以自行定义或修改。F1、F2为写死的系统功能。 204 | + 功能调整: 将除#session之外的所有其他#命令实现统一到Session类中实现,这些命令均支持通过Session.exec_command运行 205 | + 功能调整: python -m pymud init时,创建的pymud.cfg文件增加了keys字典 206 | 207 | ## 0.19.0 (2024-03-01) 208 | + 实现调整: session.info/warning/error处理多行时,会给每一行加上同样颜色 209 | + 功能新增: 初次运行时,可以使用python -m pymud init来初始化环境,自动创建目录并在该目录中建立配置文件和样例脚本文件 210 | + 实现调整: 将缓冲清除行数的实现调整到SessionBuffer中,减少代码耦合并进一步降低内存占用 211 | + 功能新增: 新增命令行命令#T+, #T-, 可以使能/禁用指定组,相当于session.enableGroup操作 212 | + 功能新增: 新增命令行命令#task,可以列出所有系统管理的Task清单,主要用于开发测试 213 | + 实现调整: 调整系统管理Task的清空和退出机制,减少处理时间占用和内存占用 214 | + 实现调整: 调整COPY-RAW模式复制,即使仅选中行中的部分内容,也自动识别整行(多行模式也是整个多行) 215 | + 功能新增: Settings中新增keys字典,用于定义快捷键。可定义快捷键参见prompt_toolkit中Keys的定义。其值为可在session.exec_command运行支持的所有内容。该字典内容可以被pymud.cfg所覆盖。 216 | 217 | ## 0.18.4post4 (2024-02-23) 218 | + 功能新增:新增Settings.client["buffer_lines"],表示保留的缓冲行数(默认5000)。当Session内容缓冲行数达到该值2倍时(10000行),将截取一半(5000行),后一半内容进行保留,前一半丢弃。此功能是为了减少长时挂机的内存消耗和响应时间。 219 | + 功能修复:解决在显示美化(Settings.client["beautify"])打开之后,复制部分文字不能正确判断起始终止的问题。 220 | + 功能调整:修改缓冲行数判断逻辑,加快客户端判断响应速度。 221 | + 问题调整:修改缓冲截取处理中的小BUG。 222 | + 功能调整:将帮助窗口中的链接改到帮助网址: https://pymud.readthedocs.org 223 | + 问题修复:修复了随包提供的pkuxkx.py样例脚本中的几处错误 224 | 225 | ## 0.18.3 (2024-02-07) 226 | + 功能调整:原#unload时通过调用__del__来实现卸载的时间不可控,现将模块卸载改为调用unload函数。若需卸载时人工清除有关定时器、触发器等,请在Configuration类下新增unload函数(参数仅self),并在其中进行实现 227 | + 功能新增:新增会话Variable和全局Global的删除接口。可以通过session.delVariable(name)删除一个变量,可以通过session.delGlobal(name)来删除一个全局Global变量 228 | 229 | ## 0.18.2 (2024-02-06) 230 | + 问题修复:修改了定时器实现,以避免出现递归调用超限异常 231 | + 问题修复:修改了参数替代时的默认值,从None改为字符串"None",以避免替代时报None异常 232 | 233 | ## 0.18.1 (2024-02-05) 234 | + 问题修复:统一处置了task.cancel的参数和create_task的name属性,以适应更低版本的python环境(低至3.8) 235 | + 实现调整:为解决同步/异步执行问题,在CodeLine和CodeBlock的实现中,会通过调用命令来判断是否使用同步模式(默认为异步)。#gag、#replace为强制同步,#wa为强制异步。当同时存在时,同步失效,异步执行。 236 | + 实现调整:将%line、%raw的访问传递到触发器内部的执行中,避免同步异步问题。 237 | + 新增文档:将帮助文档添加到本项目,帮助文档自动同步到 pymud.readthedocs.org (文档内容暂未更新) 238 | 239 | ## 0.18.0 (2024-01-24) 240 | + 问题修复:修复了delTrigger/delAlias等等无法删除对象的问题 241 | + 功能调整:delTrigger等函数,修改为既可以接受Trigger对象本身,也可以接受其id。其他类似 242 | + 功能增加:增加了delTriggers(注意,带s)等函数,可以删除多个指定对象。可接受列表、元组等可迭代对象,并且其内容既可以为对象本身,也可以为id。 243 | + 功能增加:增加了session.reset()功能,可清除会话所有有关脚本信息。也可以在命令行使用#reset调用,另外,#unload不带参数调用时,有同样效果 244 | + 功能增加:增加了#ignore/#ig参数,类似于zmud的#ignore功能,可以切换全局触发器禁用状态。当全局被禁用时,底部状态栏右侧会显示此状态。(未全局禁用时不显示) 245 | + 功能调整:移除了会话切换时,状态栏显示的内容 246 | + 功能调整:会话命令的执行整体进行了实现调整,将参数替代延迟到特定命令执行时刻。(此实现影响面较大,请大家使用中发现BUG时都报告下,我及时修改) 247 | + 功能调整:代码块现在可以使用{}括起来了。这种情况下,命令和命令可以嵌套了。例如,#3 {#3 get g1b from bo yu;combine gem;pack gem;#wa 3000},该代码可以执行三次合并g1b宝石 248 | + 功能新增:增加了#ali,#tri,#ti的三参数使用,可以在命令行直接代码创建SimpleAlias, SimpleTrigger和SimpleTimer。 249 | + 使用示例:#ali {gp\s(\S+)} {get %1 from corpse}, #tri {^[> ]*【\S+】.+} {#mess %line}, #ti 10 {xixi;haha} 250 | + 功能新增:新增#session_name cmd命令,可以直接使名为session_name的会话执行cmd命令 251 | + 功能新增:session类型新增exec方法,使用方法为:session.exec(cmd, session_name)。可以使名为session_name的会话执行cmd命令。当不指定session_name时,在当前会话执行。 252 | + 功能调整:定时器创建时若不指定id,其自动生成的id前缀由tmr调整为ti 253 | + 实现调整:将#all、#session_name cmd等命令的实现从pymud.py中移动到了session.py中,这样可以在脚本中使用session.exec_command("#all xixi")。 254 | + 问题修复:修复了点击菜单"重新加载脚本配置"报错的问题 255 | + 功能调整:从菜单里点击创建会话时,会自动以登录名为本会话创建id变量 256 | + 当前已知问题:由于同步/异步执行问题,在SimpleTrigger中,#gag和#replace的执行结果会很奇怪,可能会隐藏和替换掉非触发行。可行的办法为在onSuccess里,调用session.replace进行处理。 257 | 258 | ## 0.17.4 (2024-01-08) 259 | + 问题修复:修复了DotDict在dump时出现错误的问题 260 | + 问题修复:修改了reconnect的实现方式,修复了断开重连时报错的问题 261 | + 功能增加:为Session增加两个事件属性,分别为event_connected和event_disconnected,接受一个带有session参数的函数,在连接和连接断开时触发。 262 | + 功能调整:调整了时间显示格式,只显示到秒,不显示毫秒数。 263 | 264 | ## 0.17.3 (2024-01-02) 265 | + 问题修复:修复了原有的#repeat功能。命令行#repeat/#rep可以重复输入上一次命令(这个基本没用,主要是我在远程连接时,手机上没有方向键...) 266 | + 问题修复:修改定时器的实现方式,真正修复了定时器每reload后会新增一个的bug。 267 | + 功能增加:命令行使用#tri, #ali, #cmd, #ti时,除了接受on/off参数外,增加了del参数,可以删除对应的触发器、别名、命令、定时器。例如:#ti tm_test del 可以删除id为“tm_test”的定时器。 268 | + 功能调整:调整了#help {cmd}的显示格式,最后一行也增加了换行符,确保后续数据在下一行出现。 269 | + 功能调整:调整了Timer和SimpleTimer在#timer时的显示格式。 270 | + 实现调整:调整了Session.clean实现中各对象清理的顺序,将任务清除移到了最后。 271 | 272 | ## 0.17.2post4 (2023-12-29) 273 | + 功能修改:会话菜单 "显示/隐藏命令" 和 "打开/关闭自动重连" 操作后,增加在当前会话中提示状态信息。 274 | + 功能修改:Timer实现进行修改,以确保一个定时器仅创建一个任务。 275 | + 功能调整:Timer对象在复位Session对象时,也同时复位。目的是确保reload时不重新创建定时器任务。 276 | + 功能调整:在会话连接时,不再复位Session有关对象信息。该复位活动仅在连接断开时和脚本重新加载时进行。 277 | + 功能调整:启动PYMUD时,会将控制台标题设置为PYMUD+版本号。 278 | + 问题修复:修复会话特定脚本模块会被其他会话加载的bug。 279 | + 问题修复:修复定时器Timer中的bug。 280 | 281 | ## 0.17.1post1 (2023-12-27) 282 | 本版对模块功能进行了整体调整,支持加载/卸载/重载/预加载多个模块,具体内容如下: 283 | + 当模块中存在名为Configuration类时,以主模块形式加载,即:自动创建该Configuration类的实例(与原脚本相同) 284 | + 当模块中不存在名为Configuration类时,以子模块形式加载,即:仅加载该模块,但不会创建Configuration的实例 285 | + #load命令支持同时加载多个模块,模块名以半角逗号(,)隔开即可。此时按给定的名称顺序逐一加载。如:#load mod1,mod2 286 | + 增加#unload命令,卸载卸载名称模块,同时卸载多个模块时,模块名以半角逗号(,)隔开即可。卸载时,如果该模块有Configuration类,会自动调用其__del__方法 287 | + 修改reload命令功能,当不带参数时,重新加载所有已加载模块,带参数时,首先尝试重新加载指定名称模块,若模块中不存在该名称模块,则重新加载指定名称的插件。若存在同名模块和插件,则仅重新加载插件(建议不要让插件和模块同名) 288 | + 增加#modules(简写为#mods)命令,可以列出所有已经加载的模块清单 289 | + Session类新增load_module方法,可以在脚本中调用以加载给定名称的模块。该方法接受1个参数,可以使用元组/列表形式指定多个模块,也可以使用字符串指定单个模块 290 | + Session类新增unload_module方法,可以在脚本中调用以卸载给定名称的模块。参数与load_module类似。 291 | + Session类新增reload_module方法,可以在脚本中调用以重新加载给定名称的模块。当不指定参数时,重新加载所有模块。当指定1个参数时,与load_module和unload_module方法类似 292 | + 修改Settings.py和本地pymud.cfg文件中sessions块脚本的定义的可接受值。默认加载脚本default_script现可接受字符串和列表以支持多个模块加载。多个模块加载有两种形式,既可以用列表形式指定多个,如["script1","script2"],也可以用字符串以逗号隔开指定多个,如"script1,script2" 293 | + 修改Settings.py和本地pymud.cfg文件中sessions块脚本中chars指定的会话菜单参数。当前,菜单后面的列表参数可以支持额外增加第3个对象,其中第3个为该会话特定需要加载的模块。该参数也可以使用逗号分隔或者列表形式。 294 | + 当创建会话时,自动加载的模块会首先加载default_script中指定的模块名称,然后再加载chars中指定的模块名称。 295 | + 上述所有修改均向下兼容,不影响原脚本使用。 296 | + 一个新的修改后的pymud.cfg示例如下 297 | ``` 298 | { 299 | "sessions": { 300 | "pkuxkx" : { 301 | "host" : "mud.pkuxkx.net", 302 | "port" : "8081", 303 | "encoding" : "utf8", 304 | "autologin" : "{0};{1}", 305 | "default_script": ["pkuxkx.common", "pkuxkx.commands", "pkuxkx.main"], 306 | "chars" : { 307 | "char1": ["yourid1", "yourpassword1"], 308 | "char2": ["yourid2", "yourpassword2", "pkuxkx.wudang"], 309 | "char3": ["yourid3", "yourpassword3", "pkuxkx.wudang,pkuxkx.lingwu"], 310 | "char4": ["yourid4", "yourpassword4", ["pkuxkx.shaolin","pkuxkx.lingwu"]] 311 | } 312 | } 313 | } 314 | } 315 | ``` 316 | 317 | + 问题修复:修复enableGroup中定时器处的bug 318 | + 功能修改:会话连接和重新连接时,取消原定时器停止的设定,目前保留为只清除所有task、复位Command 319 | + 功能修改:auto_reconnect设定目前对正常/异常断开均有效。若设置为True,当连接断开后15s后自动重连 320 | + 功能修改:会话菜单下增加“打开/关闭自动重连”子菜单,可以动态切换自动重连是否打开。 321 | 322 | ## 0.17.0 (2023-12-24) 323 | + 功能修改:调整修改GMCP数据的wildcards处理方式,恢复为eval,其余不变。(回滚0.16.2版更改) 324 | + 功能修改:将本地pymud.cfg文件的读取默认编码调整为utf8,以避免加载出现问题 325 | + 问题修复:sessions.py中,修复系统command与会话command重名的问题(这次才发现) 326 | + 功能修改:将自动脚本加载调整到session创建初始,而不论是否连接服务器 327 | + 功能修改:脚本load和reload时,不再清空任何对象,保留内容包括:中止并清空所有task,关闭所有定时器,将所有异步对象复位 328 | + 功能修改:去掉了左右边框 329 | + 问题修复:修复了当使用session.addCommand/addTrigger/addAlias等添加对象,而对象是Command/Trigger/Alias等的子类时,由于类型检查失败导致无法成功的问题 330 | + 功能修改:增加自动重连配置,Settings.client["auto_reconnect"]配置,当为True时,若连接过程中出现异常断开,则10秒后自动重连。该配置默认为False。 331 | + 功能修改:当连接过程中出现异常时,异常提示中增加异常时刻。 332 | + 功能修改:#reload指令增加可以重新加载插件功能。例如,#reload chathook会重新加载名为chathook的插件。 333 | + 功能增加:增加#py指令,可以直接在命令行中写代码并执行。执行的上下文环境为当前环境,即self代表当前session。例如,#py self.writeline("xixi")相当于直接在脚本会话中调用发送xixi指令 334 | + 功能新增:新增插件(Plugins)功能。将自动读取pymud模块目录的plugins子目录以及当前脚本目录的plugins子目录下的.py文件,若发现遵照插件规范脚本,将自动加载该模块到pymud。可以使用#plugins查看所有被加载的插件,可以直接带参数插件名(如#plugins myplugin)查看插件的详细信息(自动打印插件的__doc__属性,即写在文件最前面的字符串常量)插件文件中必须有以下定义: 335 | 336 | |名称|类型|状态|含义| 337 | |-|-|-|-| 338 | |PLUGIN_NAME|str|必须有|插件唯一名称| 339 | |PLUGIN_DESC|dict|必须有|插件描述信息的详情,必要关键字包含VERSION(版本)、AUTHOR(作者)、RELEASE_DATE(发布日期)、DESCRIPTION(简要描述)| 340 | |PLUGIN_PYMUD_START|func(app)|函数定义必须有,函数体可以为空|PYMUD自动读取并加载插件时自动调用的函数, app为PyMudApp(pymud管理类)。该函数仅会在程序运行时,自动加载一次| 341 | |PLUGIN_SESSION_CREATE|func(session)|函数定义必须有,函数体可以为空|在会话中加载插件时自动调用的函数, session为加载插件的会话。该函数在每一个会话创建时均被自动加载一次| 342 | |PLUGIN_SESSION_DESTROY|func(session)|函数定义必须有,函数体可以为空|在会话中卸载插件时自动调用的函数, session为卸载插件的会话。卸载在每一个会话关闭时均被自动运行一次。| 343 | 344 | + 功能修改:对session自动加载mud文件中变量失败时的异常进行管理,此时将不加载变量,自动继续进行 345 | + 功能修改:所有匹配类对象的匹配模式patterns支持动态修改,涉及Alias,Trigger,Command。修改方式为直接对其patterns属性赋值。如tri.patterns = aNewPattern 346 | + 功能修改:连接/断开连接时刻都会在提示中增加时刻信息,而不论是否异常。 347 | 348 | ## 0.16.2 (2023-12-19) 349 | + 功能修改:归一化#命令和非#命令处理,使session.exec_command、exec_command_async、exec_command_after均可以处理#命令,例如session.exec_command("#save")。同时,也可以在命令行使用#all发送#命令,如"#all #save"此类 350 | + 功能修改:调整脚本加载与变量自动加载的顺序。当前为连接自动加载时,首先加载变量,然后再加载脚本。目的是使脚本的变化可以覆盖加载的变量内容,而不是反向覆盖。 351 | + 功能修改:会话变量保存和加载可以配置是否打开,默认为打开。见Settings.client["var_autosave] 和 Settings.client["var_autoload"]。同理,该配置可以被本地pymud.cfg所覆盖 352 | + 功能修改:将MatchObject的同步onSuccess和异步await的执行顺序进行调整,以确保一定是同步onSuccess先执行。涉及Trigger、Command等。 353 | + 功能修改:修改了GMCPTrigger的onSuccess处置和await triggered处置参数,以保持与Trigger同步。当前,onSuccess函数传递3个参数,name,line(GMCP收到的原始str数据),wildcards(经eval处理的GMCP数据,大概率是dict,偶尔也可能eval失败,返回与line相同值)。await triggered返回与Triggerd的await triggered相同,均为BaseObject.State,包含4个参数的元组,result(永为True),name(GMCP的id),line(GMCP原始数据),wildcards(GMCP处理后数据)。其中,后3个参数与onSuccess函数调用时传递参数相同。 354 | + 功能修改:增加GMCP默认处理。当未使用GMCPTrigger对对应的GMCP消息进行处理时,默认使用[GMCP] name: value的形式输出GMCP收到的消息,以便于个人脚本调试。 355 | + 功能修改:修改GMCP数据的处理方式从eval修改为json.load,其余不变。 356 | 357 | ## 0.16.1.post2 (2023-12-12) 358 | + 问题修复:修复__init__.py中的__all__变量为字符串 359 | + 功能增加:可以加载自定义Settings。在执行python -m pymud时,会自动从当前目录读取pymud.cfg文件。使用json格式将配置信息写在该文件中即可。支持模块中settings.py里的sessions, client, server, styles, text字段内容。 360 | + 功能增加:增加全局变量集,可以使用session.setGlobal和session.getGlobal进行访问,以便于跨session通信。也可以使用#global在命令行访问 361 | + 功能增加:增加变量的持久化,持久化文件保存于当前目录,文件名为session名称.mud,该文件在会话初始化时自动读取,会话断开时自动保存,其他时候使用#save保存。 362 | + 功能增加:在extras.py中增加DotDict,用于支持字典的.访问方式 363 | + 功能增加:使用DotDict增加了session有关对象的点访问方式(.)的快捷访问,包括变量vars,全局变量globals,触发器tris,别名alis,命令cmds,定时器timers,gmcp。例如:session.vars.charname,相当于session.getVariable('charname') 364 | + 功能增加:增加#all命令,可以向当前所有活动会话发送同一消息,例如#all xixi,可以使所有连接的会话都发送emote 365 | + 功能增加:增加%copy系统变量,当复制后,会将复制内容赋值给%copy变量 366 | + 功能增加:增加Trigger测试功能,使用#test {msg}在命令行输入后,会如同接收到服务端数据一样引发触发反应,并且会使用[PYMUD TRIGGER TEST]进行信息显示。 367 | + 功能增加:匹配#test命令和%copy变量使用如下:窗体中复制有关行,然后命令行中输入#test %copy可使用复制的行来测试触发器 368 | + 功能修改:将原CodeBlock修改为CodeBlock和CodeLine组成,以适应新的#test命令 369 | + 功能修改:session对命令的输入异步处理函数handle_input_async进行微小调整,以适应#test命令使用 370 | + 功能修改:退出时未断开session时的提示窗口文字改为红色(原黄色对比度问题,看不清楚) 371 | + 功能修改:恢复了#help功能,可以在任意会话中使用#help列出所有帮助主题,#help {topic}可以查看主题详情 372 | + 功能修改:在#reload重新加载脚本时,保留变量数据 373 | + 问题修复:修复版本显示,更正为0.16.1(原0.16.0) 374 | + 问题修复:发布日期标志修改为当前时间 375 | + 功能修改:CodeLine的执行运行处理修改为不删除中间的多余空白 376 | + 问题修复:修改github项目地址为原pymud地址 377 | 378 | ## 0.15.8 (2023-12-05) 379 | 首次发布到pip,增加模块使用 -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | 3 | name = "pymud" # Required 4 | version = "0.21.2" # Required 5 | description = "a MUD Client written in Python" # Optional 6 | readme = "README.md" # Optional 7 | requires-python = ">=3.8" 8 | # license = {file = "LICENSE.txt"} 9 | license = "GPL-3.0-or-later" 10 | license-files = ["LICEN[CS]E*"] 11 | 12 | keywords = ["MUD", "multi-user dungeon", "client"] # Optional 13 | authors = [ 14 | {name = "newstart@pkuxkx", email = "crapex@hotmail.com" } # Optional 15 | ] 16 | maintainers = [ 17 | {name = "newstart@pkuxkx", email = "crapex@hotmail.com" } # Optional 18 | ] 19 | 20 | classifiers = [ # Optional 21 | # How mature is this project? Common values are 22 | # 3 - Alpha 23 | # 4 - Beta 24 | # 5 - Production/Stable 25 | "Development Status :: 5 - Production/Stable", 26 | #"Development Status :: 3 - Alpha", 27 | "Intended Audience :: End Users/Desktop", 28 | "Topic :: Games/Entertainment :: Multi-User Dungeons (MUD)", 29 | # "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 30 | "Programming Language :: Python :: 3", 31 | "Programming Language :: Python :: 3.8", 32 | "Programming Language :: Python :: 3.9", 33 | "Programming Language :: Python :: 3.10", 34 | "Programming Language :: Python :: 3.11", 35 | "Programming Language :: Python :: 3.12", 36 | "Programming Language :: Python :: 3.13", 37 | "Programming Language :: Python :: 3 :: Only", 38 | ] 39 | 40 | dependencies = [ 41 | "wcwidth", 42 | "pyperclip", 43 | "pygments", 44 | "prompt-toolkit>=3.0.51", 45 | ] 46 | 47 | [project.urls] # Optional 48 | "Homepage" = "https://github.com/crapex/pymud/" 49 | "Bug Reports" = "https://github.com/crapex/pymud/issues" 50 | "Source" = "https://github.com/crapex/pymud/" 51 | "document" = "https://pymud.readthedocs.io/" 52 | 53 | [project.scripts] # Optional 54 | pymud = "pymud:main" 55 | 56 | # This is configuration specific to the `setuptools` build backend. 57 | # If you are using a different build backend, you will need to change this. 58 | [tool.setuptools] 59 | # If there are data files included in your packages that need to be 60 | # installed, specify them here. 61 | # package-data = {"sample" = ["*.dat"]} 62 | 63 | [build-system] 64 | # These are the assumed default build requirements from pip: 65 | # https://pip.pypa.io/en/stable/reference/pip/#pep-517-and-518-support 66 | requires = ["setuptools>=43.0.0", "wheel"] 67 | build-backend = "setuptools.build_meta" 68 | -------------------------------------------------------------------------------- /src/pymud/__init__.py: -------------------------------------------------------------------------------- 1 | from .settings import Settings 2 | from .pymud import PyMudApp 3 | from .modules import IConfigBase, IConfig, PymudMeta 4 | from .objects import CodeBlock, Alias, SimpleAlias, Trigger, SimpleTrigger, Command, SimpleCommand, Timer, SimpleTimer, GMCPTrigger 5 | from .extras import DotDict 6 | from .session import Session 7 | from .logger import Logger 8 | from .main import main 9 | from .decorators import exception, async_exception, PymudDecorator, alias, trigger, timer, gmcp 10 | 11 | __all__ = [ 12 | "PymudMeta", "IConfigBase", "IConfig", "PyMudApp", "Settings", "CodeBlock", 13 | "Alias", "SimpleAlias", "Trigger", "SimpleTrigger", 14 | "Command", "SimpleCommand", "Timer", "SimpleTimer", 15 | "GMCPTrigger", "Session", "DotDict", "Logger", "main", 16 | "exception", "async_exception", "PymudDecorator", "alias", "trigger", "timer", "gmcp", 17 | ] -------------------------------------------------------------------------------- /src/pymud/__main__.py: -------------------------------------------------------------------------------- 1 | from .main import main 2 | 3 | if __name__ == "__main__": 4 | main() -------------------------------------------------------------------------------- /src/pymud/decorators.py: -------------------------------------------------------------------------------- 1 | import sys, functools, traceback 2 | from typing import Union, Optional, List 3 | 4 | def print_exception(session, e: Exception): 5 | """打印异常信息""" 6 | from .settings import Settings 7 | from .session import Session 8 | if isinstance(session, Session): 9 | # tb = sys.exc_info()[2] 10 | # frames = traceback.extract_tb(tb) 11 | # frame = frames[-1] 12 | # session.error(Settings.gettext("exception_traceback", frame.filename, frame.lineno, frame.name), Settings.gettext("script_error")) 13 | # if frame.line: 14 | # session.error(f" {frame.line}", Settings.gettext("script_error")) 15 | 16 | # session.error(Settings.gettext("exception_message", type(e).__name__, e), Settings.gettext("script_error")) 17 | # session.error("===========================", Settings.gettext("script_error")) 18 | session.error(traceback.format_exc(), Settings.gettext("script_error")) 19 | 20 | def exception(func): 21 | """方法异常处理装饰器,捕获异常后通过会话的session.error打印相关信息""" 22 | @functools.wraps(func) 23 | def wrapper(self, *args, **kwargs): 24 | from .objects import BaseObject 25 | from .modules import ModuleInfo, IConfig 26 | from .session import Session 27 | from .settings import Settings 28 | try: 29 | return func(self, *args, **kwargs) 30 | except Exception as e: 31 | # 调用类的错误处理方法 32 | if isinstance(self, Session): 33 | session = self 34 | elif isinstance(self, (BaseObject, IConfig, ModuleInfo)): 35 | session = self.session 36 | else: 37 | session = None 38 | 39 | if isinstance(session, Session): 40 | print_exception(session, e) 41 | #session.error(Settings.gettext("exception_message", e, type(e))) 42 | #session.error(Settings.gettext("exception_traceback", traceback.format_exc())) 43 | else: 44 | raise # 当没有会话时,选择重新抛出异常 45 | return wrapper 46 | 47 | def async_exception(func): 48 | """异步方法异常处理装饰器,捕获异常后通过会话的session.error打印相关信息""" 49 | @functools.wraps(func) 50 | async def wrapper(self, *args, **kwargs): 51 | from .objects import BaseObject 52 | from .modules import ModuleInfo, IConfig 53 | from .session import Session 54 | from .settings import Settings 55 | try: 56 | return await func(self, *args, **kwargs) 57 | except Exception as e: 58 | if isinstance(self, Session): 59 | session = self 60 | elif isinstance(self, (BaseObject, IConfig, ModuleInfo)): 61 | session = self.session 62 | else: 63 | session = None 64 | 65 | if isinstance(session, Session): 66 | print_exception(session, e) 67 | 68 | else: 69 | raise # 当没有会话时,选择重新抛出异常 70 | return wrapper 71 | 72 | 73 | class PymudDecorator: 74 | """ 75 | 装饰器基类。使用装饰器可以快捷创建各类Pymud基础对象。 76 | 77 | :param type: 装饰器的类型,用于区分不同的装饰器,为字符串类型。 78 | :param args: 可变位置参数,用于传递额外的参数。 79 | :param kwargs: 可变关键字参数,用于传递额外的键值对参数。 80 | """ 81 | def __init__(self, type: str, *args, **kwargs): 82 | self.type = type 83 | self.args = args 84 | self.kwargs = kwargs 85 | 86 | def __call__(self, func): 87 | decos = getattr(func, "__pymud_decorators__", []) 88 | decos.append(self) 89 | func.__pymud_decorators__ = decos 90 | 91 | return func 92 | 93 | def __repr__(self): 94 | return f"<{self.__class__.__name__} type = {self.type} args = {self.args} kwargs = {self.kwargs}>" 95 | 96 | 97 | def alias( 98 | patterns: Union[List[str], str], 99 | id: Optional[str] = None, 100 | group: str = "", 101 | enabled: bool = True, 102 | ignoreCase: bool = False, 103 | isRegExp: bool = True, 104 | keepEval: bool = False, 105 | expandVar: bool = True, 106 | priority: int = 100, 107 | oneShot: bool = False, 108 | *args, **kwargs): 109 | """ 110 | 使用装饰器创建一个别名(Alias)对象。被装饰的函数将在别名成功匹配时调用。 111 | 被装饰的函数,除第一个参数为类实例本身self之外,另外包括id, line, wildcards三个参数。 112 | 其中id为别名对象的唯一标识符,line为匹配的文本行,wildcards为匹配的通配符列表。 113 | 114 | :param patterns: 别名匹配的模式。 115 | :param id: 别名对象的唯一标识符,不指定时将自动生成唯一标识符。 116 | :param group: 别名所属的组名,默认为空字符串。 117 | :param enabled: 别名是否启用,默认为 True。 118 | :param ignoreCase: 匹配时是否忽略大小写,默认为 False。 119 | :param isRegExp: 模式是否为正则表达式,默认为 True。 120 | :param keepEval: 若存在多个可匹配的别名时,是否持续匹配,默认为 False。 121 | :param expandVar: 是否展开变量,默认为 True。 122 | :param priority: 别名的优先级,值越小优先级越高,默认为 100。 123 | :param oneShot: 别名是否只触发一次后自动失效,默认为 False。 124 | :param args: 可变位置参数,用于传递额外的参数。 125 | :param kwargs: 可变关键字参数,用于传递额外的键值对参数。 126 | :return: PymudDecorator 实例,类型为 "alias"。 127 | """ 128 | # 将传入的参数更新到 kwargs 字典中 129 | kwargs.update({ 130 | "patterns": patterns, 131 | "id": id, 132 | "group": group, 133 | "enabled": enabled, 134 | "ignoreCase": ignoreCase, 135 | "isRegExp": isRegExp, 136 | "keepEval": keepEval, 137 | "expandVar": expandVar, 138 | "priority": priority, 139 | "oneShot": oneShot}) 140 | 141 | # 如果 id 为 None,则从 kwargs 中移除 "id" 键 142 | if not id: 143 | kwargs.pop("id") 144 | 145 | return PymudDecorator("alias", *args, **kwargs) 146 | 147 | def trigger( 148 | patterns: Union[List[str], str], 149 | id: Optional[str] = None, 150 | group: str = "", 151 | enabled: bool = True, 152 | ignoreCase: bool = False, 153 | isRegExp: bool = True, 154 | keepEval: bool = False, 155 | expandVar: bool = True, 156 | raw: bool = False, 157 | priority: int = 100, 158 | oneShot: bool = False, 159 | *args, **kwargs): 160 | """ 161 | 使用装饰器创建一个触发器(Trigger)对象。 162 | 163 | :param patterns: 触发器匹配的模式。单行模式可以是字符串或正则表达式,多行模式必须是元组或列表,其中每个元素都是字符串或正则表达式。 164 | :param id: 触发器对象的唯一标识符,不指定时将自动生成唯一标识符。 165 | :param group: 触发器所属的组名,默认为空字符串。 166 | :param enabled: 触发器是否启用,默认为 True。 167 | :param ignoreCase: 匹配时是否忽略大小写,默认为 False。 168 | :param isRegExp: 模式是否为正则表达式,默认为 True。 169 | :param keepEval: 若存在多个可匹配的触发器时,是否持续匹配,默认为 False。 170 | :param expandVar: 是否展开变量,默认为 True。 171 | :param raw: 是否使用原始匹配方式,默认为 False。原始匹配方式下,不对 VT100 下的 ANSI 颜色进行解码,因此可以匹配颜色;正常匹配仅匹配文本。 172 | :param priority: 触发器的优先级,值越小优先级越高,默认为 100。 173 | :param oneShot: 触发器是否只触发一次后自动失效,默认为 False。 174 | :param args: 可变位置参数,用于传递额外的参数。 175 | :param kwargs: 可变关键字参数,用于传递额外的键值对参数。 176 | :return: PymudDecorator 实例,类型为 "trigger"。 177 | """ 178 | # 将传入的参数更新到 kwargs 字典中 179 | kwargs.update({ 180 | "patterns": patterns, 181 | "id": id, 182 | "group": group, 183 | "enabled": enabled, 184 | "ignoreCase": ignoreCase, 185 | "isRegExp": isRegExp, 186 | "keepEval": keepEval, 187 | "expandVar": expandVar, 188 | "raw": raw, 189 | "priority": priority, 190 | "oneShot": oneShot}) 191 | if not id: 192 | kwargs.pop("id") 193 | return PymudDecorator("trigger", *args, **kwargs) 194 | 195 | def timer(timeout: float, id: Optional[str] = None, group: str = "", enabled: bool = True, *args, **kwargs): 196 | """ 197 | 使用装饰器创建一个定时器(Timer)对象。 198 | 199 | :param timeout: 定时器超时时间,单位为秒。 200 | :param id: 定时器对象的唯一标识符,不指定时将自动生成唯一标识符。 201 | :param group: 定时器所属的组名,默认为空字符串。 202 | :param enabled: 定时器是否启用,默认为 True。 203 | :param args: 可变位置参数,用于传递额外的参数。 204 | :param kwargs: 可变关键字参数,用于传递额外的键值对参数。 205 | :return: PymudDecorator 实例,类型为 "timer"。 206 | """ 207 | kwargs.update({ 208 | "timeout": timeout, 209 | "id": id, 210 | "group": group, 211 | "enabled": enabled 212 | }) 213 | if not id: 214 | kwargs.pop("id") 215 | return PymudDecorator("timer", *args, **kwargs) 216 | 217 | def gmcp(name: str, group: str = "", enabled: bool = True, *args, **kwargs): 218 | """ 219 | 使用装饰器创建一个GMCP触发器(GMCPTrigger)对象。 220 | 221 | :param name: GMCP触发器的名称。 222 | :param group: GMCP触发器所属的组名,默认为空字符串。 223 | :param enabled: GMCP触发器是否启用,默认为 True。 224 | :param args: 可变位置参数,用于传递额外的参数。 225 | :param kwargs: 可变关键字参数,用于传递额外的键值对参数。 226 | :return: PymudDecorator 实例,类型为 "gmcp"。 227 | """ 228 | kwargs.update({ 229 | "id": name, 230 | "group": group, 231 | "enabled": enabled 232 | }) 233 | 234 | return PymudDecorator("gmcp", *args, **kwargs) 235 | -------------------------------------------------------------------------------- /src/pymud/dialogs.py: -------------------------------------------------------------------------------- 1 | import asyncio, webbrowser 2 | from typing import Any, Callable, Iterable, List, Tuple, Union 3 | from prompt_toolkit.layout import AnyContainer, ConditionalContainer, Float, VSplit, HSplit, Window, WindowAlign, ScrollablePane, ScrollOffsets 4 | from prompt_toolkit.widgets import Button, Dialog, Label, MenuContainer, MenuItem, TextArea, SystemToolbar, Frame, RadioList 5 | from prompt_toolkit.layout.dimension import Dimension, D 6 | from prompt_toolkit import ANSI, HTML 7 | from prompt_toolkit.mouse_events import MouseEvent, MouseEventType 8 | from prompt_toolkit.formatted_text import FormattedText, AnyFormattedText 9 | from prompt_toolkit.application.current import get_app 10 | from .extras import EasternButton 11 | 12 | from .settings import Settings 13 | 14 | class BasicDialog: 15 | def __init__(self, title: AnyFormattedText = "", modal = True): 16 | self.future = asyncio.Future() 17 | self.dialog = Dialog( 18 | body = self.create_body(), 19 | title = title, 20 | buttons = self.create_buttons(), 21 | modal = modal, 22 | width = D(preferred=80), 23 | ) 24 | 25 | def set_done(self, result: Any = True): 26 | self.future.set_result(result) 27 | 28 | def create_body(self) -> AnyContainer: 29 | return HSplit([Label(text=Settings.gettext("basic_dialog"))]) 30 | 31 | def create_buttons(self): 32 | ok_button = EasternButton(text=Settings.gettext("ok"), handler=(lambda: self.set_done())) 33 | return [ok_button] 34 | 35 | def set_exception(self, exc): 36 | self.future.set_exception(exc) 37 | 38 | def __pt_container__(self): 39 | return self.dialog 40 | 41 | class MessageDialog(BasicDialog): 42 | def __init__(self, title="", message = "", modal=True): 43 | self.message = message 44 | super().__init__(title, modal) 45 | 46 | def create_body(self) -> AnyContainer: 47 | return HSplit([Label(text=self.message)]) 48 | 49 | class QueryDialog(BasicDialog): 50 | def __init__(self, title: AnyFormattedText = "", message: AnyFormattedText = "", modal = True): 51 | self.message = message 52 | super().__init__(title, modal) 53 | 54 | def create_body(self) -> AnyContainer: 55 | return HSplit([Label(text=self.message)]) 56 | 57 | def create_buttons(self): 58 | ok_button = EasternButton(text=Settings.gettext("ok"), handler=(lambda: self.set_done(True))) 59 | cancel_button = EasternButton(text=Settings.gettext("cancel"), handler=(lambda: self.set_done(False))) 60 | return [ok_button, cancel_button] 61 | 62 | class WelcomeDialog(BasicDialog): 63 | def __init__(self, modal=True): 64 | self.website = FormattedText( 65 | [('', f'{Settings.gettext("visit")} '), 66 | #('class:b', 'GitHub:'), 67 | ('', Settings.__website__, self.open_url), 68 | ('', f' {Settings.gettext("displayhelp")}')] 69 | ) 70 | super().__init__("PYMUD", modal) 71 | 72 | def open_url(self, event: MouseEvent): 73 | if event.event_type == MouseEventType.MOUSE_UP: 74 | webbrowser.open(Settings.__website__) 75 | 76 | def create_body(self) -> AnyContainer: 77 | import platform, sys 78 | body = HSplit([ 79 | Window(height=1), 80 | Label(HTML(Settings.gettext("appinfo", Settings.__version__, Settings.__release__)), align=WindowAlign.CENTER), 81 | Label(HTML(Settings.gettext("author", Settings.__author__, Settings.__email__)), align=WindowAlign.CENTER), 82 | Label(self.website, align=WindowAlign.CENTER), 83 | Label(Settings.gettext("sysversion", platform.system(), platform.version(), platform.python_version()), align = WindowAlign.CENTER), 84 | 85 | Window(height=1), 86 | ]) 87 | 88 | return body 89 | 90 | class NewSessionDialog(BasicDialog): 91 | def __init__(self): 92 | super().__init__(Settings.gettext("new_session"), True) 93 | 94 | def create_body(self) -> AnyContainer: 95 | body = HSplit([ 96 | VSplit([ 97 | HSplit([ 98 | Label(f" {Settings.gettext('sessionname')}:"), 99 | Frame(body=TextArea(name = "session", text="session", multiline=False, wrap_lines=False, height = 1, dont_extend_height=True, width = D(preferred=10), focus_on_click=True, read_only=False),) 100 | ]), 101 | HSplit([ 102 | Label(f" {Settings.gettext('host')}:"), 103 | Frame(body=TextArea(name = "host", text="mud.pkuxkx.net", multiline=False, wrap_lines=False, height = 1, dont_extend_height=True, width = D(preferred=20), focus_on_click=True, read_only=False),) 104 | ]), 105 | HSplit([ 106 | Label(f" {Settings.gettext('port')}:"), 107 | Frame(body=TextArea(name = "port", text="8081", multiline=False, wrap_lines=False, height = 1, dont_extend_height=True, width = D(max=8), focus_on_click=True, read_only=False),) 108 | ]), 109 | HSplit([ 110 | Label(f" {Settings.gettext('encoding')}:"), 111 | Frame(body=TextArea(name = "encoding", text="utf8", multiline=False, wrap_lines=False, height = 1, dont_extend_height=True, width = D(max=8), focus_on_click=True, read_only=False),) 112 | ]), 113 | ]) 114 | ]) 115 | 116 | return body 117 | 118 | def create_buttons(self): 119 | ok_button = EasternButton(text=Settings.gettext("ok"), handler=self.btn_ok_clicked) 120 | cancel_button = EasternButton(text=Settings.gettext("cancel"), handler=(lambda: self.set_done(False))) 121 | return [ok_button, cancel_button] 122 | 123 | def btn_ok_clicked(self): 124 | def get_text_safely(buffer_name): 125 | buffer = get_app().layout.get_buffer_by_name(buffer_name) 126 | return buffer.text if buffer else "" 127 | name = get_text_safely("session") 128 | host = get_text_safely("host") 129 | port = get_text_safely("port") 130 | encoding = get_text_safely("encoding") 131 | result = (name, host, port, encoding) 132 | self.set_done(result) 133 | 134 | 135 | class LogSelectionDialog(BasicDialog): 136 | def __init__(self, text, values, modal=True): 137 | self._header_text = text 138 | self._selection_values = values 139 | self._itemsCount = len(values) 140 | if len(values) > 0: 141 | self._radio_list = RadioList(values = self._selection_values) 142 | else: 143 | self._radio_list = Label(Settings.gettext("nolog").center(13)) 144 | super().__init__(Settings.gettext("chooselog"), modal) 145 | 146 | def create_body(self) -> AnyContainer: 147 | body=HSplit([ 148 | Label(text = self._header_text, dont_extend_height=True), 149 | self._radio_list 150 | ]) 151 | return body 152 | 153 | def create_buttons(self): 154 | ok_button = EasternButton(text=Settings.gettext("ok"), handler=self.btn_ok_clicked) 155 | cancel_button = EasternButton(text=Settings.gettext("cancel"), handler=(lambda: self.set_done(False))) 156 | return [ok_button, cancel_button] 157 | 158 | def btn_ok_clicked(self): 159 | if self._itemsCount: 160 | if isinstance(self._radio_list, RadioList): 161 | result = self._radio_list.current_value 162 | else: 163 | result = None 164 | self.set_done(result) 165 | else: 166 | self.set_done(False) 167 | -------------------------------------------------------------------------------- /src/pymud/i18n.py: -------------------------------------------------------------------------------- 1 | # internationalization (i18n) 2 | import os, importlib 3 | 4 | def i18n_ListAvailableLanguages(): 5 | """ 6 | List all available languages. 7 | 8 | This function checks all files in the `lang` directory for files starting with `i18n.` and ending with `.py`. 9 | These files represent internationalization configurations for different languages. The default language is Simplified Chinese ("chs"). 10 | 11 | Returns: 12 | list: A list containing all available language codes. 13 | """ 14 | # Define the default language list, here the default language is Simplified Chinese 15 | languages = [] 16 | # Define the directory where the language files are located 17 | lang_dir = os.path.join(os.path.dirname(__file__), "lang") 18 | 19 | # Check if the language directory exists. If it doesn't, return the default language list directly 20 | if os.path.exists(lang_dir): 21 | # Iterate through all files in the language directory 22 | for filename in os.listdir(lang_dir): 23 | # Check if the file starts with "i18n.", ends with ".py", and is not the default Simplified Chinese file 24 | if filename.startswith("i18n_") and filename.endswith(".py"): 25 | # Extract the language code from the filename, removing "i18n." and ".py" 26 | language = filename[5:-3] 27 | # Add the extracted language code to the list of available languages 28 | languages.append(language) 29 | 30 | if not languages: 31 | languages.append("chs") 32 | 33 | return languages 34 | 35 | def i18n_LoadLanguage(lang: str): 36 | lang_file = os.path.join(os.path.dirname(__file__), "lang", f"i18n_{lang}.py") 37 | if os.path.exists(lang_file): 38 | modLang = importlib.import_module(f"pymud.lang.i18n_{lang}") 39 | TRANS = modLang.__dict__["TRANSLATION"] 40 | if isinstance(TRANS, dict): 41 | from .settings import Settings 42 | Settings.text.update(TRANS["text"]) 43 | 44 | if "docstring" in TRANS.keys(): 45 | docstring = TRANS["docstring"] 46 | if isinstance(docstring, dict): 47 | if "Session" in docstring.keys(): 48 | from .session import Session 49 | docstring_class_session = docstring["Session"] 50 | if isinstance(docstring_class_session, dict): 51 | for key, newdoc in docstring_class_session.items(): 52 | if hasattr(Session, key): 53 | obj = getattr(Session, key) 54 | obj.__doc__ = newdoc 55 | 56 | if "PyMudApp" in docstring.keys(): 57 | from .pymud import PyMudApp 58 | docstring_class_pymudapp = docstring["PyMudApp"] 59 | if isinstance(docstring_class_pymudapp, dict): 60 | for key, newdoc in docstring_class_pymudapp.items(): 61 | if hasattr(PyMudApp, key): 62 | obj = getattr(PyMudApp, key) 63 | obj.__doc__ = newdoc -------------------------------------------------------------------------------- /src/pymud/lang/i18n_chs.py: -------------------------------------------------------------------------------- 1 | TRANSLATION = { 2 | "text" : { 3 | "welcome" : "欢迎使用PYMUD客户端 - 北大侠客行,最好的中文MUD游戏", # the welcome text shown in the statusbar when pymud start 4 | 5 | # text in pymud.py 6 | "world" : "世界", # the display text of menu "world" 7 | "new_session" : "创建新会话", # the display text of sub-menu "new_session" 8 | "show_log" : "显示记录信息", # the display text of sub-menu "show_log" 9 | "exit" : "退出", # the display text of sub-menu "exit" 10 | "session" : "会话", # the display text of menu "session" 11 | "connect" : "连接/重新连接", # the display text of sub-menu "connect" 12 | "disconnect" : "断开连接", # the display text of sub-menu "disconnect" 13 | "beautify" : "打开/关闭美化显示", # the display text of sub-menu "toggle beautify" 14 | "echoinput" : "显示/隐藏输入指令", # the display text of sub-menu "toggle echo input" 15 | "nosplit" : "取消分屏", # the display text of sub-menu "no split" 16 | "copy" : "复制(纯文本)", # the display text of sub-menu "copy (pure text)" 17 | "copyraw" : "复制(ANSI)", # the display text of sub-menu "copy (raw infomation)" 18 | "clearsession" : "清空会话内容", # the display text of sub-menu "clear session buffer" 19 | "closesession" : "关闭当前页面", # the display text of sub-menu "close current session" 20 | "autoreconnect" : "打开/关闭自动重连", # the display text of sub-menu "toggle auto reconnect" 21 | "loadconfig" : "加载脚本配置", # the display text of sub-menu "load config" 22 | "reloadconfig" : "重新加载脚本配置", # the display text of sub-menu "reload config" 23 | "layout" : "布局", # the display text of menu "layout" (not used now) 24 | "hide" : "隐藏状态窗口", # the display text of sub-menu "hide status window" (not used now) 25 | "horizon" : "下方状态窗口", # the display text of sub-menu "horizon layout" (not used now) 26 | "vertical" : "右侧状态窗口", # the display text of sub-menu "vertical layout" (not used now) 27 | "help" : "帮助", # the display text of menu "help" 28 | "about" : "关于", # the display text of menu "about" 29 | 30 | "session_changed" : "已成功切换到会话: {0}", 31 | "input_prompt" : '命令:', 32 | "msg_copy" : "已复制:{0}", 33 | "msg_copylines" : "已复制:行数{0}", 34 | "msg_no_selection" : "未选中任何内容...", 35 | "msg_session_exists" : "错误!已存在一个名为 {0} 的会话,请更换名称再试.", 36 | 37 | "logfile_name" : "记录文件名", 38 | "logfile_size" : "文件大小", 39 | "logfile_modified" : "最后修改时间", 40 | 41 | "warning" : "警告", 42 | "warning_exit" : "程序退出警告", 43 | "session_close_prompt" : "当前会话 {0} 还处于连接状态,确认要关闭?", 44 | "app_exit_prompt" : "尚有 {0} 个会话 {1} 还处于连接状态,确认要关闭?", 45 | 46 | "msg_beautify" : "显示美化已", 47 | "msg_echoinput" : "回显输入命令被设置为:", 48 | "msg_autoreconnect" : "自动重连被设置为:", 49 | "msg_open" : "打开", 50 | "msg_close" : "关闭", 51 | 52 | "msg_cmd_session_error" : '通过单一参数快速创建会话时,要使用 group.name 形式,如 #session pkuxkx.newstart', 53 | "msg_cmdline_input" : "命令行键入:", 54 | "msg_no_session" : "当前没有正在运行的session.", 55 | "msg_invalid_plugins" : "文件: {0} 不是一个合法的插件文件,加载错误,信息为: {1}", 56 | 57 | "status_nobeautify" : "美化已关闭", 58 | "status_mouseinh" : "鼠标已禁用", 59 | "status_ignore" : "全局禁用", 60 | "status_notconnect" : "未连接", 61 | "status_connected" : "已连接", 62 | 63 | # text in dialogs.py 64 | "basic_dialog" : "基础对话框", 65 | "ok" : "确定", 66 | "cancel" : "取消", 67 | "visit" : "访问", 68 | "displayhelp" : "以查看最新帮助文档", 69 | "appinfo" : 'PYMUD {0} - a MUD Client Written in Python', 70 | "author" : '作者: {0} E-mail: {1}', 71 | "sysversion" : '系统:{} {} Python版本:{}', 72 | "sessionname" : "会话名称", 73 | "host" : "服务器地址", 74 | "port" : "端口", 75 | "encoding" : "编码", 76 | "nolog" : "无记录", 77 | "chooselog" : "选择查看的记录", 78 | 79 | # text in modules.py 80 | "configuration_created" : "配置对象 {0}.{1} 创建成功.", 81 | "configuration_recreated" : "配置对象 {0}.{1} 重新创建成功.", 82 | "configuration_fail" : "配置对象 {0}.{1} 创建失败. 错误信息为: {}", 83 | "entity_module" : "主配置模块", 84 | "non_entity_module" : "从配置模块", 85 | "load_ok" : "加载完成", 86 | "load_fail" : "加载失败", 87 | "unload_ok" : "卸载完成", 88 | "reload_ok" : "重新加载完成", 89 | "msg_plugin_unloaded" : "本会话中插件 {0} 已停用。", 90 | "msg_plugin_loaded" : "本会话中插件 {0} 已启用。", 91 | 92 | # text in objects.py 93 | "excpetion_brace_not_matched" : "错误的代码块,大括号数量不匹配", 94 | "exception_quote_not_matched" : "引号的数量不匹配", 95 | "exception_forced_async" : "该命令中同时存在强制同步命令和强制异步命令,将使用异步执行,同步命令将失效。", 96 | "exception_session_type_fail" : "session 必须是 Session 类型对象的实例!", 97 | "exception_message" : "异常信息: <{}> {}", 98 | "exception_traceback" : '脚本执行异常, 异常位于文件"{}"中的第{}行的"{}"函数中。', 99 | "script_error" : "脚本错误", 100 | 101 | 102 | # text display in session.py 103 | "msg_var_autoload_success" : "自动从 {0} 中加载保存变量成功。", 104 | "msg_var_autoload_fail" : "自动从 {0} 中加载变量失败,错误消息为: {1}。", 105 | "msg_auto_script" : "即将自动加载以下模块: {0}", 106 | "msg_connection_fail" : "创建连接过程中发生错误, 错误发生时刻 {0}, 错误信息为 {1}。", 107 | "msg_auto_reconnect" : "{0} 秒之后将自动重新连接...", 108 | "msg_connected" : "{0}: 已成功连接到服务器。", 109 | "msg_disconnected" : "{0}: 与服务器连接已断开。", 110 | "msg_duplicate_logname" : "其它会话中已存在一个名为 {0} 的记录器,将直接返回该记录器。", 111 | "msg_default_statuswindow" : "这是一个默认的状态窗口信息\n会话: {0} 连接状态: {1}", 112 | "msg_mxp_not_support" : "MXP支持尚未开发,请暂时不要打开MXP支持设置!", 113 | "msg_no_session" : "不存在名称为{0}的会话。", 114 | "msg_num_positive" : "#{num} {cmd}只能支持正整数!", 115 | "msg_cmd_not_recognized" : "未识别的命令: {0}", 116 | "msg_id_not_consistent" : "对象 {0} 字典键值 {1} 与其id {2} 不一致,将丢弃键值,以其id添加到会话中...", 117 | "msg_shall_be_string" : "{0}必须为字符串类型", 118 | "msg_shall_be_list_or_tuple" : "{0}命名应为元组或列表,不接受其他类型", 119 | "msg_names_and_values" : "names与values应不为空,且长度相等", 120 | "msg_not_null" : "{0}不能为空", 121 | "msg_topic_not_found" : "未找到主题{0}, 请确认输入是否正确。", 122 | "Day" : "天", 123 | "Hour" : "小时", 124 | "Minute" : "分", 125 | "Second" : "秒", 126 | "msg_connection_duration" : "已经与服务器连接了: {0}", 127 | "msg_no_object" : "当前会话中不存在名称为 {0} 的{1}。", 128 | "msg_no_global_object" : "全局空间中不存在名称为 {0} 的{1}。", 129 | "msg_object_value_setted" : "成功设置{0} {1} 值为 {2}。", 130 | "variable" : "变量", 131 | "globalvar" : "全局变量", 132 | "msg_object_not_exists" : "当前会话中不存在key为 {0} 的 {1}, 请确认后重试。", 133 | "msg_object_enabled" : "对象 {0} 的使能状态已打开。", 134 | "msg_object_disabled" : "对象 {0} 的使能状态已关闭。", 135 | "msg_object_deleted" : "对象 {0} 已从会话中被删除。", 136 | "msg_group_objects_enabled" : "组 {0} 中的 {1} 个 {2} 对象均已使能。", 137 | "msg_group_objects_disabled" : "组 {0} 中的 {1} 个 {2} 对象均已禁用。", 138 | "msg_group_objects_deleted" : "组 {0} 中的 {1} 个 {2} 对象均已从会话中被删除。", 139 | "msg_object_param_invalid" : "#{0}命令的第二个参数仅能接受on/off/del", 140 | "msg_ignore_on" : "所有触发器使能已全局禁用。", 141 | "msg_ignore_off" : "不再全局禁用所有触发器使能。", 142 | "msg_T_plus_incorrect" : "#T+使能组使用不正确,正确使用示例: #t+ mygroup \n请使用#help ignore进行查询。", 143 | "msg_T_minus_incorrect" : "#T-禁用组使用不正确,正确使用示例: #t- mygroup \n请使用#help ignore进行查询。", 144 | "msg_group_enabled" : "组 {0} 中的 {1} 个别名,{2} 个触发器,{3} 个命令,{4} 个定时器,{5} 个GMCP触发器均已使能。", 145 | "msg_group_disabled" : "组 {0} 中的 {1} 个别名,{2} 个触发器,{3} 个命令,{4} 个定时器,{5} 个GMCP触发器均已禁用。", 146 | "msg_repeat_invalid" : "当前会话没有连接或没有键入过指令, repeat无效", 147 | "msg_window_title" : "来自会话 {0} 的消息", 148 | "msg_module_load_fail" : "模块 {0} 加载失败,异常为 {1}, 类型为 {2}。", 149 | "msg_exception_traceback" : "异常追踪为: {0}", 150 | "msg_module_not_loaded" : "指定模块名称 {0} 并未加载。", 151 | "msg_all_module_reloaded" : "所有配置模块全部重新加载完成。", 152 | "msg_plugins_reloaded" : "插件 {0} 重新加载完成。", 153 | "msg_name_not_found" : "指定名称 {0} 既未找到模块,也未找到插件,重新加载失败...", 154 | "msg_no_module" : "当前会话并未加载任何模块。", 155 | "msg_module_list" : "当前会话已加载 {0} 个模块,包括(按加载顺序排列): {1}。", 156 | "msg_module_configurations" : "模块 {0} 中包含的配置包括: {1}。", 157 | "msg_submodule_no_config" : "模块 {0} 为子模块,不包含配置。", 158 | "msg_module_not_loaded" : "本会话中不存在指定名称 {0} 的模块,可能是尚未加载到本会话中。", 159 | "msg_variables_saved" : "会话变量信息已保存到 {0}。", 160 | "msg_alias_created" : "创建Alias {0} 成功: {1}", 161 | "msg_trigger_created" : "创建Trigger {0} 成功: {1}", 162 | "msg_timer_created" : "创建Timer {0} 成功: {1}", 163 | 164 | "msg_tri_triggered" : " {0} 正常触发。", 165 | "msg_tri_wildcards" : " 捕获:{0}", 166 | "msg_tri_prevent" : " {0}该触发器未开启keepEval, 会阻止后续触发器。{1}", 167 | "msg_tri_ignore" : " {1}{0} 可以触发,但由于优先级与keepEval设定,触发器不会触发。{2}", 168 | "msg_tri_matched" : " {0} 可以匹配触发。", 169 | "msg_enabled_summary_0" : "{0} 使能的触发器中,没有可以触发的。", 170 | "msg_enabled_summary_1" : "{0} 使能的触发器中,共有 {1} 个可以触发,实际触发 {2} 个,另有 {3} 个由于 keepEval 原因实际不会触发。", 171 | "msg_enabled_summary_2" : "{0} 使能的触发器中,共有 {1} 个全部可以被正常触发。", 172 | "msg_disabled_summary_0" : "{0} 未使能的触发器中,共有 {1} 个可以匹配。", 173 | "msg_disabled_summary_1" : "{0} 未使能触发器,没有可以匹配的。", 174 | "msg_test_summary_0" : " 测试内容: {0}", 175 | "msg_test_summary_1" : " 测试结果: 没有可以匹配的触发器。", 176 | "msg_test_summary_2" : " 测试结果: 有{0}个触发器可以被正常触发,一共有{1}个满足匹配触发要求。", 177 | "msg_test_title" : "触发器测试 - {0}", 178 | "msg_triggered_mode" : "'响应模式'", 179 | "msg_matched_mode" : "'测试模式'", 180 | 181 | "msg_no_plugins" : "PYMUD当前并未加载任何插件。", 182 | "msg_plugins_list" : "PYMUD当前已加载 {0} 个插件,分别为:", 183 | "msg_plugins_info" : "作者 {2} 版本 {1} 发布日期 {3}\n 简介: {0}", 184 | 185 | "msg_py_exception" : "Python执行错误:{0}", 186 | 187 | "title_msg" : "消息", 188 | "title_warning" : "警告", 189 | "title_error" : "错误", 190 | "title_info" : "提示", 191 | 192 | "msg_log_title" : "本会话中的记录器情况:", 193 | "msg_log_title2" : "本应用其他会话中的记录器情况:", 194 | "logger" : "记录器", 195 | "enabled" : "开启", 196 | "disabled" : "关闭", 197 | "logger_status" : "当前状态", 198 | "file_mode" : "文件模式", 199 | "logger_mode" : "记录模式", 200 | "ANSI" : "ANSI", 201 | "plain_text" : "纯文本", 202 | 203 | "filemode_new" : "新建", 204 | "filemode_append" : "追加", 205 | "filemode_overwrite" : "覆写", 206 | 207 | "msg_logger_enabled" : "{0}: 记录器{1}以{2}文件模式以及{3}记录模式开启。", 208 | "msg_logger_disabled" : "{0}: 记录器{1}记录已关闭。", 209 | "msg_logfile_not_exists" : "指定的记录文件 {0} 不存在。", 210 | 211 | "exception_logmode_error" : "错误的记录模式: {0}", 212 | "exception_plugin_file_not_found" : "指定的插件文件 {0} 不存在或者格式不正确。", 213 | }, 214 | 215 | "docstring" : { 216 | "Session": { 217 | "handle_clear" : 218 | ''' 219 | 嵌入命令 #clear / #cls 的执行函数,清空当前会话缓冲与显示。 220 | 该函数不应该在代码中直接调用。 221 | 222 | 使用: 223 | - #cls: 清空当前会话缓冲及显示 224 | ''', 225 | } 226 | } 227 | } -------------------------------------------------------------------------------- /src/pymud/logger.py: -------------------------------------------------------------------------------- 1 | import os, re, datetime, threading, pathlib 2 | from queue import SimpleQueue, Empty 3 | from pathlib import Path 4 | from .settings import Settings 5 | class Logger: 6 | """ 7 | PyMUD 的记录器类型,可用于会话中向文件记录数据。记录文件保存在当前目录下的 log 子目录中 8 | 9 | :param name: 记录器名称,各记录器名称应保持唯一。记录器名称会作为记录文件名称的主要参数 10 | :param mode: 记录模式。可选模式包括 a, w, n 三种。 11 | - a为添加模式,当新开始记录时,会添加在原有记录文件(name.log)之后。 12 | - w为覆写模式,当新开始记录时,会覆写原记录文件(name.log)。 13 | - n为新建模式,当新开始记录时,会以name和当前时间为参数新建一个文件(name.now.log)用于记录。 14 | :param encoding: 记录文件的编码格式,默认为 "utf-8" 15 | :param errors: 当编码模式失败时的处理方式,默认为 "ignore" 16 | :param raw: 记录带ANSI标记的原始内容,还是记录纯文本内容,默认为True,即记录带ANSI标记的原始内容。 17 | """ 18 | 19 | # _esc_regx = re.compile(r"\x1b\[[\d;]+[abcdmz]", flags = re.IGNORECASE) 20 | 21 | def __init__(self, name, mode = 'a', encoding = "utf-8", errors = "ignore", raw = False): 22 | self._name = name 23 | self._enabled = False 24 | self._raw = raw 25 | self.mode = mode 26 | self._encoding = encoding 27 | self._errors = errors 28 | self._lock = threading.RLock() 29 | self._stream = None 30 | 31 | self._queue = SimpleQueue() 32 | 33 | @property 34 | def name(self): 35 | "记录器名称,仅在创建时设置,过程中只读" 36 | return self._name 37 | 38 | @property 39 | def enabled(self): 40 | """ 41 | 使能属性。 42 | 从false切换到true时,会打开文件,创建后台线程进行记录。 43 | 从true切换到false时,会终止后台记录线程,并关闭记录文件。 44 | """ 45 | return self._enabled 46 | 47 | @enabled.setter 48 | def enabled(self, enabled): 49 | if self._enabled != enabled: 50 | if enabled: 51 | mode = "a" 52 | 53 | if self._mode in ("a", "w"): 54 | filename = f"{self.name}.log" 55 | mode = self._mode 56 | elif self._mode == "n": 57 | now = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S") 58 | filename = f"{self.name}.{now}.log" 59 | 60 | else: 61 | raise ValueError(Settings.gettext("exception_logmode_error", self._mode)) 62 | 63 | 64 | logdir = Path.cwd().joinpath('log') 65 | if not logdir.exists() or not logdir.is_dir(): 66 | logdir.mkdir() 67 | 68 | filename = logdir.joinpath(filename) 69 | #filename = os.path.abspath(filename) 70 | self._stream = open(filename, mode = mode, encoding = self._encoding, errors = self._errors) 71 | self._thread = t = threading.Thread(target=self._monitor) 72 | t.daemon = True 73 | t.start() 74 | 75 | else: 76 | self._queue.put_nowait(None) 77 | if self._thread: 78 | self._thread.join() 79 | self._thread = None 80 | self._closeFile() 81 | 82 | self._enabled = enabled 83 | 84 | @property 85 | def raw(self): 86 | "属性,设置和获取是否记录带有ANSI标记的原始记录" 87 | return self._raw 88 | 89 | @raw.setter 90 | def raw(self, val: bool): 91 | self._raw = val 92 | 93 | @property 94 | def mode(self): 95 | "属性,记录器模式,可为 a, w, n" 96 | return self._mode 97 | 98 | @mode.setter 99 | def mode(self, value): 100 | if value in ("a", "w", "n"): 101 | self._mode = value 102 | 103 | def _closeFile(self): 104 | """ 105 | Closes the stream. 106 | """ 107 | self._lock.acquire() 108 | try: 109 | if self._stream: 110 | try: 111 | self._stream.flush() 112 | finally: 113 | stream = self._stream 114 | self._stream = None 115 | stream.close() 116 | finally: 117 | self._lock.release() 118 | 119 | def log(self, msg): 120 | """ 121 | 向记录器记录信息。记录的信息会通过队列发送到独立的记录线程。 122 | 当记录器未使能时,使用该函数调用也不会记录。 123 | 124 | :param msg: 要记录的信息 125 | """ 126 | if self._enabled: 127 | self._queue.put_nowait(msg) 128 | 129 | def _monitor(self): 130 | """ 131 | Monitor the queue for records, and ask the handler 132 | to deal with them. 133 | 134 | This method runs on a separate, internal thread. 135 | The thread will terminate if it sees a sentinel object in the queue. 136 | """ 137 | newline = True 138 | while self._stream: 139 | try: 140 | data = self._queue.get(block = True) 141 | if data: 142 | self._lock.acquire() 143 | 144 | if newline: 145 | now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") 146 | header = f"{now} {self._name}: " 147 | self._stream.write(header) 148 | newline = False 149 | 150 | if data.endswith("\n"): 151 | data = data.rstrip("\n").rstrip("\r") + "\n" 152 | newline = True 153 | 154 | if not self._raw: 155 | from .session import Session 156 | data = Session.PLAIN_TEXT_REGX.sub("", data) 157 | #data = self._esc_regx.sub("", data) 158 | 159 | self._stream.write(data) 160 | 161 | self._stream.flush() 162 | self._lock.release() 163 | 164 | else: 165 | break 166 | except Empty: 167 | break 168 | -------------------------------------------------------------------------------- /src/pymud/main.py: -------------------------------------------------------------------------------- 1 | import os, sys, json, platform, shutil, logging, argparse, locale 2 | from pathlib import Path 3 | from .pymud import PyMudApp 4 | from .settings import Settings 5 | 6 | CFG_TEMPLATE = { 7 | "language" : "chs", # 语言设置,默认为简体中文 8 | 9 | "client": { 10 | "buffer_lines" : 5000, # 保留缓冲行数 11 | 12 | "interval" : 10, # 在自动执行中,两次命令输入中的间隔时间(ms) 13 | "auto_connect" : True, # 创建会话后,是否自动连接 14 | "auto_reconnect" : False, # 在会话异常断开之后,是否自动重连 15 | "reconnect_wait" : 15, # 自动重连等待的时间(秒数) 16 | "var_autosave" : True, # 断开时自动保存会话变量 17 | "var_autoload" : True, # 初始化时自动加载会话变量 18 | 19 | "beautify" : True, # 专门为解决控制台下PKUXKX字符画对不齐的问题 20 | "history_records" : 500, 21 | 22 | "status_divider" : False, # 是否显示状态栏的分隔线 23 | "status_display" : 1, # 状态窗口显示情况设置,0-不显示,1-显示在下方,2-显示在右侧 24 | "status_height" : 4, # 下侧状态栏的高度 25 | "status_width" : 30, # 右侧状态栏的宽度 26 | 27 | 28 | }, 29 | 30 | "sessions" : { 31 | "pkuxkx" : { 32 | "host" : "mud.pkuxkx.net", 33 | "port" : "8081", 34 | "encoding" : "utf8", 35 | "autologin" : "{0};{1}", 36 | "default_script": "examples", 37 | "chars" : { 38 | "display_title" : ["yourid", "yourpassword", ""], 39 | } 40 | } 41 | }, 42 | "keys" : { 43 | "f3" : "#ig", 44 | "f4" : "#clear", 45 | "f11" : "#close", 46 | "f12" : "#exit", 47 | } 48 | } 49 | 50 | def detect_system_language(): 51 | """ 52 | 检测系统语言,返回中文或英文" 53 | """ 54 | lang = "chs" 55 | try: 56 | value = locale.getlocale()[0] 57 | if value and (value.lower().startswith("zh") or value.lower().startswith("chinese")): # 中文 58 | lang = "chs" 59 | else: 60 | lang = "eng" 61 | except Exception as e: 62 | # default is chs 63 | pass 64 | 65 | return lang 66 | 67 | def init_pymud_env(args): 68 | system = "unknown" 69 | lang = detect_system_language() 70 | if lang == "chs": 71 | print(f"欢迎使用PyMUD, 版本{Settings.__version__}. 使用PyMUD时, 建议建立一个新目录(任意位置),并将自己的脚本以及配置文件放到该目录下.") 72 | print("即将开始为首次运行初始化环境...") 73 | 74 | dir = args.dir 75 | if dir: 76 | if dir == ".": 77 | dir_msg = "当前目录" 78 | else: 79 | dir_msg = f": {dir}" 80 | print(f"你已经指定了创建脚本的目录为{dir_msg}") 81 | dir = Path(dir) 82 | else: 83 | dir = Path.home().joinpath('pkuxkx') 84 | 85 | system = platform.system().lower() 86 | dir_enter = input(f"检测到当前系统为 {system}, 请指定游戏脚本的目录(若目录不存在会自动创建),直接回车表示使用默认值 [{dir}]:") 87 | if dir_enter: 88 | dir = Path(dir_enter) 89 | 90 | if dir.exists() and dir.is_dir(): 91 | print(f'检测到给定目录 {dir} 已存在,切换至此目录...') 92 | else: 93 | print(f'检测到给定目录 {dir} 不存在,正在创建并切换至目录...') 94 | dir.mkdir() 95 | 96 | os.chdir(dir) 97 | 98 | if os.path.exists('pymud.cfg'): 99 | print(f'检测到脚本目录下已存在pymud.cfg文件,将直接使用此文件进入PyMUD...') 100 | else: 101 | print(f'检测到脚本目录下不存在pymud.cfg文件,将使用默认内容创建该配置文件...') 102 | with open('pymud.cfg', mode = 'x') as fp: 103 | fp.writelines(json.dumps(CFG_TEMPLATE, indent = 4)) 104 | 105 | if not os.path.exists('examples.py'): 106 | from pymud import pkuxkx 107 | module_dir = pkuxkx.__file__ 108 | shutil.copyfile(module_dir, 'examples.py') 109 | print(f'已将样例脚本拷贝至脚本目录,并加入默认配置文件') 110 | 111 | print(f"后续可自行修改 {dir} 目录下的 pymud.cfg 文件以进行配置。") 112 | if system == "windows": 113 | print(f"后续运行PyMUD, 请在 {dir} 目录下键入命令: python -m pymud,或直接使用快捷命令 pymud") 114 | else: 115 | print(f"后续运行PyMUD, 请在 {dir} 目录下键入命令: python3 -m pymud,或直接使用快捷命令 pymud") 116 | 117 | input('所有内容已初始化完毕, 请按回车进入PyMUD.') 118 | 119 | else: 120 | print(f"Welcome to PyMUD, version {Settings.__version__}. When using pymud, it is suggested that a new folder should be created (in any place), and the cfg configuration and all the scripts have been placed in the directory.") 121 | print("Starting to initialize the environment for the first time...") 122 | dir = args.dir 123 | if dir: 124 | if dir == ".": 125 | dir_msg = "current directory" 126 | else: 127 | dir_msg = f": {dir}" 128 | print(f"You have specified the directory to create the script as {dir_msg}") 129 | dir = Path(dir) 130 | else: 131 | dir = Path.home().joinpath('pkuxkx') 132 | 133 | system = platform.system().lower() 134 | dir_enter = input(f"Detected the current system is {system}, please specify the directory of the game script (if the directory does not exist, it will be automatically created), press Enter to use the default value [{dir}]:") 135 | if dir_enter: 136 | dir = Path(dir_enter) 137 | 138 | if dir.exists() and dir.is_dir(): 139 | print(f'Detected that the given directory {dir} already exists, switching to this directory...') 140 | else: 141 | print(f'Detected that the given directory {dir} does not exist, creating and switching to the directory...') 142 | dir.mkdir() 143 | 144 | os.chdir(dir) 145 | 146 | if os.path.exists('pymud.cfg'): 147 | print(f'Detected that the pymud.cfg file already exists in the script directory, entering PyMUD directly using this file...') 148 | else: 149 | print(f'Detected that the pymud.cfg file does not exist in the script directory, creating the configuration file using the default content...') 150 | with open('pymud.cfg', mode = 'x') as fp: 151 | CFG_TEMPLATE["language"] = "eng" 152 | fp.writelines(json.dumps(CFG_TEMPLATE, indent = 4)) 153 | 154 | if not os.path.exists('examples.py'): 155 | from pymud import pkuxkx 156 | module_dir = pkuxkx.__file__ 157 | shutil.copyfile(module_dir, 'examples.py') 158 | print(f'The sample script has been copied to the script directory and added to the default configuration file') 159 | 160 | print(f"Afterwards, you can modify the pymud.cfg file in the {dir} directory for configuration.") 161 | if system == "windows": 162 | print(f"Afterwards, please type the command 'python -m pymud' in the {dir} directory to run PyMUD, or use the shortcut command pymud") 163 | else: 164 | print(f"Afterwards, please type the command 'python3 -m pymud' in the {dir} directory to run PyMUD, or use the shortcut command pymud") 165 | 166 | input('Press Enter to enter PyMUD.') 167 | 168 | startApp(args) 169 | 170 | def startApp(args): 171 | startup_path = Path(args.startup_dir).resolve() 172 | sys.path.append(f"{startup_path}") 173 | os.chdir(startup_path) 174 | 175 | if args.debug: 176 | logging.basicConfig(level = logging.NOTSET, 177 | format = '%(asctime)s %(name)-12s %(levelname)-8s %(message)s', 178 | datefmt = '%m-%d %H:%M', 179 | filename = args.logfile, 180 | filemode = 'a' if args.filemode else 'w', 181 | encoding = "utf-8" 182 | ) 183 | 184 | else: 185 | logging.basicConfig(level = logging.NOTSET, 186 | format = '%(asctime)s %(name)-12s: %(message)s', 187 | datefmt = '%m-%d %H:%M', 188 | handlers = [logging.NullHandler()], 189 | ) 190 | 191 | cfg = startup_path.joinpath("pymud.cfg") 192 | cfg_data = None 193 | if os.path.exists(cfg): 194 | with open(cfg, "r", encoding="utf8", errors="ignore") as fp: 195 | cfg_data = json.load(fp) 196 | 197 | app = PyMudApp(cfg_data) 198 | app.run() 199 | 200 | def main(): 201 | parser = argparse.ArgumentParser(prog = "pymud", description = "PyMUD命令行参数帮助") 202 | subparsers = parser.add_subparsers(help = 'init用于初始化运行环境') 203 | 204 | par_init = subparsers.add_parser('init', description = '初始化pymud运行环境, 包括建立脚本目录, 创建默认配置文件, 创建样例脚本等.') 205 | par_init.add_argument('-d', '--dir', dest = 'dir', metavar = 'dir', type = str, default = '.', help = '指定构建脚本目录的名称, 不指定时会根据操作系统选择不同默认值') 206 | par_init.set_defaults(func = init_pymud_env) 207 | 208 | parser.add_argument('-d', '--debug', dest = 'debug', action = 'store_true', default = False, help = '指定以调试模式进入PyMUD。此时,系统log等级将设置为logging.NOTSET, 所有log数据均会被记录。默认不启用。') 209 | parser.add_argument('-l', '--logfile', dest = 'logfile', metavar = 'logfile', default = 'pymud.log', help = '指定调试模式下记录文件名,不指定时,默认为当前目录下的pymud.log') 210 | parser.add_argument('-a', '--appendmode', dest = 'filemode', action = 'store_true', default = True, help = '指定log文件的访问模式是否为append尾部添加模式,默认为True。当为False时,使用w模式,即每次运行清空之前记录') 211 | parser.add_argument('-s', '--startup_dir', dest = 'startup_dir', metavar = 'startup_dir', default = '.', help = '指定启动目录,默认为当前目录。使用该参数可以在任何目录下,通过指定脚本目录来启动') 212 | 213 | args=parser.parse_args() 214 | 215 | if hasattr(args, 'func'): 216 | args.func(args) 217 | else: 218 | startApp(args) 219 | 220 | if __name__ == "__main__": 221 | main() -------------------------------------------------------------------------------- /src/pymud/modules.py: -------------------------------------------------------------------------------- 1 | 2 | import importlib, importlib.util, traceback 3 | from typing import Any 4 | from .settings import Settings 5 | from .extras import DotDict 6 | from .decorators import exception, async_exception, PymudDecorator, print_exception 7 | 8 | class PymudMeta(type): 9 | def __new__(cls, name, bases, attrs): 10 | decorator_funcs = {} 11 | for name, value in attrs.items(): 12 | if hasattr(value, "__pymud_decorators__"): 13 | decorator_funcs[value.__name__] = getattr(value, "__pymud_decorators__", []) 14 | 15 | attrs["_decorator_funcs"] = decorator_funcs 16 | 17 | return super().__new__(cls, name, bases, attrs) 18 | 19 | class ModuleInfo: 20 | """ 21 | 模块管理类。对加载的模块文件进行管理。该类型由Session类进行管理,无需人工创建和干预。 22 | 23 | 有关模块的分类和使用的详细信息,请参见 `脚本 `_ 24 | 25 | :param module_name: 模块的名称, 应与 import xxx 语法中的 xxx 保持一致 26 | :param session: 加载/创建本模块的会话 27 | 28 | """ 29 | def __init__(self, module_name: str, session): 30 | from .session import Session 31 | if isinstance(session, Session): 32 | self.session = session 33 | self._name = module_name 34 | self._ismainmodule = False 35 | self.load() 36 | 37 | def _load(self, reload = False): 38 | result = True 39 | if reload: 40 | self._module = importlib.reload(self._module) 41 | else: 42 | self._module = importlib.import_module(self.name) 43 | self._config = {} 44 | for attr_name in dir(self._module): 45 | attr = getattr(self._module, attr_name) 46 | if isinstance(attr, type) and attr.__module__ == self._module.__name__: 47 | if (attr_name == "Configuration") or issubclass(attr, IConfig): 48 | try: 49 | self._config[f"{self.name}.{attr_name}"] = attr(self.session, reload = reload) 50 | if not reload: 51 | self.session.info(Settings.gettext("configuration_created", self.name, attr_name)) 52 | else: 53 | self.session.info(Settings.gettext("configuration_recreated", self.name, attr_name)) 54 | 55 | except Exception as e: 56 | result = False 57 | self.session.error(Settings.gettext("configuration_fail", self.name, attr_name, e)) 58 | self._ismainmodule = (self._config != {}) 59 | return result 60 | 61 | def _unload(self): 62 | from .objects import BaseObject, Command 63 | for key, config in self._config.items(): 64 | if isinstance(config, Command): 65 | # Command 对象在从会话中移除时,自动调用其 unload 系列方法,因此不能产生递归 66 | self.session.delObject(config) 67 | 68 | else: 69 | 70 | if hasattr(config, "__unload__"): 71 | unload = getattr(config, "__unload__", None) 72 | if callable(unload): unload() 73 | 74 | if hasattr(config, "unload"): 75 | unload = getattr(config, "unload", None) 76 | if callable(unload): unload() 77 | 78 | if isinstance(config, BaseObject): 79 | self.session.delObject(config) 80 | 81 | del config 82 | self._config.clear() 83 | 84 | def load(self): 85 | "加载模块内容" 86 | if self._load(): 87 | self.session.info(f"{Settings.gettext('entity_module' if self.ismainmodule else 'non_entity_module')} {self.name} {Settings.gettext('load_ok')}") 88 | else: 89 | self.session.info(f"{Settings.gettext('entity_module' if self.ismainmodule else 'non_entity_module')} {self.name} {Settings.gettext('load_fail')}") 90 | 91 | def unload(self): 92 | "卸载模块内容" 93 | self._unload() 94 | self._loaded = False 95 | self.session.info(f"{Settings.gettext('entity_module' if self.ismainmodule else 'non_entity_module')} {self.name} {Settings.gettext('unload_ok')}") 96 | 97 | def reload(self): 98 | "模块文件更新后调用,重新加载已加载的模块内容" 99 | self._unload() 100 | self._load(reload = True) 101 | self.session.info(f"{Settings.gettext('entity_module' if self.ismainmodule else 'non_entity_module')} {self.name} {Settings.gettext('reload_ok')}") 102 | 103 | @property 104 | def name(self): 105 | "只读属性,模块名称" 106 | return self._name 107 | 108 | @property 109 | def module(self): 110 | "只读属性,模块文件的 ModuleType 对象" 111 | return self._module 112 | 113 | @property 114 | def config(self): 115 | "只读字典属性,根据模块文件 ModuleType 对象创建的其中名为 Configuration 的类型或继承自 IConfig 的子类型实例(若有)" 116 | return self._config 117 | 118 | @property 119 | def ismainmodule(self): 120 | "只读属性,区分是否主模块(即包含具体config的模块)" 121 | return self._ismainmodule 122 | 123 | class IConfigBase(metaclass = PymudMeta): 124 | """ 125 | 用于支持对装饰器写法对象进行管理的基础类。 126 | 该类型相当于原来的IConfig类,唯一区别时,模块加载时,不会对本类型创建实例对象。 127 | 主要用于对插件中定义的Command提供装饰器写法支持,因为这些Command是在会话构建时创建,因此不能在模块加载时自动创建,也就不能继承自IConfig。 128 | """ 129 | def __init__(self, session, *args, **kwargs): 130 | from .session import Session 131 | from .objects import Alias, Trigger, Timer, GMCPTrigger 132 | if isinstance(session, Session): 133 | self.session = session 134 | self.__inline_objects__ = DotDict() 135 | 136 | if hasattr(self, "_decorator_funcs"): 137 | deco_funcs = getattr(self, "_decorator_funcs") 138 | for func_name, decorators in deco_funcs.items(): 139 | func = getattr(self, func_name) 140 | for deco in decorators: 141 | if isinstance(deco, PymudDecorator): 142 | if deco.type == "alias": 143 | #patterns = deco.kwargs.pop("patterns") 144 | ali = Alias(self.session, *deco.args, **deco.kwargs, onSuccess = func) 145 | self.__inline_objects__[ali.id] = ali 146 | 147 | elif deco.type == "trigger": 148 | #patterns = deco.kwargs.pop("patterns") 149 | tri = Trigger(self.session, *deco.args, **deco.kwargs, onSuccess = func) 150 | self.__inline_objects__[tri.id] = tri 151 | 152 | elif deco.type == "timer": 153 | tim = Timer(self.session, *deco.args, **deco.kwargs, onSuccess = func) 154 | self.__inline_objects__[tim.id] = tim 155 | 156 | elif deco.type == "gmcp": 157 | gmcp = GMCPTrigger(self.session, name = deco.kwargs.get("id"), *deco.args, **deco.kwargs, onSuccess = func) 158 | self.__inline_objects__[gmcp.id] = gmcp 159 | 160 | def __unload__(self): 161 | from .objects import BaseObject 162 | if self.session: 163 | self.session.delObjects(self.__inline_objects__) 164 | if isinstance(self, BaseObject): 165 | self.session.delObject(self) 166 | 167 | @property 168 | def objs(self) -> DotDict: 169 | "返回内联自动创建的对象字典" 170 | return self.__inline_objects__ 171 | 172 | def obj(self, obj_id: str): 173 | "根据对象ID返回内联自动创建的对象" 174 | return self.__inline_objects__.get(obj_id, None) # type: ignore 175 | 176 | class IConfig(IConfigBase): 177 | """ 178 | 用于提示PyMUD应用是否自动创建该配置类型的基础类。 179 | 180 | 继承 IConfig 类型让应用自动管理该类型,唯一需要的是,构造函数中,仅存在一个必须指定的参数 Session。 181 | 182 | 在应用自动创建 IConfig 实例时,除 session 参数外,还会传递一个 reload 参数 (bool类型),表示是首次加载还是重新加载特性。 183 | 可以从kwargs 中获取该参数,并针对性的设计相应代码。例如,重新加载相关联的其他模块等。 184 | """ 185 | 186 | class Plugin: 187 | """ 188 | 插件管理类。对加载的插件文件进行管理。该类型由PyMudApp进行管理,无需人工创建。 189 | 190 | 有关插件的详细信息,请参见 `插件 `_ 191 | 192 | :param name: 插件的文件名, 如'myplugin.py' 193 | :param location: 插件所在的目录。自动加载的插件包括PyMUD包目录下的plugins目录以及当前目录下的plugins目录 194 | 195 | """ 196 | def __init__(self, name, location): 197 | self._plugin_file = name 198 | self._plugin_loc = location 199 | 200 | self.reload() 201 | 202 | def reload(self): 203 | "加载/重新加载插件对象" 204 | #del self.modspec, self.mod 205 | self.modspec = importlib.util.spec_from_file_location(self._plugin_file[:-3], self._plugin_loc) 206 | if self.modspec and self.modspec.loader: 207 | self.mod = importlib.util.module_from_spec(self.modspec) 208 | self.modspec.loader.exec_module(self.mod) 209 | 210 | self._app_init = self._load_mod_function("PLUGIN_PYMUD_START") 211 | self._session_create = self._load_mod_function("PLUGIN_SESSION_CREATE") 212 | self._session_destroy = self._load_mod_function("PLUGIN_SESSION_DESTROY") 213 | self._app_destroy = self._load_mod_function("PLUGIN_PYMUD_DESTROY") 214 | 215 | else: 216 | raise FileNotFoundError(Settings.gettext("exception_plugin_file_not_found", self._plugin_file)) 217 | 218 | def _load_mod_function(self, func_name): 219 | # 定义一个默认函数,当插件文件中未定义指定名称的函数时,返回该函数 220 | # 该函数接受任意数量的位置参数和关键字参数,但不执行任何操作 221 | def default_func(*args, **kwargs): 222 | pass 223 | 224 | result = default_func 225 | if func_name in self.mod.__dict__: 226 | func = self.mod.__dict__[func_name] 227 | if callable(func): 228 | result = func 229 | return result 230 | 231 | @property 232 | def name(self): 233 | "插件名称,由插件文件中的 PLUGIN_NAME 常量定义" 234 | return self.mod.__dict__["PLUGIN_NAME"] 235 | 236 | @property 237 | def desc(self): 238 | "插件描述,由插件文件中的 PLUGIN_DESC 常量定义" 239 | return self.mod.__dict__["PLUGIN_DESC"] 240 | 241 | @property 242 | def help(self): 243 | "插件帮助,由插件文件中的文档字符串定义" 244 | return self.mod.__doc__ 245 | 246 | def onAppInit(self, app): 247 | """ 248 | PyMUD应用启动时对插件执行的操作,由插件文件中的 PLUGIN_PYMUD_START 函数定义 249 | 250 | :param app: 启动的 PyMudApp 对象实例 251 | """ 252 | self._app_init(app) 253 | 254 | def onSessionCreate(self, session): 255 | """ 256 | 新会话创建时对插件执行的操作,由插件文件中的 PLUGIN_SESSION_CREATE 函数定义 257 | 258 | :param session: 新创建的会话对象实例 259 | """ 260 | try: 261 | self._session_create(session) 262 | except Exception as e: 263 | print_exception(session, e) 264 | 265 | 266 | def onSessionDestroy(self, session): 267 | """ 268 | 会话关闭时(注意不是断开)对插件执行的操作,由插件文件中的 PLUGIN_SESSION_DESTROY 函数定义 269 | 270 | :param session: 所关闭的会话对象实例 271 | """ 272 | try: 273 | self._session_destroy(session) 274 | except Exception as e: 275 | print_exception(session, e) 276 | 277 | def onAppDestroy(self, app): 278 | """ 279 | PyMUD应用关闭时对插件执行的操作,由插件文件中的 PLUGIN_PYMUD_DESTROY 函数定义 280 | :param app: 关闭的 PyMudApp 对象实例 281 | """ 282 | self._app_destroy(app) 283 | 284 | def __getattr__(self, __name: str) -> Any: 285 | if hasattr(self.mod, __name): 286 | return self.mod.__getattribute__(__name) -------------------------------------------------------------------------------- /src/pymud/pkuxkx.py: -------------------------------------------------------------------------------- 1 | # 示例脚本:如何在PyMud中玩PKUXKX 2 | 3 | import webbrowser 4 | from pymud import Session, IConfig, alias, trigger, timer, gmcp, Alias, Trigger, Timer, SimpleTrigger, SimpleAlias 5 | 6 | # 在PyMud中,使用#load {filename}可以加载对应的配置作为脚本文件以提供支撑。支持多脚本加载 7 | # 本示例脚本对PyMud支持的变量(Variable)、触发器(Trigger,包含单行与多行触发)、别名(Alias)、定时器(Timer)进行了代码示例 8 | # 使用#load {filename}加载的配置文件中,若有一个类型继承自IConfig,则在#load操作时,会自动创建此类型;若没有继承自IConfig的类,则仅将文件引入 9 | # 例如,加载本文件指定的配置,则使用 #load pymud.pkuxkx即可 10 | 11 | # 定义一个自定义配置类,并继承自IConfig。 12 | # 目前不在推荐使用Configuration类,而是使用IConfig接口。因为只有使用IConfig接口,才能在类型函数中自动管理由装饰器创建的对象 13 | class MyConfig(IConfig): 14 | # 类的构造函数,传递参数session,是会话本身。另外请保留*args和**kwargs,以便后续扩展 15 | def __init__(self, session: Session, *args, **kwargs) -> None: 16 | # 建议将 super().__init__()放在类型init的首句代码,该代码用于对装饰器@alias等函数所装饰的对象进行管理 17 | # 调用super().__init__()时,会自动将session传递给父类,以便后续使用。 18 | # 因此此处无需再使用self.session = session来保存传递的会话类型 19 | # 20 | super().__init__(session, *args, **kwargs) 21 | 22 | # 所有自行构建的对象, 建议统一放到self._objs中,方便管理和卸载。 23 | # 目前加载卸载可以支持字典、列表、单个对象均可。此处使用字典,是为了方便后续处理其中某个单个对象。 24 | # 对象创建时将自动增加到会话中,不需要手动调用session.addObject操作了 25 | self._objs = { 26 | # 别名,触发器可以通过创建一个对应类型的实例来生成 27 | "tri_gem" : SimpleTrigger(self.session ,r'^[> ]*从.+身上.+[◎☆★].+', "pack gem", group = "sys"), 28 | "ali_yz_xm" : SimpleAlias(self.session ,'^yz_xm$', "w;#wa 100;w;#wa 100;w;#wa 100;w", group = "sys") 29 | } 30 | 31 | # 将自定义的状态窗口函数赋值给会话的status_maker属性,这样会话就会使用该函数来显示状态信息。 32 | self.session.status_maker = self.status_window 33 | 34 | 35 | # 如果仅使用了装饰器定义的PyMUD对象(Alias,Trigger等),则无需实现__unload__方法。 36 | # 但如果自定义了PyMUD对象,那么必须实现__unload__方法,否则会导致加载的对象无法被正常卸载。 37 | # 如果实现了__unload__方法,那么在该方法中必须调用super().__unload__(),否则会导致@alias等函数装饰器生成的对象不能被正常卸载 38 | def __unload__(self): 39 | # 在__unload__方法中定义卸载时需要从会话中清除的对象。 40 | # 目前加载卸载可以支持字典、列表、单个对象均可。 41 | self.session.delObjects(self._objs) 42 | 43 | # 不要遗漏 super().__unload__(),否则会导致@alias等函数装饰器生成的对象不能被正常卸载 44 | super().__unload__() 45 | 46 | # 别名, gp gold = get gold from corpse 47 | @alias(r"^gp\s(.+)$", id = "ali_get", group = "sys") 48 | def getfromcorpse(self, id, line, wildcards): 49 | cmd = f"get {wildcards[0]} from corpse" 50 | self.session.writeline(cmd) 51 | 52 | # 定时器,每5秒打印一次信息 53 | @timer(5) 54 | def onTimer(self, id, *args, **kwargs): 55 | self.session.info("每5秒都会打印本信息", "定时器测试") 56 | 57 | # 导航触发器示例 58 | @trigger('^http://fullme.pkuxkx.net/robot.php.+$', group = "sys") 59 | def ontri_webpage(self, id, line, wildcards): 60 | webbrowser.open(line) 61 | 62 | # 若多个对象共用同一个处理函数,也可以同时使用多个装饰器实现 63 | @trigger(r"^\s+你可以获取(.+)") 64 | @trigger(r"^\s+这里位于(.+)和(.+)的.+") 65 | def ontri_multideco(self, id, line, wildcards): 66 | self.session.info("触发器触发,ID: {0}, 内容: {1}, 匹配项: {2}".format(id, line, wildcards), "测试") 67 | 68 | # 多行触发器示例 69 | @trigger([r'^[> ]*#(\d+.?\d*[KM]?),(\d+),(\d+),(\d+),(\d+),(\d+)$', r'^[> ]*#(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)$', r'^[> ]*#(\d+),(\d+),(-?\d+),(-?\d+),(\d+),(\d+)$'], group = "sys") 70 | def ontri_hpbrief_3lines(self, id, line, wildcards): 71 | # 注意注意,此处捕获的额内容在wildcards里都是str类型,直接用下面这种方式赋值的时候,保存的变量也是str类型,因此这种在status_window直接调用并用于计算时,需要另行处理 72 | self.session.setVariables([ 73 | "combat_exp", "potential", "max_neili", "neili", "max_jingli", "jingli", 74 | "max_qi", "eff_qi", "qi", "max_jing", "eff_jing", "jing", 75 | "vigour/qi", "vigour/yuan", "food", "water", "fighting", "busy" 76 | ] 77 | , wildcards) 78 | # 因为GMCP.Status传递来的是busy和fighting,与hpbrief逻辑相反,因此重新处理下,保证hpbrief和GMCP.Status一致 79 | is_busy = not wildcards[-1] 80 | is_fighting = not wildcards[-2] 81 | self.session.setVariables(['is_busy', 'is_fighting'], [is_busy, is_fighting]) 82 | 83 | # gmcp定义式,name的大小写必须与GMCP的大小写一致,否则无法触发 84 | @gmcp("GMCP.Status") 85 | def ongmcp_status(self, id, line, wildcards): 86 | # GMCP.Status in pkuxkx 87 | # 自己的Status和敌人的Status均会使用GMCP.Status发送 88 | # 区别在于,敌人的Status会带有id属性。但登录首次自己也会发送id属性,但同时有很多属性,因此增加一个实战经验属性判定 89 | if isinstance(wildcards, dict): # 正常情况下,GMCP.Status应该是一个dict 90 | if ("id" in wildcards.keys()) and (not "combat_exp" in wildcards.keys()): 91 | # 说明是敌人的,暂时忽略 92 | #self.session.info(f"GMCP.Status 收到非自己信息: {wildcards}") 93 | pass 94 | 95 | else: 96 | # GMCP.status收到的wildcards是一个json格式转换过来的字典信息,可以直接用于变量赋值 97 | # 但json过来的true/false时全小写字符串,此处转换为bool类型使用 98 | #self.session.info(f"GMCP.Status 收到个人信息: {wildcards}") 99 | for key, value in wildcards.items(): 100 | if value == "false": value = False 101 | elif value == "true": value = True 102 | self.session.setVariable(key, value) 103 | 104 | # 如果这些变量显示在状态窗口中,可以调用下面代码强制刷新状态窗口 105 | self.session.application.invalidate() 106 | 107 | # 创建自定义的健康条用作分隔符 108 | def create_status_bar(self, current, effective, maximum, barlength = 20, barstyle = "—"): 109 | from wcwidth import wcswidth 110 | barline = list() 111 | stylewidth = wcswidth(barstyle) 112 | filled_length = int(round(barlength * current / maximum / stylewidth)) 113 | # 计算有效健康值部分的长度 114 | effective_length = int(round(barlength * effective / maximum / stylewidth)) 115 | 116 | # 计算剩余部分长度 117 | remaining_length = barlength - effective_length 118 | 119 | # 构造健康条 120 | barline.append(("fg:lightcyan", barstyle * filled_length)) 121 | barline.append(("fg:yellow", barstyle * (effective_length - filled_length))) 122 | barline.append(("fg:red", barstyle * remaining_length)) 123 | 124 | return barline 125 | 126 | # 自定义状态栏窗口 127 | def status_window(self): 128 | from pymud.settings import Settings 129 | try: 130 | formatted_list = list() 131 | 132 | # line 0. hp bar 133 | jing = self.session.getVariable("jing", 0) 134 | effjing = self.session.getVariable("eff_jing", 0) 135 | maxjing = self.session.getVariable("max_jing", 0) 136 | jingli = self.session.getVariable("jingli", 0) 137 | maxjingli = self.session.getVariable("max_jingli", 0) 138 | qi = self.session.getVariable("qi", 0) 139 | effqi = self.session.getVariable("eff_qi", 0) 140 | maxqi = self.session.getVariable("max_qi", 0) 141 | neili = self.session.getVariable("neili", 0) 142 | maxneili = self.session.getVariable("max_neili", 0) 143 | 144 | barstyle = "━" 145 | screenwidth = self.session.application.get_width() 146 | barlength = screenwidth // 2 - 1 147 | span = screenwidth - 2 * barlength 148 | qi_bar = self.create_status_bar(qi, effqi, maxqi, barlength, barstyle) 149 | jing_bar = self.create_status_bar(jing, effjing, maxjing, barlength, barstyle) 150 | 151 | formatted_list.extend(qi_bar) 152 | formatted_list.append(("", " " * span)) 153 | formatted_list.extend(jing_bar) 154 | formatted_list.append(("", "\n")) 155 | 156 | # line 1. char, menpai, deposit, food, water, exp, pot 157 | formatted_list.append((Settings.styles["title"], "【角色】")) 158 | formatted_list.append((Settings.styles["value"], "{0}({1})".format(self.session.getVariable('name'), self.session.getVariable('id')))) 159 | formatted_list.append(("", " ")) 160 | 161 | formatted_list.append((Settings.styles["title"], "【食物】")) 162 | 163 | food = int(self.session.getVariable('food', '0')) 164 | max_food = self.session.getVariable('max_food', 350) 165 | if food < 100: 166 | style = Settings.styles["value.worst"] 167 | elif food < 200: 168 | style = Settings.styles["value.worse"] 169 | elif food < max_food: 170 | style = Settings.styles["value"] 171 | else: 172 | style = Settings.styles["value.better"] 173 | 174 | formatted_list.append((style, "{}".format(food))) 175 | formatted_list.append(("", " ")) 176 | 177 | formatted_list.append((Settings.styles["title"], "【饮水】")) 178 | water = int(self.session.getVariable('water', '0')) 179 | max_water = self.session.getVariable('max_water', 350) 180 | if water < 100: 181 | style = Settings.styles["value.worst"] 182 | elif water < 200: 183 | style = Settings.styles["value.worse"] 184 | elif water < max_water: 185 | style = Settings.styles["value"] 186 | else: 187 | style = Settings.styles["value.better"] 188 | formatted_list.append((style, "{}".format(water))) 189 | formatted_list.append(("", " ")) 190 | formatted_list.append((Settings.styles["title"], "【经验】")) 191 | formatted_list.append((Settings.styles["value"], "{}".format(self.session.getVariable('combat_exp')))) 192 | formatted_list.append(("", " ")) 193 | formatted_list.append((Settings.styles["title"], "【潜能】")) 194 | formatted_list.append((Settings.styles["value"], "{}".format(self.session.getVariable('potential')))) 195 | formatted_list.append(("", " ")) 196 | 197 | formatted_list.append((Settings.styles["title"], "【门派】")) 198 | formatted_list.append((Settings.styles["value"], "{}".format(self.session.getVariable('family/family_name')))) 199 | formatted_list.append(("", " ")) 200 | formatted_list.append((Settings.styles["title"], "【存款】")) 201 | formatted_list.append((Settings.styles["value"], "{}".format(self.session.getVariable('deposit')))) 202 | formatted_list.append(("", " ")) 203 | 204 | # line 2. hp 205 | # a new-line 206 | formatted_list.append(("", "\n")) 207 | 208 | formatted_list.append((Settings.styles["title"], "【精神】")) 209 | if int(effjing) < int(maxjing): 210 | style = Settings.styles["value.worst"] 211 | elif int(jing) < 0.8 * int(effjing): 212 | style = Settings.styles["value.worse"] 213 | else: 214 | style = Settings.styles["value"] 215 | 216 | if maxjing == 0: 217 | pct1 = pct2 = 0 218 | else: 219 | pct1 = 100.0*float(jing)/float(maxjing) 220 | pct2 = 100.0*float(effjing)/float(maxjing) 221 | formatted_list.append((style, "{0}[{1:3.0f}%] / {2}[{3:3.0f}%]".format(jing, pct1, effjing, pct2))) 222 | 223 | formatted_list.append(("", " ")) 224 | 225 | formatted_list.append((Settings.styles["title"], "【气血】")) 226 | if int(effqi) < int(maxqi): 227 | style = Settings.styles["value.worst"] 228 | elif int(qi) < 0.8 * int(effqi): 229 | style = Settings.styles["value.worse"] 230 | else: 231 | style = Settings.styles["value"] 232 | 233 | if maxqi == 0: 234 | pct1 = pct2 = 0 235 | else: 236 | pct1 = 100.0*float(qi)/float(maxqi) 237 | pct2 = 100.0*float(effqi)/float(maxqi) 238 | formatted_list.append((style, "{0}[{1:3.0f}%] / {2}[{3:3.0f}%]".format(qi, pct1, effqi, pct2))) 239 | formatted_list.append(("", " ")) 240 | 241 | # 内力 242 | formatted_list.append((Settings.styles["title"], "【内力】")) 243 | if int(neili) < 0.6 * int(maxneili): 244 | style = Settings.styles["value.worst"] 245 | elif int(neili) < 0.8 * int(maxneili): 246 | style = Settings.styles["value.worse"] 247 | elif int(neili) < 1.2 * int(maxneili): 248 | style = Settings.styles["value"] 249 | else: 250 | style = Settings.styles["value.better"] 251 | 252 | if maxneili == 0: 253 | pct = 0 254 | else: 255 | pct = 100.0*float(neili)/float(maxneili) 256 | formatted_list.append((style, "{0} / {1}[{2:3.0f}%]".format(neili, maxneili, pct))) 257 | formatted_list.append(("", " ")) 258 | 259 | # 精力 260 | formatted_list.append((Settings.styles["title"], "【精力】")) 261 | if int(jingli) < 0.6 * int(maxjingli): 262 | style = Settings.styles["value.worst"] 263 | elif int(jingli) < 0.8 * int(maxjingli): 264 | style = Settings.styles["value.worse"] 265 | elif int(jingli) < 1.2 * int(maxjingli): 266 | style = Settings.styles["value"] 267 | else: 268 | style = Settings.styles["value.better"] 269 | 270 | if maxjingli == 0: 271 | pct = 0 272 | else: 273 | pct = 100.0*float(jingli)/float(maxjingli) 274 | 275 | formatted_list.append((style, "{0} / {1}[{2:3.0f}%]".format(jingli, maxjingli, pct))) 276 | formatted_list.append(("", " ")) 277 | 278 | return formatted_list 279 | 280 | except Exception as e: 281 | return f"{e}" -------------------------------------------------------------------------------- /src/pymud/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | PyMUD Settings 文件 3 | 用于保存与App有关的各类配置、常量等 4 | """ 5 | 6 | import importlib.metadata 7 | 8 | class Settings: 9 | "保存PyMUD配置的全局对象" 10 | 11 | # 下列内容为APP的常量定义,请勿修改 12 | __appname__ = "PYMUD" 13 | "APP 名称, 默认PYMUD" 14 | __appdesc__ = "a MUD client written in Python" 15 | "APP 简要描述" 16 | __version__ = importlib.metadata.version("pymud") 17 | "APP 当前版本" 18 | __release__ = "2025-05-27" 19 | "APP 当前版本发布日期" 20 | __author__ = "本牛(newstart)@北侠" 21 | "APP 作者" 22 | __email__ = "crapex@crapex.cc" 23 | "APP 作者邮箱" 24 | __website__ = "https://pymud.readthedocs.io/" 25 | "帮助文档发布网址" 26 | 27 | language = "chs" 28 | 29 | server = { 30 | "default_encoding" : "utf-8", # 服务器默认编码 31 | "encoding_errors" : "ignore", # 默认编码转换失效时错误处理 32 | "newline" : "\n", # 服务器端换行符特性 33 | 34 | 35 | "SGA" : True, # Supress Go Ahead 36 | "ECHO" : False, # Echo 37 | "GMCP" : True, # Generic Mud Communication Protocol 38 | "MSDP" : True, # Mud Server Data Protocol 39 | "MSSP" : True, # Mud Server Status Protocol 40 | "MCCP2" : False, # Mud Compress Communication Protocol V2 41 | "MCCP3" : False, # Mud Compress Communication Protocol V3 42 | "MSP" : False, # Mud 音频协议 43 | "MXP" : False, # Mud 扩展协议 44 | } 45 | "服务器的默认配置信息" 46 | 47 | mnes = { 48 | "CHARSET" : server["default_encoding"], 49 | "CLIENT_NAME" : __appname__, 50 | "CLIENT_VERSION" : __version__, 51 | "AUTHOR" : __author__, 52 | } 53 | "MUD协议所需的的默认MNES(Mud New-Environment Standard)配置信息" 54 | 55 | client = { 56 | "buffer_lines" : 5000, # 保留缓冲行数 57 | 58 | "naws_width" : 150, # 客户端NAWS宽度 59 | "naws_height" : 40, # 客户端NAWS高度 60 | "newline" : "\n", # 客户端换行符 61 | "tabstop" : 4, # 制表符改成空格 62 | "seperator" : ";", # 多个命令分隔符(默认;) 63 | "appcmdflag" : "#", # app命令标记(默认#) 64 | 65 | "interval" : 10, # 在自动执行中,两次命令输入中的间隔时间(ms) 66 | "auto_connect" : True, # 创建会话后,是否自动连接 67 | "auto_reconnect" : False, # 在会话异常断开之后,是否自动重连 68 | "reconnect_wait" : 15, # 自动重连等待的时间(秒数) 69 | "var_autosave" : True, # 断开时自动保存会话变量 70 | "var_autoload" : True, # 初始化时自动加载会话变量 71 | 72 | "remain_last_input" : False, 73 | "echo_input" : False, 74 | "beautify" : True, # 专门为解决控制台下PKUXKX字符画对不齐的问题 75 | "history_records" : 500, # 记录发送到服务器的命令的上限数量,0表示不记录,-1表示无限记录 76 | 77 | "status_divider" : True, # 是否显示状态栏的分隔线 78 | "status_display" : 1, # 状态窗口显示情况设置,0-不显示,1-显示在下方,2-显示在右侧 79 | "status_width" : 30, # 右侧状态栏的宽度 80 | "status_height" : 6, # 下侧状态栏的高度 81 | } 82 | "客户端的默认配置信息" 83 | 84 | text = { 85 | "welcome" : "欢迎使用PYMUD客户端 - 北大侠客行,最好的中文MUD游戏", 86 | "world" : "世界", 87 | "new_session" : "创建新会话...", 88 | "show_log" : "显示记录信息", 89 | "exit" : "退出", 90 | "session" : "会话", 91 | "connect" : "连接/重新连接", 92 | "disconnect" : "断开连接", 93 | "beautify" : "打开/关闭美化显示", 94 | "echoinput" : "显示/隐藏输入指令", 95 | "nosplit" : "取消分屏", 96 | "copy" : "复制(纯文本)", 97 | "copyraw" : "复制(ANSI)", 98 | "clearsession" : "清空会话内容", 99 | "closesession" : "关闭当前页面", 100 | "autoreconnect" : "打开/关闭自动重连", 101 | "loadconfig" : "加载脚本配置", 102 | "reloadconfig" : "重新加载脚本配置", 103 | "layout" : "布局", 104 | "hide" : "隐藏状态窗口", 105 | "horizon" : "下方状态窗口", 106 | "vertical" : "右侧状态窗口", 107 | "help" : "帮助", 108 | "about" : "关于", 109 | 110 | "session_changed" : "已成功切换到会话: {0}", 111 | 112 | "input_prompt" : '命令:', # HTML格式,输入命令行的提示信息 113 | } 114 | 115 | 116 | keys = { 117 | "f3" : "#ig", 118 | "f4" : "#clear", 119 | "f5" : "", 120 | "f6" : "", 121 | "f7" : "", 122 | "f8" : "", 123 | "f9" : "", 124 | "f10" : "", 125 | "f11" : "#close", 126 | "f12" : "#exit", 127 | 128 | "c-1" : "", 129 | "c-2" : "", 130 | "c-3" : "", 131 | "c-4" : "", 132 | "c-5" : "", 133 | "c-6" : "", 134 | "c-7" : "", 135 | "c-8" : "", 136 | "c-9" : "", 137 | "c-0" : "", 138 | } 139 | 140 | sessions = { 141 | "pkuxkx" : { 142 | "host" : "mud.pkuxkx.net", 143 | "port" : "8081", 144 | "encoding" : "utf8", 145 | "autologin" : "{0};{1}", 146 | "default_script": "common_modules", 147 | "chars" : { 148 | "display_title" : ["yourid", "yourpassword", "special_modules"], 149 | } 150 | }, 151 | "another-mud-evennia" : { 152 | "host" : "another.mud", 153 | "port" : "4000", 154 | "encoding" : "utf8", 155 | "autologin" : "connect {0} {1}", 156 | "default_script": None, 157 | "chars" : { 158 | "evennia" : ["name", "pass"], 159 | } 160 | } 161 | } 162 | 163 | styles = { 164 | "status" : "reverse", 165 | "shadow" : "bg:#440044", 166 | 167 | "prompt" : "", 168 | 169 | "selected" : "bg:#555555 fg:#eeeeee bold", 170 | "selected.connected" : "bg:#555555 fg:#33ff33 bold", 171 | "normal" : "fg:#aaaaaa", 172 | "normal.connected" : "fg:#33aa33", 173 | 174 | "skyblue" : "fg:skyblue", 175 | "yellow" : "fg:yellow", 176 | "red" : "fg:red", 177 | "green" : "fg:green", 178 | "blue" : "fg:blue", 179 | "link" : "fg:green underline", 180 | "title" : "bold", 181 | "value" : "fg:green", 182 | } 183 | 184 | INFO_STYLE = "\x1b[48;5;22m\x1b[38;5;252m" #"\x1b[38;2;0;128;255m" 185 | WARN_STYLE = "\x1b[48;5;220m\x1b[38;5;238m" 186 | ERR_STYLE = "\x1b[48;5;160m\x1b[38;5;252m" 187 | CLR_STYLE = "\x1b[0m" 188 | 189 | @classmethod 190 | def gettext(cls, text: str, *args, **kwargs): 191 | if len(args) == 0 and len(kwargs) == 0: 192 | return cls.text[text] if text in cls.text else text 193 | else: 194 | return cls.text[text].format(*args, **kwargs) if text in cls.text else text.format(*args, **kwargs) 195 | 196 | 197 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # this file is *not* meant to cover or endorse the use of tox or pytest or 2 | # testing in general, 3 | # 4 | # It's meant to show the use of: 5 | # 6 | # - check-manifest 7 | # confirm items checked into vcs are in your sdist 8 | # - readme_renderer (when using a ReStructuredText README) 9 | # confirms your long_description will render correctly on PyPI. 10 | # 11 | # and also to help confirm pull requests to this project. 12 | 13 | [tox] 14 | envlist = py{37,38,39,310,311} 15 | 16 | # Define the minimal tox version required to run; 17 | # if the host tox is less than this the tool with create an environment and 18 | # provision it with a tox that satisfies it under provision_tox_env. 19 | # At least this version is needed for PEP 517/518 support. 20 | minversion = 3.3.0 21 | 22 | # Activate isolated build environment. tox will use a virtual environment 23 | # to build a source distribution from the source tree. For build tools and 24 | # arguments use the pyproject.toml file as specified in PEP-517 and PEP-518. 25 | isolated_build = true 26 | 27 | [testenv] 28 | deps = 29 | check-manifest >= 0.42 30 | # If your project uses README.rst, uncomment the following: 31 | # readme_renderer 32 | flake8 33 | pytest 34 | build 35 | twine 36 | commands = 37 | check-manifest --ignore 'tox.ini,tests/**' 38 | python -m build 39 | python -m twine check dist/* 40 | flake8 . 41 | py.test tests {posargs} 42 | 43 | [flake8] 44 | exclude = .tox,*.egg,build,data 45 | select = E,W,F 46 | --------------------------------------------------------------------------------