pymud 0.20.0a5__py3-none-any.whl → 0.20.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pymud/dialogs.py CHANGED
@@ -1,7 +1,7 @@
1
1
  import asyncio, webbrowser
2
2
 
3
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
4
+ from prompt_toolkit.widgets import Button, Dialog, Label, MenuContainer, MenuItem, TextArea, SystemToolbar, Frame, RadioList
5
5
  from prompt_toolkit.layout.dimension import Dimension, D
6
6
  from prompt_toolkit import ANSI, HTML
7
7
  from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
@@ -133,12 +133,14 @@ class LogSelectionDialog(BasicDialog):
133
133
  def __init__(self, text, values, modal=True):
134
134
  self._header_text = text
135
135
  self._selection_values = values
136
- self._radio_list = RadioList(values = self._selection_values)
136
+ self._itemsCount = len(values)
137
+ if len(values) > 0:
138
+ self._radio_list = RadioList(values = self._selection_values)
139
+ else:
140
+ self._radio_list = Label('无记录'.center(13))
137
141
  super().__init__('选择查看的记录', modal)
138
142
 
139
143
  def create_body(self) -> AnyContainer:
140
-
141
-
142
144
  body=HSplit([
143
145
  Label(text = self._header_text, dont_extend_height=True),
144
146
  self._radio_list
@@ -151,6 +153,9 @@ class LogSelectionDialog(BasicDialog):
151
153
  return [ok_button, cancel_button]
152
154
 
153
155
  def btn_ok_clicked(self):
154
- result = self._radio_list.current_value
155
- self.set_done(result)
156
+ if self._itemsCount:
157
+ result = self._radio_list.current_value
158
+ self.set_done(result)
159
+ else:
160
+ self.set_done(False)
156
161
 
pymud/extras.py CHANGED
@@ -1,8 +1,7 @@
1
1
  # External Libraries
2
2
  from unicodedata import east_asian_width
3
3
  from wcwidth import wcwidth
4
- from typing import Any
5
- import time, datetime
4
+ import time, re, logging
6
5
 
7
6
  from typing import Iterable
8
7
  from prompt_toolkit import ANSI
@@ -65,6 +64,10 @@ class MudFormatProcessor(Processor):
65
64
  self.FULL_BLOCKS = set("▂▃▅▆▇▄█")
66
65
  self.SINGLE_LINES = set("┌└├┬┼┴╭╰─")
67
66
  self.DOUBLE_LINES = set("╔╚╠╦╪╩═")
67
+ self.START_COLOR_REGX = re.compile(r"^\[[\d;]+m")
68
+ self.COLOR_REGX = re.compile(r"\[[\d;]+m")
69
+ self._color_start = ""
70
+ self._color_correction = False
68
71
 
69
72
  def width_correction(self, line: str) -> str:
70
73
  new_str = []
@@ -88,11 +91,36 @@ class MudFormatProcessor(Processor):
88
91
  def tab_correction(self, line: str):
89
92
  return line.replace("\t", " " * Settings.client["tabstop"])
90
93
 
94
+ def color_correction(self, line: str):
95
+ # 注:发现processer处理并非自上而下逐行处理的,因此不能使用这种颜色校正方式。
96
+ if self._color_correction:
97
+ other = self.COLOR_REGX.findall(line)
98
+
99
+ line = f"{self._color_start}{line}"
100
+ logging.debug(f"已校正增加颜色标志 {self._color_start}: {line}")
101
+
102
+ if other:
103
+ self._color_correction = False
104
+ self._color_start = ""
105
+ logging.debug(f"颜色校正结束: {line}")
106
+ else:
107
+ color = self.START_COLOR_REGX.findall(line)
108
+ if color:
109
+ other = self.COLOR_REGX.findall(line)
110
+ if len(other) == 1:
111
+ self._color_correction = True
112
+ self._color_start = color[0]
113
+ logging.debug(f"获取到一个颜色开头 {color[0]}: {line}")
114
+
115
+ return line
116
+
91
117
  def line_correction(self, line: str):
92
118
  # 处理\r符号(^M)
93
119
  line = self.return_correction(line)
94
120
  # 处理Tab(\r)符号(^I)
95
121
  line = self.tab_correction(line)
122
+ # 处理颜色跨行问题。发现processer处理并非自上而下逐行处理的,因此不能使用这种颜色校正方式。
123
+ # line = self.color_correction(line)
96
124
  # 美化(解决中文英文在Console中不对齐的问题)
97
125
  if Settings.client["beautify"]:
98
126
  line = self.width_correction(line)
@@ -430,9 +458,9 @@ class SessionBufferControl(BufferControl):
430
458
 
431
459
  if double_click:
432
460
  start = buffer.document.translate_row_col_to_index(position.y, 0)
433
- end = buffer.document.translate_row_col_to_index(position.y, 10000000)
461
+ end = buffer.document.translate_row_col_to_index(position.y + 1, 0) - 1
434
462
  buffer.cursor_position = start
435
- buffer.start_selection(selection_type=SelectionType.CHARACTERS)
463
+ buffer.start_selection(selection_type=SelectionType.LINES)
436
464
  buffer.cursor_position = end
437
465
 
438
466
  else:
@@ -1008,76 +1036,5 @@ class DotDict(dict):
1008
1036
  def __setstate__(self, state):
1009
1037
  self.update(state)
1010
1038
 
1011
- import importlib
1012
- import importlib.util
1013
-
1014
- class Plugin:
1015
- """
1016
- 插件管理类。对加载的插件文件进行管理。该类型由PyMudApp进行管理,无需人工创建。
1017
-
1018
- 有关插件的详细信息,请参见 `插件 <plugins.html>`_
1019
-
1020
- :param name: 插件的文件名, 如'myplugin.py'
1021
- :param location: 插件所在的目录。自动加载的插件包括PyMUD包目录下的plugins目录以及当前目录下的plugins目录
1022
-
1023
- """
1024
- def __init__(self, name, location):
1025
- self._plugin_file = name
1026
- self._plugin_loc = location
1027
-
1028
- self.reload()
1029
-
1030
- def reload(self):
1031
- "加载/重新加载插件对象"
1032
- #del self.modspec, self.mod
1033
- self.modspec = importlib.util.spec_from_file_location(self._plugin_file[:-3], self._plugin_loc)
1034
- self.mod = importlib.util.module_from_spec(self.modspec)
1035
- self.modspec.loader.exec_module(self.mod)
1036
-
1037
- self._app_init = self.mod.__dict__["PLUGIN_PYMUD_START"]
1038
- self._session_create = self.mod.__dict__["PLUGIN_SESSION_CREATE"]
1039
- self._session_destroy = self.mod.__dict__["PLUGIN_SESSION_DESTROY"]
1040
-
1041
- @property
1042
- def name(self):
1043
- "插件名称,由插件文件中的 PLUGIN_NAME 常量定义"
1044
- return self.mod.__dict__["PLUGIN_NAME"]
1045
-
1046
- @property
1047
- def desc(self):
1048
- "插件描述,由插件文件中的 PLUGIN_DESC 常量定义"
1049
- return self.mod.__dict__["PLUGIN_DESC"]
1050
-
1051
- @property
1052
- def help(self):
1053
- "插件帮助,由插件文件中的文档字符串定义"
1054
- return self.mod.__doc__
1055
-
1056
- def onAppInit(self, app):
1057
- """
1058
- PyMUD应用启动时对插件执行的操作,由插件文件中的 PLUGIN_PYMUD_START 函数定义
1059
-
1060
- :param app: 启动的 PyMudApp 对象实例
1061
- """
1062
- self._app_init(app)
1063
-
1064
- def onSessionCreate(self, session):
1065
- """
1066
- 新会话创建时对插件执行的操作,由插件文件中的 PLUGIN_SESSION_CREATE 函数定义
1067
-
1068
- :param session: 新创建的会话对象实例
1069
- """
1070
- self._session_create(session)
1071
-
1072
- def onSessionDestroy(self, session):
1073
- """
1074
- 会话关闭时(注意不是断开)对插件执行的操作,由插件文件中的 PLUGIN_SESSION_DESTROY 函数定义
1075
-
1076
- :param session: 所关闭的会话对象实例
1077
- """
1078
- self._session_destroy(session)
1079
1039
 
1080
- def __getattr__(self, __name: str) -> Any:
1081
- if hasattr(self.mod, __name):
1082
- return self.mod.__getattribute__(__name)
1083
1040
 
pymud/logger.py CHANGED
@@ -1,9 +1,10 @@
1
- import os, re, datetime, threading
1
+ import os, re, datetime, threading, pathlib
2
2
  from queue import SimpleQueue, Empty
3
+ from pathlib import Path
3
4
 
4
5
  class Logger:
5
6
  """
6
- PyMUD 的记录器类型,可用于会话中向文件记录数据
7
+ PyMUD 的记录器类型,可用于会话中向文件记录数据。记录文件保存在当前目录下的 log 子目录中
7
8
 
8
9
  :param name: 记录器名称,各记录器名称应保持唯一。记录器名称会作为记录文件名称的主要参数
9
10
  :param mode: 记录模式。可选模式包括 a, w, n 三种。
@@ -56,7 +57,12 @@ class Logger:
56
57
  now = datetime.datetime.now().strftime("%Y_%m_%d_%H_%M_%S")
57
58
  filename = f"{self.name}.{now}.log"
58
59
 
59
- filename = os.path.abspath(filename)
60
+ logdir = Path.cwd().joinpath('log')
61
+ if not logdir.exists() or not logdir.is_dir():
62
+ logdir.mkdir()
63
+
64
+ filename = logdir.joinpath(filename)
65
+ #filename = os.path.abspath(filename)
60
66
  self._stream = open(filename, mode = mode, encoding = self._encoding, errors = self._errors)
61
67
  self._thread = t = threading.Thread(target=self._monitor)
62
68
  t.daemon = True
pymud/modules.py CHANGED
@@ -1,11 +1,23 @@
1
1
 
2
- import importlib
2
+ import importlib, importlib.util
3
3
  from abc import ABC, ABCMeta
4
+ from typing import Any
5
+ from .objects import BaseObject, Command
4
6
 
5
7
  class ModuleInfo:
8
+ """
9
+ 模块管理类。对加载的模块文件进行管理。该类型由Session类进行管理,无需人工创建和干预。
10
+
11
+ 有关模块的分类和使用的详细信息,请参见 `脚本 <scripts.html>`_
12
+
13
+ :param module_name: 模块的名称, 应与 import xxx 语法中的 xxx 保持一致
14
+ :param session: 加载/创建本模块的会话
15
+
16
+ """
6
17
  def __init__(self, module_name: str, session):
7
18
  self.session = session
8
19
  self._name = module_name
20
+ self._ismainmodule = False
9
21
  self.load()
10
22
 
11
23
  def _load(self, reload = False):
@@ -20,61 +32,157 @@ class ModuleInfo:
20
32
  if isinstance(attr, type) and attr.__module__ == self._module.__name__:
21
33
  if (attr_name == "Configuration") or issubclass(attr, IConfig):
22
34
  try:
23
- self._config[f"{self.name}.{attr_name}"] = attr(self.session)
24
- self.session.info(f"配置对象 {self.name}.{attr_name} 创建成功.")
35
+ self._config[f"{self.name}.{attr_name}"] = attr(self.session, reload = reload)
36
+ self.session.info(f"配置对象 {self.name}.{attr_name} {'重新' if reload else ''}创建成功.")
25
37
  except Exception as e:
26
38
  result = False
27
39
  self.session.error(f"配置对象 {self.name}.{attr_name} 创建失败. 错误信息为: {e}")
28
-
40
+ self._ismainmodule = (self._config != {})
29
41
  return result
30
42
 
31
43
  def _unload(self):
32
44
  for key, config in self._config.items():
33
- if hasattr(config, "__unload__"):
34
- unload = getattr(config, "__unload__", None)
35
- if callable(unload): unload()
36
-
37
- if hasattr(config, "unload"):
38
- unload = getattr(config, "unload", None)
39
- if callable(unload): unload()
45
+ if isinstance(config, Command):
46
+ # Command 对象在从会话中移除时,自动调用其 unload 系列方法,因此不能产生递归
47
+ self.session.delObject(config)
48
+
49
+ else:
50
+
51
+ if hasattr(config, "__unload__"):
52
+ unload = getattr(config, "__unload__", None)
53
+ if callable(unload): unload()
54
+
55
+ if hasattr(config, "unload"):
56
+ unload = getattr(config, "unload", None)
57
+ if callable(unload): unload()
58
+
59
+ if isinstance(config, BaseObject):
60
+ self.session.delObject(config)
40
61
 
41
62
  del config
42
63
  self._config.clear()
43
64
 
44
65
  def load(self):
66
+ "加载模块内容"
45
67
  if self._load():
46
68
  self.session.info(f"{'主' if self.ismainmodule else '从'}配置模块 {self.name} 加载完成.")
47
69
  else:
48
70
  self.session.error(f"{'主' if self.ismainmodule else '从'}配置模块 {self.name} 加载失败.")
49
71
 
50
72
  def unload(self):
73
+ "卸载模块内容"
51
74
  self._unload()
75
+ self._loaded = False
52
76
  self.session.info(f"{'主' if self.ismainmodule else '从'}配置模块 {self.name} 卸载完成.")
53
77
 
54
78
  def reload(self):
79
+ "模块文件更新后调用,重新加载已加载的模块内容"
55
80
  self._unload()
56
81
  self._load(reload = True)
57
82
  self.session.info(f"{'主' if self.ismainmodule else '从'}配置模块 {self.name} 重新加载完成.")
58
83
 
59
84
  @property
60
85
  def name(self):
86
+ "只读属性,模块名称"
61
87
  return self._name
62
88
 
63
89
  @property
64
90
  def module(self):
91
+ "只读属性,模块文件的 ModuleType 对象"
65
92
  return self._module
66
93
 
67
94
  @property
68
95
  def config(self):
96
+ "只读字典属性,根据模块文件 ModuleType 对象创建的其中名为 Configuration 的类型或继承自 IConfig 的子类型实例(若有)"
69
97
  return self._config
70
98
 
71
99
  @property
72
100
  def ismainmodule(self):
73
- return self._config != {}
101
+ "只读属性,区分是否主模块(即包含具体config的模块)"
102
+ return self._ismainmodule
74
103
 
75
104
  class IConfig(metaclass = ABCMeta):
105
+ """
106
+ 用于提示PyMUD应用是否自动创建该配置类型的基础类(模拟接口)。
107
+
108
+ 继承 IConfig 类型让应用自动管理该类型,唯一需要的是,构造函数中,仅存在一个必须指定的参数 Session。
109
+
110
+ 在应用自动创建 IConfig 实例时,除 session 参数外,还会传递一个 reload 参数 (bool类型),表示是首次加载还是重新加载特性。
111
+ 可以从kwargs 中获取该参数,并针对性的设计相应代码。例如,重新加载相关联的其他模块等。
112
+ """
76
113
  def __init__(self, session, *args, **kwargs):
77
114
  self.session = session
78
115
 
79
116
  def __unload__(self):
80
- self.session.delObject(self)
117
+ if self.session:
118
+ self.session.delObject(self)
119
+
120
+ class Plugin:
121
+ """
122
+ 插件管理类。对加载的插件文件进行管理。该类型由PyMudApp进行管理,无需人工创建。
123
+
124
+ 有关插件的详细信息,请参见 `插件 <plugins.html>`_
125
+
126
+ :param name: 插件的文件名, 如'myplugin.py'
127
+ :param location: 插件所在的目录。自动加载的插件包括PyMUD包目录下的plugins目录以及当前目录下的plugins目录
128
+
129
+ """
130
+ def __init__(self, name, location):
131
+ self._plugin_file = name
132
+ self._plugin_loc = location
133
+
134
+ self.reload()
135
+
136
+ def reload(self):
137
+ "加载/重新加载插件对象"
138
+ #del self.modspec, self.mod
139
+ self.modspec = importlib.util.spec_from_file_location(self._plugin_file[:-3], self._plugin_loc)
140
+ self.mod = importlib.util.module_from_spec(self.modspec)
141
+ self.modspec.loader.exec_module(self.mod)
142
+
143
+ self._app_init = self.mod.__dict__["PLUGIN_PYMUD_START"]
144
+ self._session_create = self.mod.__dict__["PLUGIN_SESSION_CREATE"]
145
+ self._session_destroy = self.mod.__dict__["PLUGIN_SESSION_DESTROY"]
146
+
147
+ @property
148
+ def name(self):
149
+ "插件名称,由插件文件中的 PLUGIN_NAME 常量定义"
150
+ return self.mod.__dict__["PLUGIN_NAME"]
151
+
152
+ @property
153
+ def desc(self):
154
+ "插件描述,由插件文件中的 PLUGIN_DESC 常量定义"
155
+ return self.mod.__dict__["PLUGIN_DESC"]
156
+
157
+ @property
158
+ def help(self):
159
+ "插件帮助,由插件文件中的文档字符串定义"
160
+ return self.mod.__doc__
161
+
162
+ def onAppInit(self, app):
163
+ """
164
+ PyMUD应用启动时对插件执行的操作,由插件文件中的 PLUGIN_PYMUD_START 函数定义
165
+
166
+ :param app: 启动的 PyMudApp 对象实例
167
+ """
168
+ self._app_init(app)
169
+
170
+ def onSessionCreate(self, session):
171
+ """
172
+ 新会话创建时对插件执行的操作,由插件文件中的 PLUGIN_SESSION_CREATE 函数定义
173
+
174
+ :param session: 新创建的会话对象实例
175
+ """
176
+ self._session_create(session)
177
+
178
+ def onSessionDestroy(self, session):
179
+ """
180
+ 会话关闭时(注意不是断开)对插件执行的操作,由插件文件中的 PLUGIN_SESSION_DESTROY 函数定义
181
+
182
+ :param session: 所关闭的会话对象实例
183
+ """
184
+ self._session_destroy(session)
185
+
186
+ def __getattr__(self, __name: str) -> Any:
187
+ if hasattr(self.mod, __name):
188
+ return self.mod.__getattribute__(__name)
pymud/objects.py CHANGED
@@ -322,7 +322,12 @@ class BaseObject:
322
322
  "内部缩写代码前缀"
323
323
 
324
324
  def __init__(self, session, *args, **kwargs):
325
- self.session = session
325
+ from .session import Session
326
+ if isinstance(session, Session):
327
+ self.session = session
328
+ else:
329
+ assert("session must be an instance of class Session!")
330
+
326
331
  self._enabled = True # give a default value
327
332
  self.log = logging.getLogger(f"pymud.{self.__class__.__name__}")
328
333
  self.id = kwargs.get("id", session.getUniqueID(self.__class__.__abbr__))
@@ -395,7 +400,8 @@ class BaseObject:
395
400
  return self.__detailed__()
396
401
 
397
402
  def __detailed__(self) -> str:
398
- return f'<{self.__class__.__name__}> id = "{self.id}" group = "{self.group}" enabled = {self.enabled}'
403
+ group = f'group = "{self.group}" ' if self.group else ''
404
+ return f'<{self.__class__.__name__}> id = "{self.id}" {group}enabled = {self.enabled}'
399
405
 
400
406
  class GMCPTrigger(BaseObject):
401
407
  """
@@ -445,7 +451,8 @@ class GMCPTrigger(BaseObject):
445
451
  self._onSuccess(self.id, value, value_exp)
446
452
 
447
453
  def __detailed__(self) -> str:
448
- return f'<{self.__class__.__name__}> name = "{self.id}" value = "{self.value}" group = "{self.group}" enabled = {self.enabled} '
454
+ group = f'group = "{self.group}" ' if self.group else ''
455
+ return f'<{self.__class__.__name__}> name = "{self.id}" value = "{self.value}" {group}enabled = {self.enabled} '
449
456
 
450
457
  class MatchObject(BaseObject):
451
458
  """
@@ -519,11 +526,11 @@ class MatchObject(BaseObject):
519
526
  self._mline = 0
520
527
 
521
528
  def reset(self):
522
- "复位事件,用于async执行未等待结果时,对事件的复位"
529
+ "复位事件,用于async执行未等待结果时,对事件的复位。仅异步有效。"
523
530
  self.event.clear()
524
531
 
525
532
  def set(self):
526
- "设置事件标记,用于人工强制触发"
533
+ "设置事件标记,可以用于人工强制触发,仅在异步触发器下生效。"
527
534
  self.event.set()
528
535
 
529
536
  def match(self, line: str, docallback = True) -> BaseObject.State:
@@ -590,14 +597,16 @@ class MatchObject(BaseObject):
590
597
  state = BaseObject.State(result, self.id, "\n".join(self.lines), tuple(self.wildcards))
591
598
 
592
599
  # 采用回调方式执行的时候,执行函数回调(仅当self.sync和docallback均为真时才执行同步
593
- if self.sync and docallback:
594
- if state.result == self.SUCCESS:
595
- self._onSuccess(state.id, state.line, state.wildcards)
596
- self.event.set()
597
- elif state.result == self.FAILURE:
598
- self._onFailure(state.id, state.line, state.wildcards)
599
- elif state.result == self.TIMEOUT:
600
- self._onTimeout(state.id, state.line, state.wildcards)
600
+ # docallback为真时,是真正的进行匹配和触发,为false时,仅返回匹配结果,不实际触发
601
+ if docallback:
602
+ self.event.set()
603
+ if self.sync:
604
+ if state.result == self.SUCCESS:
605
+ self._onSuccess(state.id, state.line, state.wildcards)
606
+ elif state.result == self.FAILURE:
607
+ self._onFailure(state.id, state.line, state.wildcards)
608
+ elif state.result == self.TIMEOUT:
609
+ self._onTimeout(state.id, state.line, state.wildcards)
601
610
 
602
611
  self.state = state
603
612
  return state
@@ -619,7 +628,8 @@ class MatchObject(BaseObject):
619
628
  return self.state
620
629
 
621
630
  def __detailed__(self) -> str:
622
- return f'<{self.__class__.__name__}> id = "{self.id}" group = "{self.group}" enabled = {self.enabled} patterns = "{self.patterns}"'
631
+ group = f'group = "{self.group}" ' if self.group else ''
632
+ return f'<{self.__class__.__name__}> id = "{self.id}" {group}enabled = {self.enabled} patterns = "{self.patterns}"'
623
633
 
624
634
  class Alias(MatchObject):
625
635
  """
@@ -649,7 +659,8 @@ class SimpleAlias(Alias):
649
659
  self._codeblock.execute(self.session, id = id, line = line, wildcards = wildcards)
650
660
 
651
661
  def __detailed__(self) -> str:
652
- return f'<{self.__class__.__name__}> id = "{self.id}" group = "{self.group}" enabled = {self.enabled} patterns = "{self.patterns}" code = "{self._code}"'
662
+ group = f'group = "{self.group}" ' if self.group else ''
663
+ return f'<{self.__class__.__name__}> id = "{self.id}" {group}enabled = {self.enabled} patterns = "{self.patterns}" code = "{self._code}"'
653
664
 
654
665
  def __repr__(self) -> str:
655
666
  return self.__detailed__()
@@ -700,7 +711,8 @@ class SimpleTrigger(Trigger):
700
711
  self._codeblock.execute(self.session, id = id, line = line, raw = raw, wildcards = wildcards)
701
712
 
702
713
  def __detailed__(self) -> str:
703
- return f'<{self.__class__.__name__}> id = "{self.id}" group = "{self.group}" enabled = {self.enabled} patterns = "{self.patterns}" code = "{self._code}"'
714
+ group = f'group = "{self.group}" ' if self.group else ''
715
+ return f'<{self.__class__.__name__}> id = "{self.id}" {group}enabled = {self.enabled} patterns = "{self.patterns}" code = "{self._code}"'
704
716
 
705
717
  def __repr__(self) -> str:
706
718
  return self.__detailed__()
@@ -986,7 +998,8 @@ class Timer(BaseObject):
986
998
  self.startTimer()
987
999
 
988
1000
  def __detailed__(self) -> str:
989
- return f'<{self.__class__.__name__}> id = "{self.id}" group = "{self.group}" enabled = {self.enabled} timeout = {self.timeout}'
1001
+ group = f'group = "{self.group}" ' if self.group else ''
1002
+ return f'<{self.__class__.__name__}> id = "{self.id}" {group}enabled = {self.enabled} timeout = {self.timeout}'
990
1003
 
991
1004
  def __repr__(self) -> str:
992
1005
  return self.__detailed__()
@@ -1008,5 +1021,6 @@ class SimpleTimer(Timer):
1008
1021
  self._codeblock.execute(self.session, id = id)
1009
1022
 
1010
1023
  def __detailed__(self) -> str:
1011
- return f'<{self.__class__.__name__}> id = "{self.id}" group = "{self.group}" enabled = {self.enabled} timeout = {self.timeout} code = "{self._code}"'
1024
+ group = f'group = "{self.group}" ' if self.group else ''
1025
+ return f'<{self.__class__.__name__}> id = "{self.id}" {group}enabled = {self.enabled} timeout = {self.timeout} code = "{self._code}"'
1012
1026
 
pymud/pymud.py CHANGED
@@ -1,6 +1,6 @@
1
- import asyncio, functools, re, logging, math, json, os, webbrowser, threading
1
+ import asyncio, functools, re, os, webbrowser, threading
2
2
  from datetime import datetime
3
- import importlib.util
3
+ from pathlib import Path
4
4
  from prompt_toolkit.shortcuts import set_title, radiolist_dialog
5
5
  from prompt_toolkit.output import ColorDepth
6
6
  from prompt_toolkit.clipboard.pyperclip import PyperclipClipboard
@@ -36,7 +36,8 @@ from prompt_toolkit.layout.processors import (
36
36
  from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
37
37
 
38
38
  from .objects import CodeBlock
39
- from .extras import MudFormatProcessor, SessionBuffer, EasternMenuContainer, VSplitWindow, SessionBufferControl, DotDict, Plugin
39
+ from .extras import MudFormatProcessor, SessionBuffer, EasternMenuContainer, VSplitWindow, SessionBufferControl, DotDict
40
+ from .modules import Plugin
40
41
  from .session import Session
41
42
  from .settings import Settings
42
43
  from .dialogs import MessageDialog, WelcomeDialog, QueryDialog, NewSessionDialog, LogSelectionDialog
@@ -92,6 +93,7 @@ class PyMudApp:
92
93
  self._mouse_support = True
93
94
  self._plugins = DotDict() # 增加 插件 字典
94
95
  self._globals = DotDict() # 增加所有session使用的全局变量
96
+ self._onTimerCallbacks = dict()
95
97
  self.sessions = {}
96
98
  self.current_session = None
97
99
  self.status_display = STATUS_DISPLAY(Settings.client["status_display"])
@@ -149,6 +151,24 @@ class PyMudApp:
149
151
 
150
152
  self.load_plugins()
151
153
 
154
+ async def onSystemTimerTick(self):
155
+ while True:
156
+ await asyncio.sleep(1)
157
+ self.app.invalidate()
158
+ for callback in self._onTimerCallbacks.values():
159
+ if callable(callback):
160
+ callback()
161
+
162
+ def addTimerTickCallback(self, name, func):
163
+ '注册一个系统定时器回调,每1s触发一次。指定name为回调函数关键字,func为回调函数。'
164
+ if callable(func) and (not name in self._onTimerCallbacks.keys()):
165
+ self._onTimerCallbacks[name] = func
166
+
167
+ def removeTimerTickCallback(self, name):
168
+ '从系统定时器回调中移除一个回调函数。指定name为回调函数关键字。'
169
+ if name in self._onTimerCallbacks.keys():
170
+ self._onTimerCallbacks.pop(name)
171
+
152
172
  def initUI(self):
153
173
  """初始化UI界面"""
154
174
  self.style = Style.from_dict(Settings.styles)
@@ -567,6 +587,7 @@ class PyMudApp:
567
587
  log_list = list()
568
588
  files = [f for f in os.listdir('.') if os.path.isfile(f) and f.endswith('.log')]
569
589
  for file in files:
590
+ file = os.path.abspath(file)
570
591
  filename = os.path.basename(file).ljust(20)
571
592
  filesize = f"{os.path.getsize(file):,} Bytes".rjust(20)
572
593
  # ctime = datetime.fromtimestamp(os.path.getctime(file)).strftime('%Y-%m-%d %H:%M:%S').rjust(23)
@@ -574,6 +595,19 @@ class PyMudApp:
574
595
 
575
596
  file_display_line = "{}{}{}".format(filename, filesize, mtime)
576
597
  log_list.append((file, file_display_line))
598
+
599
+ logDir = os.path.abspath(os.path.join(os.curdir, 'log'))
600
+ if os.path.exists(logDir):
601
+ files = [f for f in os.listdir(logDir) if f.endswith('.log')]
602
+ for file in files:
603
+ file = os.path.join(logDir, file)
604
+ filename = ('log/' + os.path.basename(file)).ljust(20)
605
+ filesize = f"{os.path.getsize(file):,} Bytes".rjust(20)
606
+ # ctime = datetime.fromtimestamp(os.path.getctime(file)).strftime('%Y-%m-%d %H:%M:%S').rjust(23)
607
+ mtime = datetime.fromtimestamp(os.path.getmtime(file)).strftime('%Y-%m-%d %H:%M:%S').rjust(23)
608
+
609
+ file_display_line = "{}{}{}".format(filename, filesize, mtime)
610
+ log_list.append((file, file_display_line))
577
611
 
578
612
  dialog = LogSelectionDialog(
579
613
  text = head_line,
@@ -1130,16 +1164,17 @@ class PyMudApp:
1130
1164
 
1131
1165
  async def run_async(self):
1132
1166
  "以异步方式运行本程序"
1133
- await self.app.run_async()
1167
+ asyncio.create_task(self.onSystemTimerTick())
1168
+ await self.app.run_async(set_exception_handler = False)
1134
1169
 
1135
1170
  def run(self):
1136
1171
  "运行本程序"
1137
- self.app.run()
1138
- #asyncio.run(self.run_async())
1172
+ #self.app.run(set_exception_handler = False)
1173
+ asyncio.run(self.run_async())
1139
1174
 
1140
1175
  def get_width(self):
1141
- "获取ConsoleView的实际宽度,等于输出宽度-4,(左右线条宽度, 滚动条宽度,右边让出的1列)"
1142
- size = self.app.output.get_size().columns - 4
1176
+ "获取ConsoleView的实际宽度,等于输出宽度,(已经没有左右线条和滚动条了)"
1177
+ size = self.app.output.get_size().columns
1143
1178
  if Settings.client["status_display"] == 2:
1144
1179
  size = size - Settings.client["status_width"] - 1
1145
1180
  return size
pymud/session.py CHANGED
@@ -4,9 +4,9 @@ from collections import OrderedDict
4
4
  import logging, queue
5
5
  from logging import FileHandler
6
6
  from logging.handlers import QueueHandler, QueueListener
7
-
7
+ from wcwidth import wcswidth, wcwidth
8
8
  from .logger import Logger
9
- from .extras import SessionBuffer, DotDict, Plugin
9
+ from .extras import SessionBuffer, DotDict
10
10
  from .protocol import MudClientProtocol
11
11
  from .modules import ModuleInfo
12
12
  from .objects import BaseObject, Trigger, Alias, Command, Timer, SimpleAlias, SimpleTrigger, SimpleTimer, GMCPTrigger, CodeBlock, CodeLine
@@ -100,6 +100,7 @@ class Session:
100
100
  "ig" : "ignore",
101
101
  "t+" : "ignore",
102
102
  "t-" : "ignore",
103
+ "show": "test",
103
104
  }
104
105
 
105
106
  def __init__(self, app, name, host, port, encoding = None, after_connect = None, loop = None, **kwargs):
@@ -558,7 +559,7 @@ class Session:
558
559
 
559
560
  半数的数量由 Settings.client['buffer_lines'] 确定,默认为5000行。
560
561
  """
561
- if (self._line_count >= 2 * Settings.client["buffer_lines"]) and self.buffer.document.is_cursor_at_the_end:
562
+ if (Settings.client["buffer_lines"] > 0) and (self._line_count >= 2 * Settings.client["buffer_lines"]) and self.buffer.document.is_cursor_at_the_end:
562
563
  self._line_count = self.buffer.clear_half()
563
564
 
564
565
  def feed_data(self, data) -> None:
@@ -653,24 +654,25 @@ class Session:
653
654
  self.display_line = raw_line
654
655
 
655
656
  if not self._ignore:
656
- all_tris = list(self._triggers.values())
657
+ # 修改实现,形成列表时即排除非使能状态触发器,加快响应速度
658
+ #all_tris = list(self._triggers.values())
659
+ all_tris = [tri for tri in self._triggers.values() if isinstance(tri, Trigger) and tri.enabled]
657
660
  all_tris.sort(key = lambda tri: tri.priority)
658
661
 
659
662
  for tri in all_tris:
660
- if isinstance(tri, Trigger) and tri.enabled:
661
- if tri.raw:
662
- state = tri.match(raw_line, docallback = True)
663
- else:
664
- state = tri.match(tri_line, docallback = True)
663
+ if tri.raw:
664
+ state = tri.match(raw_line, docallback = True)
665
+ else:
666
+ state = tri.match(tri_line, docallback = True)
665
667
 
666
- if state.result == Trigger.SUCCESS:
667
- if tri.oneShot: # 仅执行一次的trigger,匹配成功后,删除该Trigger(从触发器列表中移除)
668
- self._triggers.pop(tri.id)
668
+ if state.result == Trigger.SUCCESS:
669
+ if tri.oneShot: # 仅执行一次的trigger,匹配成功后,删除该Trigger(从触发器列表中移除)
670
+ self._triggers.pop(tri.id)
669
671
 
670
- if not tri.keepEval: # 非持续匹配的trigger,匹配成功后停止检测后续Trigger
671
- break
672
- else:
673
- pass
672
+ if not tri.keepEval: # 非持续匹配的trigger,匹配成功后停止检测后续Trigger
673
+ break
674
+ else:
675
+ pass
674
676
 
675
677
  # 将数据写入缓存添加到此处
676
678
  if len(self.display_line) > 0:
@@ -764,7 +766,7 @@ class Session:
764
766
  lines = line.split(self.seperator)
765
767
  for ln in lines:
766
768
  if Settings.client["echo_input"]:
767
- self.writetobuffer(f"\x1b[32m{ln}\x1b[0m")
769
+ self.writetobuffer(f"\x1b[32m{ln}\x1b[0m", True)
768
770
  else:
769
771
  self.log.log(f"\x1b[32m{ln}\x1b[0m\n")
770
772
 
@@ -773,7 +775,7 @@ class Session:
773
775
 
774
776
  else:
775
777
  if Settings.client["echo_input"]:
776
- self.writetobuffer(f"\x1b[32m{line}\x1b[0m")
778
+ self.writetobuffer(f"\x1b[32m{line}\x1b[0m", True)
777
779
  else:
778
780
  self.log.log(f"\x1b[32m{line}\x1b[0m\n")
779
781
 
@@ -1280,6 +1282,8 @@ class Session:
1280
1282
  """
1281
1283
  从会话中移除一个对象,可直接删除 Alias, Trigger, GMCPTrigger, Command, Timer 或它们的子类本身
1282
1284
 
1285
+ ** 注 ** 现在 delObject 和 delObjects 使用结果相同,都可以清除单个对象、对个对象的list, tuple或dict, 可以有效防止代码写错
1286
+
1283
1287
  :param obj: 要删除的多个特定对象组成的元组、列表或者字典,可以为 Alias, Trigger, GMCPTrigger, Command, Timer 或其子类
1284
1288
 
1285
1289
  示例:
@@ -1316,10 +1320,15 @@ class Session:
1316
1320
  elif isinstance(obj, GMCPTrigger):
1317
1321
  self._gmcp.pop(obj.id, None)
1318
1322
 
1323
+ elif isinstance(obj, (list, tuple, dict)):
1324
+ self.delObjects(obj)
1325
+
1319
1326
  def delObjects(self, objs):
1320
1327
  """
1321
1328
  从会话中移除一组对象,可直接删除多个 Alias, Trigger, GMCPTrigger, Command, Timer
1322
1329
 
1330
+ ** 注 ** 现在 delObject 和 delObjects 使用结果相同,都可以清除单个对象、对个对象的list, tuple或dict, 可以有效防止代码写错
1331
+
1323
1332
  :param objs: 要删除的一组对象的元组、列表或者字典(保持兼容性),其中对象可以为 Alias, Trigger, GMCPTrigger, Command, Timer 或它们的子类
1324
1333
 
1325
1334
  示例:
@@ -1351,6 +1360,9 @@ class Session:
1351
1360
  for key, item in objs.items():
1352
1361
  self.delObject(item)
1353
1362
 
1363
+ elif isinstance(objs, BaseObject):
1364
+ self.delObject(objs)
1365
+
1354
1366
  def addAliases(self, alis):
1355
1367
  """
1356
1368
  向会话中增加多个别名
@@ -1952,7 +1964,7 @@ class Session:
1952
1964
  else:
1953
1965
  vars_simple[k] = v
1954
1966
 
1955
- width = self.application.get_width()
1967
+ width = self.application.get_width() - 2 # 保留2个字符,防止 > 导致换行
1956
1968
 
1957
1969
  title = f" VARIABLE LIST IN SESSION {self.name} "
1958
1970
  left = (width - len(title)) // 2
@@ -1960,6 +1972,7 @@ class Session:
1960
1972
  self.writetobuffer("="*left + title + "="*right, newline = True)
1961
1973
 
1962
1974
  # print vars in simple, 每个变量占40格,一行可以多个变量
1975
+ # 这里可以考虑调整一下,默认40, 但如果一个变量值太长,则选择占两个位置
1963
1976
  var_count = len(vars_simple)
1964
1977
  var_per_line = (width - 2) // 40
1965
1978
  lines = math.ceil(var_count / var_per_line)
@@ -1975,14 +1988,18 @@ class Session:
1975
1988
  self.writetobuffer(" " * left_space)
1976
1989
  line_vars = var_keys[start:end]
1977
1990
  for var in line_vars:
1978
- self.writetobuffer("{0:>18} = {1:<19}".format(var, vars_simple[var].__repr__()))
1991
+ repr = vars_simple[var].__repr__()
1992
+ vwidth = 22 - (wcswidth(repr) - len(repr))
1993
+ self.writetobuffer("{0} = {1}".format(var.rjust(20), repr.ljust(vwidth)))
1994
+ #self.writetobuffer("{0:>18} = {1:<19}".format(var, vars_simple[var].__repr__()))
1979
1995
 
1980
1996
  self.writetobuffer("", newline = True)
1981
1997
 
1982
1998
  # print vars in complex, 每个变量占1行
1983
- for k, v in vars_complex.items():
1999
+ var_keys = sorted(vars_complex.keys())
2000
+ for key in var_keys:
1984
2001
  self.writetobuffer(" " * left_space)
1985
- self.writetobuffer("{0:>18} = {1}".format(k, v.__repr__()), newline = True)
2002
+ self.writetobuffer("{0:>20} = {1}".format(key, vars_complex[key].__repr__()), newline = True)
1986
2003
 
1987
2004
  self.writetobuffer("="*width, newline = True)
1988
2005
 
@@ -2035,7 +2052,7 @@ class Session:
2035
2052
  else:
2036
2053
  vars_simple[k] = v
2037
2054
 
2038
- width = self.application.get_width()
2055
+ width = self.application.get_width() - 2 # 保留2个字符,防止 > 导致换行
2039
2056
 
2040
2057
  title = f" GLOBAL VARIABLES LIST "
2041
2058
  left = (width - len(title)) // 2
@@ -2058,21 +2075,23 @@ class Session:
2058
2075
  self.writetobuffer(" " * left_space)
2059
2076
  line_vars = var_keys[start:end]
2060
2077
  for var in line_vars:
2061
- self.writetobuffer("{0:>18} = {1:<19}".format(var, vars_simple[var].__repr__()))
2078
+ repr = vars_simple[var].__repr__()
2079
+ vwidth = 22 - (wcswidth(repr) - len(repr))
2080
+ self.writetobuffer("{0} = {1}".format(var.rjust(20), repr.ljust(vwidth)))
2062
2081
 
2063
2082
  self.writetobuffer("", newline = True)
2064
2083
 
2065
2084
  # print vars in complex, 每个变量占1行
2066
2085
  for k, v in vars_complex.items():
2067
2086
  self.writetobuffer(" " * left_space)
2068
- self.writetobuffer("{0:>18} = {1}".format(k, v.__repr__()), newline = True)
2087
+ self.writetobuffer("{0:>20} = {1}".format(k, v.__repr__()), newline = True)
2069
2088
 
2070
2089
  self.writetobuffer("="*width, newline = True)
2071
2090
 
2072
2091
  elif len(args) == 1:
2073
2092
  var = args[0]
2074
2093
  if var in self.application.globals.keys():
2075
- self.info("{0:>18} = {1:<19}".format(var, self.application.get_globals(var).__repr__()), "全局变量")
2094
+ self.info("{0:>20} = {1:<22}".format(var, self.application.get_globals(var).__repr__()), "全局变量")
2076
2095
  else:
2077
2096
  self.info("全局空间不存在名称为 {} 的变量".format(var), "全局变量")
2078
2097
 
@@ -2714,7 +2733,7 @@ class Session:
2714
2733
 
2715
2734
  elif mod in self.plugins.keys():
2716
2735
  self.application.reload_plugin(self.plugins[mod])
2717
-
2736
+ self.info(f'插件 {mod} 重新加载完成!')
2718
2737
  else:
2719
2738
  self.warning(f"指定名称 {mod} 既未找到模块,也未找到插件,重新加载失败..")
2720
2739
 
@@ -2855,19 +2874,26 @@ class Session:
2855
2874
 
2856
2875
  def handle_test(self, code: CodeLine = None, *args, **kwargs):
2857
2876
  '''
2858
- 嵌入命令 #test 的执行函数,触发器测试命令。类似于zmud的#show命令。
2877
+ 嵌入命令 #test / #show 的执行函数,触发器测试命令。类似于zmud的#show命令。
2859
2878
  该函数不应该在代码中直接调用。
2860
2879
 
2861
2880
  使用:
2862
- - #test {some_text}: 测试服务器收到{some_text}时的触发器响应情况
2881
+ - #test {some_text}: 测试服务器收到{some_text}时的触发器响应情况。此时,触发器不会真的响应。
2882
+ - #tt {some_test}: 与#test 的差异是,若存在匹配的触发器,无论其是否被使能,该触发器均会实际响应。
2863
2883
 
2864
2884
  示例:
2865
- - ``#test 你深深吸了口气,站了起来。`` : 模拟服务器收到“你深深吸了口气,站了起来。”时的情况进行触发测试
2885
+ - ``#test 你深深吸了口气,站了起来。`` : 模拟服务器收到“你深深吸了口气,站了起来。”时的情况进行触发测试(仅显示触发测试情况)
2866
2886
  - ``#test %copy``: 复制一句话,模拟服务器再次收到复制的这句内容时的情况进行触发器测试
2887
+ - ``#test 你深深吸了口气,站了起来。`` : 模拟服务器收到“你深深吸了口气,站了起来。”时的情况进行触发测试(会实际导致触发器动作)
2867
2888
 
2868
2889
  注意:
2869
- - #test命令测试触发器时,enabled为False的触发器不会响应。
2890
+ - #test命令测试触发器时,触发器不会真的响应。
2891
+ - #tt命令测试触发器时,触发器无论是否使能,均会真的响应。
2870
2892
  '''
2893
+ cmd = code.code[1].lower()
2894
+ docallback = False
2895
+ if cmd == "test":
2896
+ docallback = True
2871
2897
 
2872
2898
  new_cmd_text, new_code = code.expand(self, *args, **kwargs)
2873
2899
  line = new_cmd_text[6:] # 取出#test 之后的所有内容
@@ -2878,32 +2904,82 @@ class Session:
2878
2904
  lines = []
2879
2905
  lines.append(line)
2880
2906
 
2907
+ info_all = []
2908
+ info_enabled = [] # 组织好每一行显示的内容之后,统一输出,不逐行info
2909
+ info_disabled = []
2910
+ triggered = 0
2911
+ triggered_enabled = 0
2912
+ triggered_disabled = 0
2913
+
2914
+
2915
+ tris_enabled = [tri for tri in self._triggers.values() if isinstance(tri, Trigger) and tri.enabled]
2916
+ tris_enabled.sort(key = lambda tri: tri.priority)
2917
+
2918
+ tris_disabled = [tri for tri in self._triggers.values() if isinstance(tri, Trigger) and not tri.enabled]
2919
+ tris_disabled.sort(key = lambda tri: tri.priority)
2920
+
2881
2921
  for raw_line in lines:
2882
- #raw_line = "".join(args)
2883
2922
  tri_line = self.getPlainText(raw_line)
2923
+
2924
+ block = False
2925
+ for tri in tris_enabled:
2926
+ if tri.raw:
2927
+ state = tri.match(raw_line, docallback = docallback)
2928
+ else:
2929
+ state = tri.match(tri_line, docallback = docallback)
2930
+
2931
+ if state.result == Trigger.SUCCESS:
2932
+ triggered_enabled += 1
2933
+ if not block:
2934
+ triggered += 1
2935
+ info_enabled.append(f" {Settings.INFO_STYLE}{tri.__detailed__()} 正常触发。{Settings.CLR_STYLE}")
2936
+ info_enabled.append(f" {Settings.INFO_STYLE}捕获:{state.wildcards}{Settings.CLR_STYLE}")
2937
+
2938
+ if not tri.keepEval: # 非持续匹配的trigger,匹配成功后停止检测后续Trigger
2939
+ info_enabled.append(f" {Settings.WARN_STYLE}该触发器未开启keepEval, 会阻止后续触发器。{Settings.CLR_STYLE}")
2940
+ block = True
2941
+ else:
2942
+ info_enabled.append(f" {Settings.WARN_STYLE}{tri.__detailed__()} 可以触发,但由于优先级与keepEval设定,触发器不会触发。{Settings.CLR_STYLE}")
2943
+
2944
+
2945
+ for tri in tris_disabled:
2946
+ if tri.raw:
2947
+ state = tri.match(raw_line, docallback = docallback)
2948
+ else:
2949
+ state = tri.match(tri_line, docallback = docallback)
2884
2950
 
2885
- all_tris = list(self._triggers.values())
2886
- all_tris.sort(key = lambda tri: tri.priority)
2951
+ if state.result == Trigger.SUCCESS:
2952
+ triggered_disabled += 1
2953
+ info_disabled.append(f" {Settings.INFO_STYLE}{tri.__detailed__()} 可以匹配触发。{Settings.CLR_STYLE}")
2887
2954
 
2888
- for tri in all_tris:
2889
- if isinstance(tri, Trigger) and tri.enabled:
2890
- if tri.raw:
2891
- state = tri.match(raw_line, docallback = True)
2892
- else:
2893
- state = tri.match(tri_line, docallback = True)
2955
+ if triggered_enabled + triggered_disabled == 0:
2956
+ info_all.append("")
2894
2957
 
2895
- if state.result == Trigger.SUCCESS:
2896
- self.info(f"TRIGGER {tri.id} 被触发", "PYMUD TRIGGER TEST")
2897
- if tri.oneShot: # 仅执行一次的trigger,匹配成功后,删除该Trigger(从触发器列表中移除)
2898
- self._triggers.pop(tri.id)
2958
+ if triggered_enabled == 0:
2959
+ info_enabled.insert(0, f"使能的触发器中,没有可以触发的。")
2960
+ elif triggered < triggered_enabled:
2961
+ info_enabled.insert(0, f"使能的触发器中,共有 {triggered_enabled} 个可以触发,实际触发 {triggered} 个,另有 {triggered_enabled - triggered} 个由于 keepEval 原因实际不会触发。")
2962
+ else:
2963
+ info_enabled.insert(0, f"使能的触发器中,共有 {triggered_enabled} 个全部可以被正常触发。")
2899
2964
 
2900
- if not tri.keepEval: # 非持续匹配的trigger,匹配成功后停止检测后续Trigger
2901
- break
2902
- else:
2903
- pass
2965
+ if triggered_disabled > 0:
2966
+ info_disabled.insert(0, f"未使能的触发器中,共有 {triggered_disabled} 个可以匹配。")
2967
+ else:
2968
+ info_disabled.insert(0, f"未使能触发器,没有可以匹配的。")
2969
+
2970
+ if triggered_enabled + triggered_disabled == 0:
2971
+ info_all.append(f"PYMUD 触发器测试: {'响应模式' if docallback else '测试模式'}")
2972
+ info_all.append(f" 测试内容: {line}")
2973
+ info_all.append(f" 测试结果: 没有可以匹配的触发器。")
2974
+ else:
2975
+ info_all.append(f"PYMUD 触发器测试: {'响应模式' if docallback else '测试模式'}")
2976
+ info_all.append(f" 测试内容: {line}")
2977
+ info_all.append(f" 测试结果: 有{triggered}个触发器可以被正常触发,一共有{triggered_enabled + triggered_disabled}个满足匹配触发要求。")
2978
+ info_all.extend(info_enabled)
2979
+ info_all.extend(info_disabled)
2904
2980
 
2905
- if len(raw_line) > 0:
2906
- self.info(raw_line, "PYMUD TRIGGER TEST")
2981
+ self.info("\n".join(info_all), "PYMUD 触发器测试")
2982
+ #self.info("PYMUD 触发器测试 完毕")
2907
2983
 
2908
2984
  def handle_plugins(self, code: CodeLine = None, *args, **kwargs):
2909
2985
  '''
@@ -2934,7 +3010,7 @@ class Session:
2934
3010
  if name in self.plugins.keys():
2935
3011
  plugin = self.plugins[name]
2936
3012
  self.info(f"{plugin.desc['DESCRIPTION']}, 版本 {plugin.desc['VERSION']} 作者 {plugin.desc['AUTHOR']} 发布日期 {plugin.desc['RELEASE_DATE']}", f"PLUGIN {name}")
2937
- self.writetobuffer(plugin.help)
3013
+ self.writetobuffer(plugin.help, True)
2938
3014
 
2939
3015
  def handle_replace(self, code: CodeLine = None, *args, **kwargs):
2940
3016
  '''
@@ -3054,7 +3130,8 @@ class Session:
3054
3130
  new_lines.append("{}{}".format(style, line))
3055
3131
 
3056
3132
  msg = Settings.client["newline"].join(new_lines)
3057
-
3133
+
3134
+ # 将颜色跨行显示移动到了MudFormatProcessor中,此处无需再处理(不行,还得恢复)
3058
3135
  self.writetobuffer("{}[{}] {}{}".format(style, title, msg, Settings.CLR_STYLE), newline = True)
3059
3136
 
3060
3137
  def info(self, msg, title = "PYMUD INFO", style = Settings.INFO_STYLE):
@@ -3113,8 +3190,8 @@ class Session:
3113
3190
 
3114
3191
  示例:
3115
3192
  - ``#log`` : 在当前会话窗口列出所有记录器状态
3116
- - ``#log start`` : 启动本会话默认记录器(记录器名为会话名)。该记录器以纯文本模式,将后续所有屏幕输出、键盘键入、命令输入等记录到 name.log 文件的后端
3117
- - ``#log start -r`` : 启动本会话默认记录器。该记录器以raw模式,将后续所有屏幕输出、键盘键入、命令输入等记录到 name.log 文件的后端
3193
+ - ``#log start`` : 启动本会话默认记录器(记录器名为会话名)。该记录器以纯文本模式,将后续所有屏幕输出、键盘键入、命令输入等记录到 log 目录下 name.log 文件的后端
3194
+ - ``#log start -r`` : 启动本会话默认记录器。该记录器以raw模式,将后续所有屏幕输出、键盘键入、命令输入等记录到 log 目录下 name.log 文件的后端
3118
3195
  - ``#log start chat`` : 启动名为 chat 的记录器。该记录器以纯文本模式,记录代码中调用过该记录器 .log 进行记录的信息
3119
3196
  - ``#log stop`` : 停止本会话默认记录器(记录器名为会话名)。
3120
3197
 
@@ -3172,8 +3249,19 @@ class Session:
3172
3249
  elif (args[0] == "show"):
3173
3250
  if len(args) > 1 and not args[1].startswith('-'):
3174
3251
  file = args[1]
3175
- self.application.logFileShown = file
3176
- self.application.showLogInTab()
3252
+ if os.path.exists(file):
3253
+ filepath = os.path.abspath(file)
3254
+ #self.info(f'file {filepath} exists, will be shown.')
3255
+ self.application.logFileShown = filepath
3256
+ self.application.showLogInTab()
3257
+ elif os.path.exists(os.path.join('./log', file)):
3258
+ filepath = os.path.abspath(os.path.join('./log', file))
3259
+ #self.info(f'file {filepath} exists, will be shown.')
3260
+ self.application.logFileShown = filepath
3261
+ self.application.showLogInTab()
3262
+ else:
3263
+ self.warning(f'指定记录文件 {file} 不存在!')
3264
+
3177
3265
  else:
3178
3266
  self.application.show_logSelectDialog()
3179
3267
 
pymud/settings.py CHANGED
@@ -11,9 +11,9 @@ class Settings:
11
11
  "APP 名称, 默认PYMUD"
12
12
  __appdesc__ = "a MUD client written in Python"
13
13
  "APP 简要描述"
14
- __version__ = "0.20.0"
14
+ __version__ = "0.20.1"
15
15
  "APP 当前版本"
16
- __release__ = "2024-08-19"
16
+ __release__ = "2024-11-16"
17
17
  "APP 当前版本发布日期"
18
18
  __author__ = "本牛(newstart)@北侠"
19
19
  "APP 作者"
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pymud
3
- Version: 0.20.0a5
3
+ Version: 0.20.1
4
4
  Summary: a MUD Client written in Python
5
- Author-email: "newstart@pkuxkx" <crapex@crapex.cc>
6
- Maintainer-email: "newstart@pkuxkx" <crapex@crapex.cc>
5
+ Author-email: "newstart@pkuxkx" <crapex@hotmail.com>
6
+ Maintainer-email: "newstart@pkuxkx" <crapex@hotmail.com>
7
7
  License: GNU GENERAL PUBLIC LICENSE
8
8
  Version 3, 29 June 2007
9
9
 
@@ -684,7 +684,7 @@ Project-URL: Bug Reports, https://github.com/crapex/pymud/issues
684
684
  Project-URL: Source, https://github.com/crapex/pymud/
685
685
  Project-URL: document, https://pymud.readthedocs.io/
686
686
  Keywords: MUD,multi-user dungeon,client
687
- Classifier: Development Status :: 3 - Alpha
687
+ Classifier: Development Status :: 5 - Production/Stable
688
688
  Classifier: Intended Audience :: End Users/Desktop
689
689
  Classifier: Topic :: Games/Entertainment :: Multi-User Dungeons (MUD)
690
690
  Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
@@ -707,7 +707,7 @@ Requires-Dist: prompt-toolkit
707
707
  # PyMUD - Python原生MUD客户端
708
708
  ## 简介
709
709
 
710
- ### 帮助文件见北侠WIKI: https://www.pkuxkx.net/wiki/tools/pymud
710
+ ### 北侠WIKI: https://www.pkuxkx.net/wiki/tools/pymud
711
711
  ### 源代码地址: https://github.com/crapex/pymud
712
712
  ### 帮助文档地址: https://pymud.readthedocs.org
713
713
  ### PyPi项目地址: https://pypi.org/project/pymud
@@ -729,9 +729,20 @@ Requires-Dist: prompt-toolkit
729
729
  + Python拥有极为丰富的第三方库,能支持的第三方库,就能在PyMud中支持
730
730
  + 我自己还在玩,所以本客户端会持续进行更新:)
731
731
 
732
+ ### 哪些人适合使用PyMUD
733
+ + 比较熟悉Python语言,会使用Python写代码的 -> PyMUD是纯Python原生开发,不会有其他客户端对Python的支持能比得过PyMUD
734
+ + 虽不太熟悉Python语言,但有想法想学习Python语言的 -> 正好使用PyMUD玩北侠写脚本的过程中学习Python语言
735
+ + 觉得还有些功能现在所有客户端都没有的 -> 你有需求,我来增加,就是这么方便
736
+ + 觉得也想自己整一个定制客户端玩玩的 -> PyMUD完全开源,且除ui框架外全部都是一行一行代码自己写的,可以直接参考PyMUD的设计
737
+
732
738
  ## 版本更新信息
733
739
 
734
- ## 0.20.0 (2024-08-XX)
740
+ ### 0.20.1 (2024-11-16)
741
+ + 功能调整: 会话中触发器匹配实现进行部分调整,减少循环次数以提高响应速度
742
+ + 功能调整: #test / #show 触发器测试功能调整,现在会对使能的和未使能的触发器均进行匹配测试。其中,#show 命令仅测试,而 #test 命令会导致触发器真正响应。
743
+ + 功能新增: pymud对象新增了一个持续运行的1s的周期定时任务。该任务中会刷新页面显示。可以使用 session.application.addTimerTickCallback 和 session.application.removeTimerTickCallback 来注册和解除定时器回调。
744
+
745
+ ## 0.20.0 (2024-08-25)
735
746
  + 功能调整: 将模块主入口函数从__main__.py中移动到main.py中,以使可以在当前目录下,可直接使用pymud,也可使用python -m pymud启动
736
747
  + 功能调整: 使用argsparser标准模块来配置命令行,可以使用 pymud -h 查看命令行具体参数及说明
737
748
  + 功能新增: 命令行参数增加指定启动目录的功能,参数为 -s, --startup_dir。即可以从任意目录通过指定脚本目录方式启动PyMUD了。
@@ -768,11 +779,6 @@ Requires-Dist: prompt-toolkit
768
779
  - 使用示例:
769
780
 
770
781
  ```Python
771
- # 所有对象均可以使用 addObject 直接添加到会话中,而不用管是什么具体类型
772
- session.addObject(Timer(...))
773
- session.addObject(Trigger(...))
774
- session.addObject(Alias(...))
775
-
776
782
  # 所有对象均可以使用 delObject 直接从会话中移除,会自动根据对象类型推断,无需通过函数名区分
777
783
  session.delObject(self.tri1)
778
784
  session.delObject(self.ali1)
@@ -786,7 +792,6 @@ Requires-Dist: prompt-toolkit
786
792
  GMCPTrigger(session, xxx)
787
793
  ]
788
794
 
789
- session.addObjects(objs) # 可以直接将一个数组中所有对象添加到会话中,会自动判断各对象类别
790
795
  session.delObjects(objs) # 可以直接从会话中移除一个数组中的所有对象,会自动判断对象类别
791
796
  ```
792
797
 
@@ -795,7 +800,9 @@ Requires-Dist: prompt-toolkit
795
800
  - 例如, result = await self.session.cmds.cmd_runto.execute('rt yz') 与 result = await self.session.exec_async('rt yz') 等价,返回值相同
796
801
  - 但 result = await self.session.exec_async('rt yz;dzt'),该返回的result 仅是 dzt 命令的 execute 的返回值。 rt yz 命令返回值被丢弃。
797
802
  + 功能新增: 增加临时变量概念,变量名以下划线开头的为临时变量,此类变量不会被保存到 .mud 文件中。
798
-
803
+ + 功能新增: 为 BaseObject 基类的 self.session 增加了 Session 类型限定,现在自定义 Command 等时候,使用 self.session 时会有 IntelliSence 函数智能提示了,所有帮助说明已补全
804
+ + 问题修复: 修复 #var 等命令中,若含有中文则等号位置不对齐的问题
805
+ + 功能调整: 在 #tri 等命令中,当对象的 group 为空时,将不再显示 group 属性,减少无用信息
799
806
 
800
807
  ## 0.19.4 (2024-04-20)
801
808
  + 功能调整: info 现在 msg 恢复为可接受任何类型参数,不一定是 str
@@ -0,0 +1,19 @@
1
+ pymud/__init__.py,sha256=AP4Edhx90gMKrNfD1O_KVciA3SOnyX5Qt9fZY_JhsTY,574
2
+ pymud/__main__.py,sha256=hFzZjadLlcOuoLM7D8wFiFVO8mqF7vMuo9y-9xfIhRc,64
3
+ pymud/dialogs.py,sha256=p-LidObSuDyOeMif5CsqhF5qq3rizZ1lmThWHrxDyRg,6726
4
+ pymud/extras.py,sha256=Gr-gX7YRWZMmeKV73sk7h_Gf5eZVJ6GcAXvSEfJ4uMI,41124
5
+ pymud/logger.py,sha256=gtGm8y9RY_CpRpJ0udgKknRxyjsEPrrRyWecUDgBgZM,5662
6
+ pymud/main.py,sha256=b_Ui_cN4W8IfhYNyc1duwr3Bp7pYYZQusKTSafCWZIA,6534
7
+ pymud/modules.py,sha256=XoqTeYfZCgqDsV3SYxeehzsbkTzs0swelAUIxyWuL9g,7423
8
+ pymud/objects.py,sha256=hJc_GfvSUcAqXSHIWKlOg8yOKHnCK9aaoWDwAuP1YtA,39377
9
+ pymud/pkuxkx.py,sha256=jRQRUs2xtw7GzYHtLYZXOASnqMumKh0iCoOeKZs8NnU,11467
10
+ pymud/protocol.py,sha256=QfDXjlg2OcJXmVoXf_3mAemnYotRXDUlEZNQjhkfXdA,49106
11
+ pymud/pymud.py,sha256=pkTb21UV2ccYhG44JjfGdvUSPIWbu_y-vUwBqqzeHaM,51425
12
+ pymud/session.py,sha256=GAxenTtUy75A-LAfznMOx753BWd-tyozgngeNdJux4Q,135208
13
+ pymud/settings.py,sha256=ZsKGkjcNGNwqeb2DztwhIrCcRDPYYN1SxtdjWNAJ9CY,7145
14
+ pymud-0.20.1.dist-info/LICENSE.txt,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
15
+ pymud-0.20.1.dist-info/METADATA,sha256=Mhl90M3VE2V8zoZ82q5ySzDIknD2XNWlXPdxSvoNuF8,74344
16
+ pymud-0.20.1.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
17
+ pymud-0.20.1.dist-info/entry_points.txt,sha256=diPUOtTkhgC1hVny7Cdg4aRhaHSynMQoraE7ZhJxUcw,37
18
+ pymud-0.20.1.dist-info/top_level.txt,sha256=8Gp1eXjxixXjqhhti6tLCspV_8s9sNV3z5Em2_KRhD4,6
19
+ pymud-0.20.1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (73.0.1)
2
+ Generator: setuptools (75.5.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,19 +0,0 @@
1
- pymud/__init__.py,sha256=AP4Edhx90gMKrNfD1O_KVciA3SOnyX5Qt9fZY_JhsTY,574
2
- pymud/__main__.py,sha256=hFzZjadLlcOuoLM7D8wFiFVO8mqF7vMuo9y-9xfIhRc,64
3
- pymud/dialogs.py,sha256=D0ZtCeoBchF5eYzXumkOi3p-maCQZu4v9-wJgxQ790o,6500
4
- pymud/extras.py,sha256=QwWwLavVtuXfg0Qb0f_040va1_kej27P-ZB_19HB6Qk,42422
5
- pymud/logger.py,sha256=elYfbpvmKYJfB-rnPYZWY5r8ROu9yja9t-dBi1faRGc,5358
6
- pymud/main.py,sha256=b_Ui_cN4W8IfhYNyc1duwr3Bp7pYYZQusKTSafCWZIA,6534
7
- pymud/modules.py,sha256=GoNmGnMfi2BEsonJA4iq-unkgHkZRR-XBr1Wix_3-a4,2756
8
- pymud/objects.py,sha256=5lx9Z9ijrwpqy2KNJu0OyUxXVh_VVYP2PTE82inAW9c,38623
9
- pymud/pkuxkx.py,sha256=jRQRUs2xtw7GzYHtLYZXOASnqMumKh0iCoOeKZs8NnU,11467
10
- pymud/protocol.py,sha256=QfDXjlg2OcJXmVoXf_3mAemnYotRXDUlEZNQjhkfXdA,49106
11
- pymud/pymud.py,sha256=N9WxaHDqqsTWIBG8UJ38gdMC_pQ30wphcN2LtT66eA4,49584
12
- pymud/session.py,sha256=3u_ctb3ev5MPYfB7-vrLDd9ag9yw1z-K24hQ40hSrME,129365
13
- pymud/settings.py,sha256=CRNpHl4RjOuQYzAIWyUEvOo7q-f4lo01X1v_CriHWO4,7145
14
- pymud-0.20.0a5.dist-info/LICENSE.txt,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
15
- pymud-0.20.0a5.dist-info/METADATA,sha256=kaZCuQ8_FyybVKP0c7ksj_jk_qksVwda-YEgSCsmqT4,73061
16
- pymud-0.20.0a5.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91
17
- pymud-0.20.0a5.dist-info/entry_points.txt,sha256=diPUOtTkhgC1hVny7Cdg4aRhaHSynMQoraE7ZhJxUcw,37
18
- pymud-0.20.0a5.dist-info/top_level.txt,sha256=8Gp1eXjxixXjqhhti6tLCspV_8s9sNV3z5Em2_KRhD4,6
19
- pymud-0.20.0a5.dist-info/RECORD,,